Skip to content

Detect linter, formatter and type checker config files - #34

Merged
sabbaticas merged 1 commit into
mainfrom
feat/linter-detection
Oct 5, 2026
Merged

sabbaticas merged 1 commit into
mainfrom
feat/linter-detection

Conversation

@sabbaticas

Copy link
Copy Markdown
Contributor

Adds detection for linter, formatter and type checker configuration files and their ignore counterparts — 30 tools, 96 exact filenames across JavaScript, TypeScript, CSS, Python, Go, Ruby, Java, Groovy, C#/.NET and SQL.

Every convention was verified against the tool's current official documentation. docs/linter-detection-rules.md records the inventory, the source for each tool, and what was rejected.

Coverage

Language Tools
Go golangci-lint, staticcheck
Ruby RuboCop, Standard, Reek, Rufo, erb_lint, Steep, Sorbet
Python Ruff, Flake8, Pylint, mypy, Pyright, isort, yapf, Prospector
JavaScript / TypeScript / CSS ESLint, Prettier, Stylelint, Biome, oxlint, JSHint, TypeScript, EditorConfig
Java Checkstyle
Groovy npm-groovy-lint
C# / .NET Roslyn, StyleCop
SQL SQLFluff

Eight tools have an ignore counterpart: ESLint, Prettier, Stylelint, JSHint, yapf, RuboCop, Standard, SQLFluff.

Why a table rather than 96 literal rules

LINTER_RULES describes each tool once — languages, roles, and the files that identify it — and is expanded into the existing METADATA_RULES tables at import.

Prettier alone accepts 18 config filenames carrying identical tags, so the literal form is 18 near-duplicate lines. More importantly the table makes the tag shape structurally guaranteed rather than a convention each entry has to remember — which is exactly what went wrong between the two pre-existing rules, where eslint.config.js tagged eslint and .sqlfluff tagged SQLFluff, one lowercase and one brand-cased, one with a config role and one without.

core.py and cli.py needed no change for this: get_tags() reads the expanded tables, and the existing vocabulary drift test picked up all 96 rules.

Eleven candidates rejected

Each would have been a wrong rule, and a wrong rule mislabels repositories:

  • revive auto-discovers only $HOME/revive.toml — a user-level path, not a repository artifact.
  • SpotBugs, PMD, CodeNarc — filter and ruleset files are arbitrarily named and passed on the command line. This is why Java and Groovy coverage is thin; it is the ceiling, not an omission.
  • Black, StandardJS, Spotless — no dedicated config file.
  • gofmt, gofumpt, go vet — no configuration at all.
  • pycodestyle — its dedicated file is user-level.
  • dprint — plausible, but the filename list could not be confirmed from official docs. Left out pending verification.

pyproject.toml, setup.cfg, tox.ini and package.json genuinely hold linter config, but which tool is inside is not knowable from a path, and detection here is path-based only. Tagging them would mislabel nearly every repository that has one. Negative tests assert all of the above stays true.

Two fixes this surfaced

panopticas file ignored basename mappings. It built its File type row from get_extension_filetype(), which consults only EXT_FILETYPES. panopticas file go.mod showed a blank file type while assess showed go.mod; setup.cfg was blank the same way. Now reads get_language(skip_shebang=True), so both commands agree — skip_shebang keeps the row distinct from Shebang Language, and the UNKNOWN sentinel is normalised to null for the JSON contract. Across the 96 config filenames the row went from 55/96 resolved to 96/96.

One unreadable file aborted a whole scan. check_shebang() caught FileNotFoundError and UnicodeDecodeError but not the other ways open() fails, so a directory raised IsADirectoryError and a single unreadable file killed an entire panopticas assess run with PermissionError. Now catches (OSError, ValueError) — every filesystem refusal, plus a NUL byte in the path. panopticas urls had the identical bug one layer up, fixed in the CLI loop so extract_urls_from_file()'s documented raising behaviour stays intact. Passing a filename that does not exist on disk was already safe and still is.

Also: four missing extensions

.mjs and .cjs are now JavaScript, .mts and .cts TypeScript. These were absent from EXT_FILETYPES entirely, so every file using them reported Unknown — unrelated to linters, but they surfaced here because config authors use them to escape the "type" field in package.json.

Matches GitHub Linguist, which types all four as programming.

kospex impact

Tags grew 85 → 118, filetypes 76 → 86. Every tag change is an addition or case-only (eslint → ESLint), and SQLite LIKE is case-insensitive, so stored queries still match. Nothing was renamed in a way that changes matching and nothing removed. A pin bump is needed only to pick up the new tags, which requires a re-sync — last_panopticas_version handles that.

Testing

686 tests pass. tests/test_linter_detection.py is new (141 tests): table integrity, expansion into each METADATA_RULES table, tags per language, the config/ignore split, filetype resolution for all 96 filenames, and negative guards for each rejected convention. Nine robustness tests added across test_panopticas.py and test_cli_json.py, eight of which failed before the fixes.

Version bumped to 0.0.20; CHANGELOG left as [Unreleased].

🤖 Generated with Claude Code

Add LINTER_RULES covering 30 tools and 96 exact filenames across
JavaScript, TypeScript, CSS, Python, Go, Ruby, Java, Groovy, C#/.NET and
SQL, with their ignore counterparts. Every convention was verified
against the tool's current official documentation.

LINTER_RULES describes each tool once — languages, roles, and the files
that identify it — and is expanded into the existing METADATA_RULES
tables at import. Prettier alone accepts 18 config filenames carrying
identical tags, so the literal form would be 18 near-duplicate lines; the
table also makes the tag shape structurally guaranteed rather than a
convention each entry has to remember, which is what went wrong between
the pre-existing `eslint` and `SQLFluff` rules. core.py and cli.py needed
no change: get_tags() reads the expanded tables, and the existing
vocabulary drift test picked up all 96 rules.

Eleven candidates were investigated and deliberately rejected, each of
which would have been a wrong rule — revive auto-discovers only
$HOME/revive.toml, a user-level path; SpotBugs, PMD and CodeNarc take
arbitrarily-named files passed on the command line; Black, StandardJS
and Spotless have no dedicated config file. pyproject.toml, setup.cfg,
tox.ini and package.json really do hold linter config, but which tool is
inside is not knowable from a path, so tagging them would mislabel nearly
every repository that has one. Negative tests assert all of this stays
true, and docs/linter-detection-rules.md records the sources.

Tags grew 85 -> 118, filetypes 76 -> 86. All tag changes are additions or
case-only (`eslint` -> `ESLint`), and SQLite LIKE is case-insensitive, so
kospex's stored queries still match; a pin bump is needed only to pick up
the new tags via re-sync.

Two fixes this surfaced:

- panopticas file reported no file type for any file typed by basename
  rather than by extension, because it read get_extension_filetype(),
  which consults only EXT_FILETYPES. `panopticas file go.mod` showed a
  blank file type while `assess` showed `go.mod`. The row now reads
  get_language(skip_shebang=True), so both commands agree; skip_shebang
  keeps it distinct from the Shebang Language row, and the UNKNOWN
  sentinel is normalised to null for the JSON contract.

- check_shebang() caught only FileNotFoundError and UnicodeDecodeError,
  so a directory raised IsADirectoryError and a single unreadable file
  aborted an entire `panopticas assess` scan with PermissionError. It now
  catches (OSError, ValueError), covering every filesystem refusal plus a
  NUL byte in the path. `panopticas urls` had the identical bug one layer
  up and is fixed in the CLI loop, leaving extract_urls_from_file()'s
  documented raising behaviour intact. Passing a filename that does not
  exist on disk was already safe and still is.

Also adds .mjs and .cjs as JavaScript and .mts and .cts as TypeScript,
which were missing from EXT_FILETYPES entirely — unrelated to linters,
but every file using them reported Unknown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sabbaticas
sabbaticas merged commit 7bb832f into main Oct 5, 2026
4 checks passed
@sabbaticas
sabbaticas deleted the feat/linter-detection branch October 5, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant