8000 Four issues reported against 1.4.0rc2 by devill · Pull Request #144 · habit-hooks/habit-hooks · GitHub
[go: up one dir, main page]

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
62 changes: 62 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,67 @@
# Changelog

## 1.4.0rc3

Reported by @imalliaras (#142) and @FrankRaiser (#140, #143). None of them is a
Windows bug: all three reproduce on any platform, and were found there only
because that is where these two were running.

### Fixed

- **A sensor no longer dies without a word when its tool has a lot to say.**
Node caps a captured stream at 1 MB unless told otherwise, and the eslint
sensor never told it otherwise: past that, the spawn came back with no exit
status, a truncated report and an empty stderr, and the sensor forwarded the
empty stderr. The whole notice was a command line. Around 100 files with
findings is enough to cross it, on any platform (#142). What a project tool's
run answered — how much of it there may be, whether it broke, and what to say
about it — is now one seam's question rather than each caller's, so the two
callers cannot answer it differently again. A complaint can no longer be empty.

- **`--all` no longer scans what git ignores.** Every git-derived mode asks git
and so had never seen a build artifact; a whole-project run walked the
directory tree and measured everything it found. One project, two universes:
a real pnpm monorepo held 321 tracked `.ts`/`.tsx` files and 843 on disk, and
its owner had to hand-write `!**/dist/**` into `[files]` before a run was
usable (#142). A project git cannot answer for — outside a repository, on a
machine with no git, or one that is itself ignored by the repository around it
— still measures what is on disk, because a run that silently scans nothing is
worse than one that over-scans. A submodule's files leave the scope, as they
always had from every git-derived mode, and the run now says which submodule
rather than quietly measuring less.

- **A scan that measured nothing says so.** A `[files]` that matches no file —
because it is too narrow, or because git ignores the tree it was written for —
used to render the clean guide over a run that had read nothing at all.

- **A smell is coached once, not once per sensor that saw it.** Two sensors can
report one smell — eslint's `max-lines` and the line counter both mean
`oversized-file` — and the mapper printed a block per finding, so the same
~200-word guide arrived twice (#140). Findings that resolve to the same guide
are now one finding. A sensor's own issue list is never thinned: two unused
variables declared on one line are still two.

- **habit-hooks no longer reports its own tools as unused dependencies.** It
asks a project to install `eslint`, `knip`, `ts-morph`, `jscpd` and the two
`@typescript-eslint` packages, and then reported all of them as declared and
imported nowhere (#143). The shipped knip config overlooks them. A project
with its own knip config is unaffected, as it is for every other setting.

- **A failing tool's own words are bounded within a line, not only across
lines.** The notice quoted back at most 20 lines and any number of bytes, and
`eslint -f json` is one line however many megabytes it holds — so a sensor
that died mid-report put the whole report into a coding agent's context. Both
ends of a long line now survive, with the middle named.

- **A `[runners]` command nobody installed answers in one line.** Routing a smell
to an executable guide instead of a Markdown one, and naming a command that is
not there, printed a Python stack trace — the same first-contact mistake #114
answered everywhere else, in the one place that sweep did not reach.

- **The knip sensor reports a column under the name the contract gives it.**
knip spells it `col`; every other sensor and `docs/sensor-interface.spec.md`
spell it `column`, so the position was invisible to anything asking by name.

## 1.4.0rc2

Native Windows support. Reported by @FrankRaiser (#133) and @imalliaras (#134),
Expand Down
252 changes: 252 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ mapping, config validation) are resolved and recorded above / in
PHP-specific to say about an unused local.
- **`tests/test_catalogue_coverage.py` is the gate that stops the gap
reopening.** It routes every smell in `catalogue.DEFAULT_SEVERITY` through the
real `rendering._resolve_guide` against the full installed plugin set and fails
real `rendering.resolve_guide` against the full installed plugin set and fails
if any renders `uncoached.md`. "Somebody notices missing coaching" is now a red
build, not a bug report.
- **`unused-export` stays `enforced` — deliberately, and this records why.** The
Expand Down
74 changes: 73 additions & 1 deletion docs/habit-sensors.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1254,12 +1254,17 @@ command = "cat ${dir}/clean.json"
{ "compilerOptions": { "strict": true } }
```

This project's `[files]` names `**/*.py` and it has no Python in it, so the run
also says it measured nothing — a scan that found no files must never be
mistaken for a scan that found no problems.

```bash
habit-sensors --all 2>&1 >/dev/null
```

🖥️ ✅
```text
habit-sensors: nothing matched [files] — check it in .habit-hooks/config.toml, and whether git ignores the paths you expected; nothing scanned
habit-sensors: detected typescript; the typescript plugin is installed but not enabled — add "typescript" to `plugins` in .habit-hooks/config.toml
```

Expand Down Expand Up @@ -1304,14 +1309,24 @@ comes from the `[scope]` config.

| Flag | Scope |
|------|-------|
| `--all` | every file |
| `--all` | every file the project keeps |
| `--file <path>` | a single file |
| `--branch [base]` | changed since this branch left `base` (default `scope.branchBase`) |
| `--last <n>` | changed in the last `n` commits |
| `--since <ref>` | changed since a commit |
| `--config <path>` | use an explicit config file |
| (none) | `scope.changedOnly` → uncommitted; else `scope.autoBranchOffMain` → vs base unless on `scope.mainBranch`; else all |

"Every file the project keeps" means what git keeps: everything tracked, plus
everything just written that is not ignored. So `dist/`, `.next/` and a tool
cache are out of `--all` for the same reason they were always out of `--last 1`
— no mode measures a file another mode cannot see. A submodule is another
repository and gates itself, so its files are not scanned here either; the run
names each one it left out on stderr, so a scan that shrank never passes for a
scan that found nothing wrong. Outside a git repository, or with no git
installed, `--all` falls back to walking the directory tree, because a run that
scanned nothing must never look clean.

A git-mode flag run outside a git repository errors; the config-derived modes
fall back to scanning every file instead. A ref a *real* repository does not have
is an error too: git answers a ref it never heard of with an empty diff, and a
Expand Down Expand Up @@ -1515,6 +1530,63 @@ habit-sensors --all
habit-sensors: no [files] are configured — name what to scan in .habit-hooks/config.toml; nothing scanned
```

### A whole-project scan skips what git ignores

`--all` measures what the project keeps, so a build artifact it told git to
forget is not scanned — even when `[files]` is wide enough to match it. Coaching
a generated file helps nobody: nobody edits it, and the fix is in whatever
generated it (#142).

This case builds its own repository, for the reason the next section explains in
full: without `git init` and a ceiling, git answers about habit-hooks itself.

✏️GIT_CEILING_DIRECTORIES
```text
$PWD/..
```

📄.habit-hooks/config.toml
```toml
plugins = ["generic"]
files = ["**/*.txt"]
```

📄.gitignore
```text
dist/
```

📄src/a.txt
```text
a
```

📄dist/built.txt
```text
built
```

```bash
git init -q -b main . &&
git config user.email spec@example.com &&
git config user.name "Spec Runner" &&
git config commit.gpgsign false &&
git add src .gitignore &&
git commit -q -m baseline
```

`dist/built.txt` is on disk and matches `[files]`, so only git's opinion keeps it
out of the scan.

```bash
habit-sensors --all | jq -c '[.[].issues[].key]'
```

🖥️ ✅
```json
["src/a.txt"]
```

### Git-derived scopes

`--branch`, `--since` and the `[scope]` defaults all ask git one question: what
Expand Down
2 changes: 1 addition & 1 deletion docs/smell-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ map-block), and the smell key each maps to.
| `knip:classMembers`, `knip:enumMembers` | `unused-class-member` |
| `knip:files` | `unused-file` |
| `knip:exports`, `knip:types`, `knip:nsExports`, `knip:nsTypes` | `unused-export` |
| `knip:dependencies` | `unused-dependency` |
| `knip:dependencies`, `knip:devDependencies`, `knip:optionalPeerDependencies` | `unused-dependency` |
| `knip:production:*` | `test-only-dead-code` |
| `eslint:fatal` | `parse-error` |
< 10BC8 /td>
Expand Down
2 changes: 1 addition & 1 deletion plugins/generic/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "habit-hooks-generic"
version = "1.4.0rc2"
version = "1.4.0rc3"
description = "The generic (language-agnostic) Habit Hooks plugin"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion plugins/java/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "habit-hooks-java"
version = "1.4.0rc2"
version = "1.4.0rc3"
description = "The Java Habit Hooks plugin"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion plugins/php/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "habit-hooks-php"
version = "1.4.0rc2"
version = "1.4.0rc3"
description = "The PHP Habit Hooks plugin"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion plugins/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "habit-hooks-python"
version = "1.4.0rc2"
version = "1.4.0rc3"
description = "The Python Habit Hooks plugin"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion plugins/typescript/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "habit-hooks-typescript"
version = "1.4.0rc2"
version = "1.4.0rc3"
description = "The TypeScript Habit Hooks plugin"
readme = "README.md"
license = "MIT"
Expand Down
9 changes: 8 additions & 1 deletion plugins/typescript/src/habit_hooks_typescript/knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@
"tests/**"
],
"project": ["src/**/*.ts!", "src/**/*.tsx!"],
"ignore": ["dist/**"]
"ignore": ["dist/**"],
"ignoreDependencies": [
"eslint",
"ts-morph",
"@typescript-eslint/parser",
"@typescript-eslint/eslint-plugin",
"jscpd"
]
}
40 changes: 26 additions & 14 deletions plugins/typescript/src/habit_hooks_typescript/sensors/eslint.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ const SMELL_BY_RULE = new Map(
// what this smell exists to report, so it is the one kept.
const PARSE_ERROR = "parse-error";

// A smell about the FILE, whose guide lists files rather than lines
// (`includes/file_level_issues.md`). eslint still positions its message —
// `max-lines` reports at the first line past the limit — but that is where its
// counter tripped, not where the problem is, and carrying it is what stopped
// the same file being recognised as one observation when the generic
// `line-count` sensor reported the same smell about it (#1 10BC8 40).
//
// `parse-error` is file-level too and is deliberately absent: its position is
// where parsing actually failed, and no other sensor reports it about a file
// eslint can read, so there is nothing to reconcile and real information to
// lose.
const FILE_LEVEL_SMELLS = new Set(["oversized-file"]);

// The config this plugin ships, beside the sensors directory it runs from.
const SHIPPED_CONFIG = path.join(__dirname, "..", "eslint.config.mjs");

Expand All @@ -61,12 +74,6 @@ const UNSEPARATED_ARGV =
"eslint sensor: its argv must spell '--' between the sensor's arguments and " +
"the scoped files — see sensors/eslint.toml\n";

// eslint takes the tool's usual exit code for findings (1) and reserves
// everything above it for its own breakage; a signal leaves no code at all.
function broke(result) {
return result.status === null || result.status > 1;
}

// Only eslint can say whether the project has a config, because its lookup runs
// from each linted FILE's directory (eslint 10 `lib/config/config-loader.js`) —
// a config below the directory habit-hooks was invoked in is eslint's answer
Expand All @@ -77,7 +84,7 @@ function broke(result) {
// ever change, a config-less project fails loudly rather than being mis-linted
// quietly, which is the direction to be wrong in.
function wantedAConfig(result) {
return broke(result) && (result.stderr || "").includes(NO_CONFIG_FOUND);
return projectTool.broke(result) && (result.stderr || "").includes(NO_CONFIG_FOUND);
}

// `--no-warn-ignored` stops the commonest rule-less message being raised at all.
Expand Down Expand Up @@ -108,13 +115,18 @@ function isReportable(message) {
}

function reported(file, message) {
const smell = smellOf(message);
// The keys stay whichever way, null where there is no position to give: every
// eslint issue's details carry them, and a key that disappears reads
// downstream as a different shape rather than as a missing value.
const positioned = !FILE_LEVEL_SMELLS.has(smell);
return {
smell: smellOf(message),
smell,
key: file,
details: {
file,
line: message.line ?? null,
column: message.column ?? null,
line: positioned ? (message.line ?? null) : null,
column: positioned ? (message.column ?? null) : null,
message: message.message,
source: `eslint:${message.ruleId ?? "fatal"}`,
},
Expand Down Expand Up @@ -169,10 +181,10 @@ function main() {
const files = argv.slice(boundary + 1).filter((file) => LINTABLE.test(file));
if (files.length === 0) return emit([]);
const result = lint(argv.slice(0, boundary), files);
if (broke(result) || result.stdout.trim() === "") {
return refuse(result.stderr || "");
}
return emit(findings(JSON.parse(result.stdout)));
if (projectTool.broke(result)) return refuse(projectTool.complaint(ESLINT, result));
const report = projectTool.readJsonReport(result.stdout);
if (report === null) return refuse(projectTool.unreadableOutput(ESLINT, result));
return emit(findings(report));
}

// Not process.exit(): stdout is a pipe under the runner and writes to it are
Expand Down
Loading
Loading
0