Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 67 additions & 0 deletions changes/2026-10-objective-cpp-and-swiftpm.md
Original file line number Diff line number Diff line change
@@ -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`.
10 changes: 5 additions & 5 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()`
Expand Down
10 changes: 5 additions & 5 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
7 changes: 7 additions & 0 deletions src/panopticas/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -182,6 +186,7 @@
"Kotlin",
"Makefile",
"Objective-C",
"Objective-C++",
"Perl",
"PHP",
"PowerShell",
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/tests/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/tests/Package.swift
Original file line number Diff line number Diff line change
@@ -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"),
]
)
8 changes: 8 additions & 0 deletions src/tests/file.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import <Foundation/Foundation.h>
#include <vector>

int main() {
std::vector<int> v;
NSLog(@"%lu", v.size());
return 0;
}
38 changes: 38 additions & 0 deletions tests/test_panopticas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down
Loading