From e2c015e2f91dcbefaa5b36f5bf20ded4fc926d01 Mon Sep 17 00:00:00 2001 From: Peter Freiberg <109841928+sabbaticas@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:00:03 +1100 Subject: [PATCH] Detect C++ source and header extensions beyond .cpp .cc, .cxx and .c++ now report as C++, and .hpp, .hh, .hxx and .h++ as C++ Header, a new language paralleling C Header. Only .cpp was mapped before, so a codebase using .cc and .hpp showed no C++ at all. All seven are in GCC's documented list of C++ suffixes. .h is unchanged: it is shared by C, C++ and Objective-C and cannot be resolved from the path. The changes note lists every C++ suffix considered and left unmapped, with the reason for each. The docs site counts are updated to 89 filetypes and 34 languages. Closes #43 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +- changes/2026-10-cpp-extensions.md | 75 +++++++++++++++++++++++++++++++ docs/api.md | 6 +-- docs/cli.md | 4 +- src/panopticas/constants.py | 8 ++++ src/tests/file.c++ | 6 +++ src/tests/file.cc | 6 +++ src/tests/file.cxx | 6 +++ src/tests/file.h++ | 10 +++++ src/tests/file.hh | 10 +++++ src/tests/file.hpp | 10 +++++ src/tests/file.hxx | 10 +++++ tests/test_panopticas.py | 24 ++++++++++ 13 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 changes/2026-10-cpp-extensions.md create mode 100644 src/tests/file.c++ create mode 100644 src/tests/file.cc create mode 100644 src/tests/file.cxx create mode 100644 src/tests/file.h++ create mode 100644 src/tests/file.hh create mode 100644 src/tests/file.hpp create mode 100644 src/tests/file.hxx diff --git a/CHANGELOG.md b/CHANGELOG.md index bbdd1d4..6a56549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format of this changelog is based on [Keep a Changelog](https://keepachangel - `.kts` now reports as Kotlin and `.gradle` as Groovy. Both were missing from the extension table, so Kotlin scripts and every Gradle build script — `build.gradle`, `settings.gradle`, `build.gradle.kts`, `settings.gradle.kts` — reported `Unknown`. Gradle selects the DSL by extension alone, so the mapping is exact. Repositories built with Gradle will now show a small Groovy or Kotlin count from their build scripts. - `build.gradle.kts` now carries the same `gradle`, `build` and `dependencies` tags as `build.gradle`. A project using the Gradle Kotlin DSL previously looked like it declared no dependencies. - `settings.gradle` and `settings.gradle.kts` are tagged `gradle` and `build`. They declare project structure rather than dependencies, so they do not carry `dependencies`. + - `.cc`, `.cxx` and `.c++` now report as `C++`, and `.hpp`, `.hh`, `.hxx` and `.h++` as `C++ Header`, a new language paralleling `C Header`. Only `.cpp` was mapped before, so the rest reported `Unknown`. `.h` is unchanged and still reports `C Header`. - `.mm` now reports as `Objective-C++`, a new language. It was missing from the extension table and reported `Unknown`. - Swift Package Manager files are recognised. `Package.swift` is tagged `build`, `dependencies`, `Swift` and `SwiftPM`; its lock file `Package.resolved` is tagged `dependencies`, `Swift` and `SwiftPM` and reports as `JSON` instead of `Unknown`. A Swift package previously looked like it declared no dependencies. `Swift` and `SwiftPM` are new tags. - CocoaPods files are recognised. `Podfile` and `Podfile.lock` are tagged `dependencies` and `CocoaPods`, and `Podfile` reports as `Ruby` instead of `Unknown`. @@ -16,7 +17,7 @@ The format of this changelog is based on [Keep a Changelog](https://keepachangel All of these are new tags on previously untagged files, so kospex is compatible either way; a re-sync is needed to pick them up. ### Changed - - The tag vocabulary grew from 119 to 123 tags, the filetype vocabulary from 86 to 88 and the language vocabulary from 32 to 33. + - The tag vocabulary grew from 119 to 123 tags, the filetype vocabulary from 86 to 89 and the language vocabulary from 32 to 34. - The Click floor moved from `>=8.3.1` to `>=8.3.3`, so an install can no longer resolve to a vulnerable Click release. - The package author email is now `peter@kospex.io`. diff --git a/changes/2026-10-cpp-extensions.md b/changes/2026-10-cpp-extensions.md new file mode 100644 index 0000000..4a5d1ce --- /dev/null +++ b/changes/2026-10-cpp-extensions.md @@ -0,0 +1,75 @@ +# C++ Source and Header Extensions + +Closes [#43](https://github.com/kospex/panopticas/issues/43). + +## Summary + +`.cpp` was the only C++ extension in `EXT_FILETYPES`. Every other common +spelling reported `Unknown`: + + file.cc 'Unknown' file.hpp 'Unknown' + file.cxx 'Unknown' file.hh 'Unknown' + file.c++ 'Unknown' file.hxx 'Unknown' + file.h++ 'Unknown' + +A C++ codebase using `.cc` and `.hpp` — a very common pairing — showed no C++ +at all. The gap surfaced during the `.m`/`.h` analysis in #40. + +## Changes + +Seven entries in `EXT_FILETYPES`, and `C++ Header` added to +`LANGUAGE_FILETYPES`: + + ".cc": "C++" ".hpp": "C++ Header" + ".cxx": "C++" ".hh": "C++ Header" + ".c++": "C++" ".hxx": "C++ Header" + ".h++": "C++ Header" + +## Source + +The [GCC documentation](https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html) +lists the suffixes it treats as C++: + +- **Source:** `.cc`, `.cp`, `.cxx`, `.cpp`, `.CPP`, `.c++`, `.C` +- **Header:** `.hh`, `.H`, `.hp`, `.hxx`, `.hpp`, `.HPP`, `.h++`, `.tcc` + +## Design notes + +- **`C++ Header` is a new language name**, paralleling the existing + `C Header`. Headers are reported separately from sources because that is + what panopticas already does for C. Reporting them as plain `C++` was the + alternative; it would have been simpler but inconsistent with `.h`. +- **`.h` is unchanged.** It is shared by C, C++ and Objective-C — GCC's own + entry for it reads "C, C++, Objective-C or Objective-C++ header file" — and + cannot be resolved from the path. That is the subject of #40. +- Additive only: no existing mapping changes, so kospex is unaffected until a + re-sync. + +## Not mapped + +The complete list of C++-related suffixes considered and left out, so none +needs rediscovering. + +| Suffix | What it is | Reports as | Why not mapped | +|---|---|---|---| +| `.C` | C++ source (GCC, literal capital) | `C` | panopticas lowercases the extension before lookup, so it is indistinguishable from `.c` | +| `.H` | C++ header (GCC, literal capital) | `C Header` | Same: indistinguishable from `.h` | +| `.cp` | C++ source (GCC) | `Unknown` | Rare, and short enough to collide with unrelated uses | +| `.hp` | C++ header (GCC) | `Unknown` | Rare | +| `.tcc` | C++ header holding template definitions (GCC) | `Unknown` | Rare | +| `.ii` | C++ source that should not be preprocessed (GCC) | `Unknown` | Compiler output, not something a repository normally holds | +| `.ipp`, `.tpp`, `.inl`, `.txx` | Template and inline implementation files | `Unknown` | Project conventions rather than compiler-defined suffixes; not checked against any documentation | +| `.cppm`, `.ixx`, `.ccm`, `.mpp` | C++20 module interface units | `Unknown` | Vary by compiler; not checked against any compiler's documentation | +| `.h` | C, C++ or Objective-C header | `C Header` | Shared by three languages and not resolvable from the path — see #40 | + +`.CPP` and `.HPP`, which GCC lists separately, need no entry: lowercasing +resolves them to `.cpp` and `.hpp`, so they report `C++` and `C++ Header`. + +The first six rows are from the GCC suffix list above. The template and module +rows are from general knowledge and would need verifying before any of them +became a mapping. + +## Tests + +`tests/test_panopticas.py` covers all seven extensions, asserts `.h` still +reports `C Header`, and has a fixture for each extension in `src/tests/`. diff --git a/docs/api.md b/docs/api.md index ff16f66..28a6dfe 100644 --- a/docs/api.md +++ b/docs/api.md @@ -53,7 +53,7 @@ EXT_FILETYPES["dockerfile"] # "Dockerfile" (special case) `EXT_FILETYPES` mixes two kinds of value: things that are programming languages (`Python`, `Go`) and things that are file types but not languages (`PNG`, `Gitignore`, `Lock`). These two collections classify every value in the table — -33 languages and 55 non-languages. `get_languages()` and `get_filetypes()` read +34 languages and 55 non-languages. `get_languages()` and `get_filetypes()` read from them. ```python @@ -386,7 +386,7 @@ Return every file type panopticas recognises, languages or not. from panopticas.core import get_filetypes get_filetypes() -# Returns: ['Apache JMeter', 'ASP.NET', ..., 'ZIP'] (88 file types) +# Returns: ['Apache JMeter', 'ASP.NET', ..., 'ZIP'] (89 file types) ``` **Returns:** Sorted list of file type strings @@ -399,7 +399,7 @@ Return every file type that is a programming language. from panopticas.core import get_languages get_languages() -# Returns: ['C', 'C Header', 'C#', 'C++', 'CSS', ..., 'Vue'] (33 languages) +# Returns: ['C', 'C Header', 'C#', 'C++', 'C++ Header', ..., 'Vue'] (34 languages) ``` **Returns:** Sorted list of language names — the subset of `get_filetypes()` diff --git a/docs/cli.md b/docs/cli.md index 1dd545c..ae50549 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -396,8 +396,8 @@ count. | Command | Shape | |---|---| | `tags` | `{"tags": [...], "count": 123}` | -| `languages` | `{"languages": [...], "count": 33}` | -| `filetypes` | `{"filetypes": [...], "count": 88}` | +| `languages` | `{"languages": [...], "count": 34}` | +| `filetypes` | `{"filetypes": [...], "count": 89}` | ```console $ panopticas languages --json | jq -r '.count' diff --git a/src/panopticas/constants.py b/src/panopticas/constants.py index 4515e9a..bd2959f 100644 --- a/src/panopticas/constants.py +++ b/src/panopticas/constants.py @@ -20,8 +20,11 @@ EXT_FILETYPES = { ".c": "C", + ".c++": "C++", + ".cc": "C++", ".class": "Java Class", ".cpp": "C++", + ".cxx": "C++", ".cs": "C#", ".cjs": "JavaScript", # CommonJS module, explicit ".cts": "TypeScript", # TypeScript emitting CommonJS @@ -42,6 +45,10 @@ ".groovy": "Groovy", ".gsp": "Groovy Server Pages", ".h": "C Header", + ".h++": "C++ Header", + ".hh": "C++ Header", + ".hpp": "C++ Header", + ".hxx": "C++ Header", ".aspx": "ASP.NET", ".ascx": "ASP.NET User Control", ".htm": "HTML", @@ -179,6 +186,7 @@ "C Header", "C#", "C++", + "C++ Header", "CSS", "Dockerfile", "Go", diff --git a/src/tests/file.c++ b/src/tests/file.c++ new file mode 100644 index 0000000..a5d0677 --- /dev/null +++ b/src/tests/file.c++ @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "hello" << std::endl; + return 0; +} diff --git a/src/tests/file.cc b/src/tests/file.cc new file mode 100644 index 0000000..a5d0677 --- /dev/null +++ b/src/tests/file.cc @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "hello" << std::endl; + return 0; +} diff --git a/src/tests/file.cxx b/src/tests/file.cxx new file mode 100644 index 0000000..a5d0677 --- /dev/null +++ b/src/tests/file.cxx @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "hello" << std::endl; + return 0; +} diff --git a/src/tests/file.h++ b/src/tests/file.h++ new file mode 100644 index 0000000..57a104a --- /dev/null +++ b/src/tests/file.h++ @@ -0,0 +1,10 @@ +#pragma once + +namespace example { + +class Greeter { +public: + void greet() const; +}; + +} // namespace example diff --git a/src/tests/file.hh b/src/tests/file.hh new file mode 100644 index 0000000..57a104a --- /dev/null +++ b/src/tests/file.hh @@ -0,0 +1,10 @@ +#pragma once + +namespace example { + +class Greeter { +public: + void greet() const; +}; + +} // namespace example diff --git a/src/tests/file.hpp b/src/tests/file.hpp new file mode 100644 index 0000000..57a104a --- /dev/null +++ b/src/tests/file.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace example { + +class Greeter { +public: + void greet() const; +}; + +} // namespace example diff --git a/src/tests/file.hxx b/src/tests/file.hxx new file mode 100644 index 0000000..57a104a --- /dev/null +++ b/src/tests/file.hxx @@ -0,0 +1,10 @@ +#pragma once + +namespace example { + +class Greeter { +public: + void greet() const; +}; + +} // namespace example diff --git a/tests/test_panopticas.py b/tests/test_panopticas.py index b02cff6..4af82e1 100644 --- a/tests/test_panopticas.py +++ b/tests/test_panopticas.py @@ -83,6 +83,18 @@ def test_kotlin(self): def test_kotlin_script(self): assert get_extension_filetype(".kts") == "Kotlin" + @pytest.mark.parametrize("ext", [".cc", ".cxx", ".c++"]) + def test_cpp_source_extensions(self, ext): + assert get_extension_filetype(ext) == "C++" + + @pytest.mark.parametrize("ext", [".hpp", ".hh", ".hxx", ".h++"]) + def test_cpp_header_extensions(self, ext): + assert get_extension_filetype(ext) == "C++ Header" + + def test_plain_h_is_still_a_c_header(self): + # .h is shared by C, C++ and Objective-C and is not resolved here. + assert get_extension_filetype(".h") == "C Header" + def test_podfile_is_ruby(self): # CocoaPods loads an extensionless Podfile as Ruby. assert get_extension_filetype("Podfile") == "Ruby" @@ -776,6 +788,18 @@ def test_gradle_kotlin_settings_detected(self, fixtures_exist): def test_objective_cpp_detected(self, fixtures_exist): assert get_language("file.mm") == "Objective-C++" + @pytest.mark.parametrize("filename", [ + "file.cc", "file.cxx", "file.c++", + ]) + def test_cpp_sources_detected(self, fixtures_exist, filename): + assert get_language(filename) == "C++" + + @pytest.mark.parametrize("filename", [ + "file.hpp", "file.hh", "file.hxx", "file.h++", + ]) + def test_cpp_headers_detected(self, fixtures_exist, filename): + assert get_language(filename) == "C++ Header" + def test_podfile_detected(self, fixtures_exist): assert get_language("Podfile") == "Ruby"