From 6b2aeac531e858579d396842e0a2c0e78be2093e Mon Sep 17 00:00:00 2001 From: Peter Freiberg <109841928+sabbaticas@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:51:14 +1100 Subject: [PATCH 1/2] Raise the Click floor to 8.3.3 and update the author email The Click floor moves from >=8.3.1 to >=8.3.3 so an install can no longer resolve to a vulnerable Click release. CLAUDE.md quoted the old floor. The package author email is now peter@kospex.io. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ CLAUDE.md | 2 +- pyproject.toml | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b686b..c71cd07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format of this changelog is based on [Keep a Changelog](https://keepachangel All three are new tags on previously untagged files, so kospex is compatible either way; a re-sync is needed to pick them up. +### Changed + - 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`. + ## 0.0.20 - 2026-10-05 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 32c172d..4e7a85f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -176,7 +176,7 @@ Jekyll copies the file verbatim instead of rendering it — and a nav entry in ## Dependencies -- **Click** (>=8.3.1) — CLI framework +- **Click** (>=8.3.3) — CLI framework - **rich** (>=14.0.0) — Table formatting for CLI output - **pathspec** (>=0.12.1) — Git-style path matching for `.gitignore` support diff --git a/pyproject.toml b/pyproject.toml index 60a5feb..9869c10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,9 +6,9 @@ build-backend = "setuptools.build_meta" name = "panopticas" version = "0.0.21" description = "The panopticas file type and CLI tool." -authors = [{ name = "Peter Freiberg", email = "peter.freiberg@gmail.com" }] +authors = [{ name = "Peter Freiberg", email = "peter@kospex.io" }] dependencies = [ - "Click>=8.3.1", + "Click>=8.3.3", "pathspec>=0.12.1", "rich>=14.0.0", ] From 126c9522204a4426f2bc9ed997e24a877fd25f95 Mon Sep 17 00:00:00 2001 From: Peter Freiberg <109841928+sabbaticas@users.noreply.github.com> Date: Fri, 9 Oct 2026 13:51:14 +1100 Subject: [PATCH 2/2] Detect Objective-C++ and Swift Package Manager files .mm now reports as Objective-C++, a new language. It was missing from EXT_FILETYPES and reported Unknown. Package.swift is tagged build, dependencies, Swift and SwiftPM. Its lock file Package.resolved is tagged dependencies, Swift and SwiftPM and reports as JSON. A Swift package previously looked like it declared no dependencies. Package.resolved is mapped by basename because Carthage's Cartfile.resolved shares the extension and is plain text. The docs site carried vocabulary counts and a version from 0.0.19. They now match the code: 121 tags, 87 filetypes, 33 languages. Closes #38 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +- changes/2026-10-objective-cpp-and-swiftpm.md | 67 ++++++++++++++++++++ docs/api.md | 10 +-- docs/cli.md | 10 +-- src/panopticas/constants.py | 7 ++ src/tests/Package.resolved | 14 ++++ src/tests/Package.swift | 12 ++++ src/tests/file.mm | 8 +++ tests/test_panopticas.py | 38 +++++++++++ 9 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 changes/2026-10-objective-cpp-and-swiftpm.md create mode 100644 src/tests/Package.resolved create mode 100644 src/tests/Package.swift create mode 100644 src/tests/file.mm diff --git a/CHANGELOG.md b/CHANGELOG.md index c71cd07..48161a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,13 @@ 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`. + - `.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. - All three are new tags on previously untagged files, so kospex is compatible either way; a re-sync is needed to pick them up. + 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 121 tags, the filetype vocabulary from 86 to 87 and the language vocabulary from 32 to 33. - 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-objective-cpp-and-swiftpm.md b/changes/2026-10-objective-cpp-and-swiftpm.md new file mode 100644 index 0000000..20e80fb --- /dev/null +++ b/changes/2026-10-objective-cpp-and-swiftpm.md @@ -0,0 +1,67 @@ +# Objective-C++ and Swift Package Manager Detection + +Closes [#38](https://github.com/kospex/panopticas/issues/38). + +## Summary + +`.mm` was not in `EXT_FILETYPES`, and neither Swift Package Manager file was +recognised as a dependency file: + + file.mm [] 'Unknown' + Package.swift [] 'Swift' + Package.resolved [] 'Unknown' + +A Swift package therefore looked like it declared no dependencies, the same gap +`build.gradle.kts` had for the Gradle Kotlin DSL. + +## Changes + +One entry in `EXT_FILETYPES`, and `Objective-C++` added to +`LANGUAGE_FILETYPES`: + + ".mm": "Objective-C++" + +Two entries in `METADATA_RULES["exact_filename_rules"]`: + + "package.swift": ["build", "dependencies", "Swift", "SwiftPM"] + "package.resolved": ["dependencies", "Swift", "SwiftPM"] + +One entry in `LANGUAGE_BY_BASENAME`: + + "package.resolved": "JSON" + +Result: + + file.mm [] 'Objective-C++' + Package.swift ['build', 'dependencies', 'Swift', 'SwiftPM'] 'Swift' + Package.resolved ['dependencies', 'Swift', 'SwiftPM'] 'JSON' + +## Design notes + +- **`Package.resolved` is mapped by basename, not by extension.** It is JSON, + but Carthage's `Cartfile.resolved` shares the `.resolved` extension and is + plain text. A blanket `".resolved" -> JSON` rule would mislabel it. +- **The lock file does not carry `build`.** It pins dependency versions; it + does not describe how to build. This matches `uv.lock` and `pnpm-lock.yaml`. +- **Detection is by filename, so location does not matter.** In an Xcode app + project `Package.resolved` lives inside the project bundle, under + `.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/`, and is tagged there + too. +- **`Swift` and `SwiftPM` are new tags** and `Objective-C++` a new language + name. `Swift` existed as a language but had never been used as a tag. All + are additions, so kospex is unaffected until a re-sync. + +## Not included + +- `.h` still always reports `C Header` and `.m` always `Objective-C`. Neither + an Objective-C header nor a MATLAB `.m` file can be told apart from the path, + and detection is path-based only. +- CocoaPods (`Podfile`, `Podfile.lock`) and Carthage (`Cartfile`, + `Cartfile.resolved`) files are still untagged. + +## Tests + +`tests/test_panopticas.py` covers the extension, the basename filetype, the +tags for both files, the lock file inside an Xcode project path, and guards +that `Cartfile.resolved` and ordinary `.swift` files are not claimed. Fixtures +in `src/tests/`: `file.mm`, `Package.swift` and `Package.resolved`. diff --git a/docs/api.md b/docs/api.md index 45ee656..c0fa107 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,7 +14,7 @@ Everything listed here is exported from the top-level package: import panopticas panopticas.get_language("app.py") # "Python" -panopticas.VERSION # "0.0.19" +panopticas.VERSION # "0.0.21" ``` The examples below import from `panopticas.core` and `panopticas.constants`, which @@ -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 — -32 languages and 44 non-languages. `get_languages()` and `get_filetypes()` read +33 languages and 54 non-languages. `get_languages()` and `get_filetypes()` read from them. ```python @@ -369,7 +369,7 @@ Return every tag panopticas can assign to a file. from panopticas.core import get_tags get_tags() -# Returns: ['.NET', 'agent', 'Agents', 'AI', 'Aider', ..., 'yarn'] (85 tags) +# Returns: ['.NET', 'agent', 'Agents', 'AI', 'Aider', ..., 'yarn'] (121 tags) ``` **Returns:** Sorted list of tag strings, case-insensitively ordered @@ -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'] (76 file types) +# Returns: ['Apache JMeter', 'ASP.NET', ..., 'ZIP'] (87 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'] (32 languages) +# Returns: ['C', 'C Header', 'C#', 'C++', 'CSS', ..., 'Vue'] (33 languages) ``` **Returns:** Sorted list of language names — the subset of `get_filetypes()` diff --git a/docs/cli.md b/docs/cli.md index 39e2f6f..b55c86b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -379,10 +379,10 @@ $ panopticas tags .NET agent Agents AI Aider Amazon Q Apache ASP.NET Augment Azure DevOps -binary Bitbucket build Buildkite C# +binary Biome Bitbucket build Buildkite ... -85 tags +121 tags ``` Values are sorted case-insensitively, so `binary` sorts near `Bitbucket` @@ -395,9 +395,9 @@ count. | Command | Shape | |---|---| -| `tags` | `{"tags": [...], "count": 85}` | -| `languages` | `{"languages": [...], "count": 32}` | -| `filetypes` | `{"filetypes": [...], "count": 76}` | +| `tags` | `{"tags": [...], "count": 121}` | +| `languages` | `{"languages": [...], "count": 33}` | +| `filetypes` | `{"filetypes": [...], "count": 87}` | ```console $ panopticas languages --json | jq -r '.count' diff --git a/src/panopticas/constants.py b/src/panopticas/constants.py index d0b4363..11c9ae8 100644 --- a/src/panopticas/constants.py +++ b/src/panopticas/constants.py @@ -66,6 +66,7 @@ ".mailmap": "Mailmap", ".md": "Markdown", ".mjs": "JavaScript", # ES module, explicit + ".mm": "Objective-C++", ".mts": "TypeScript", # TypeScript emitting an ES module ".nvmrc": "nvmrc", ".pdf": "PDF", @@ -144,6 +145,9 @@ # are mapped by basename for the same reason setup.cfg is. ".isort.cfg": "INI", "staticcheck.conf": "TOML", + # Swift Package Manager's lock file is JSON. Mapped by basename because + # ".resolved" is shared with Carthage's Cartfile.resolved, which is not. + "package.resolved": "JSON", } # Classification of every value in EXT_FILETYPES and LANGUAGE_BY_BASENAME as @@ -182,6 +186,7 @@ "Kotlin", "Makefile", "Objective-C", + "Objective-C++", "Perl", "PHP", "PowerShell", @@ -306,6 +311,8 @@ "uv.lock": ["dependencies", "Python", "uv"], "yarn.lock": ["dependencies", "JavaScript", "yarn", "npm"], "pnpm-lock.yaml": ["dependencies", "JavaScript", "pnpm", "npm"], + "package.swift": ["build", "dependencies", "Swift", "SwiftPM"], + "package.resolved": ["dependencies", "Swift", "SwiftPM"], ".gitattributes": ["Git"], ".gitlab-ci.yml": ["pipeline", "GitLab"], # Three letter YAML extension ".gitlab-ci.yaml": ["pipeline", "GitLab"], # Full four letter YAML extension diff --git a/src/tests/Package.resolved b/src/tests/Package.resolved new file mode 100644 index 0000000..4eae6bf --- /dev/null +++ b/src/tests/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "0000000000000000000000000000000000000000", + "version" : "1.3.0" + } + } + ], + "version" : 2 +} diff --git a/src/tests/Package.swift b/src/tests/Package.swift new file mode 100644 index 0000000..c85e600 --- /dev/null +++ b/src/tests/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "Example", + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), + ], + targets: [ + .executableTarget(name: "Example"), + ] +) diff --git a/src/tests/file.mm b/src/tests/file.mm new file mode 100644 index 0000000..12bf067 --- /dev/null +++ b/src/tests/file.mm @@ -0,0 +1,8 @@ +#import +#include + +int main() { + std::vector v; + NSLog(@"%lu", v.size()); + return 0; +} diff --git a/tests/test_panopticas.py b/tests/test_panopticas.py index 038edde..18479b3 100644 --- a/tests/test_panopticas.py +++ b/tests/test_panopticas.py @@ -83,6 +83,9 @@ def test_kotlin(self): def test_kotlin_script(self): assert get_extension_filetype(".kts") == "Kotlin" + def test_objective_cpp(self): + assert get_extension_filetype(".mm") == "Objective-C++" + def test_ruby(self): assert get_extension_filetype(".rb") == "Ruby" @@ -213,6 +216,14 @@ def test_go_mod(self): def test_go_sum(self): assert get_language_edge_cases("go.sum") == "go.sum" + def test_swiftpm_lock_file_is_json(self): + # Package.resolved is JSON, but ".resolved" is not mapped as a whole: + # Carthage's Cartfile.resolved shares the extension and is plain text. + assert get_language_edge_cases("Package.resolved") == "JSON" + + def test_other_resolved_files_are_not_claimed(self): + assert get_language_edge_cases("Cartfile.resolved") is None + def test_go_mod_in_path(self): assert get_language_edge_cases("/some/path/go.mod") == "go.mod" @@ -404,6 +415,24 @@ def test_gradle_kotlin_dsl_in_subdirectory(self): assert "gradle" in tags assert "dependencies" in tags + def test_swiftpm_manifest(self): + assert set(get_filename_metatypes("Package.swift")) == \ + {"build", "dependencies", "Swift", "SwiftPM"} + + def test_swiftpm_lock_file(self): + # A lock file pins dependencies; it does not build anything. + assert set(get_filename_metatypes("Package.resolved")) == \ + {"dependencies", "Swift", "SwiftPM"} + + def test_swiftpm_lock_file_inside_xcode_project(self): + path = ("App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/" + "Package.resolved") + assert "SwiftPM" in get_filename_metatypes(path) + + def test_ordinary_swift_file_is_not_a_manifest(self): + assert "SwiftPM" not in get_filename_metatypes("Sources/App/Package.swift.bak") + assert get_filename_metatypes("Sources/App/main.swift") == [] + def test_setup_py_in_subdirectory(self): tags = get_filename_metatypes("packages/foo/setup.py") assert "dependencies" in tags @@ -710,6 +739,15 @@ def test_gradle_groovy_settings_detected(self, fixtures_exist): def test_gradle_kotlin_settings_detected(self, fixtures_exist): assert get_language("settings.gradle.kts") == "Kotlin" + def test_objective_cpp_detected(self, fixtures_exist): + assert get_language("file.mm") == "Objective-C++" + + def test_swiftpm_manifest_detected(self, fixtures_exist): + assert get_language("Package.swift") == "Swift" + + def test_swiftpm_lock_file_detected(self, fixtures_exist): + assert get_language("Package.resolved") == "JSON" + def test_java_class_detected(self, fixtures_exist): assert get_language("somefile.class") == "Java Class"