diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b686b..48161a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,15 @@ 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`. ## 0.0.20 - 2026-10-05 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/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/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", ] 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"