What happened?
Two related bugs in fallow 2.57.0's handling of circular-dependency suppressions, found while documenting the suppression matrix for an in-house audit cleanup:
Bug 1 — Line-level inline directive that fallow recommends is a no-op for cycles
Every cycle finding in fallow dead-code --format json ships an actions[] array that includes:
{
"type": "suppress-line",
"auto_fixable": false,
"description": "Suppress with an inline comment above the line",
"comment": "// fallow-ignore-next-line circular-dependency"
}
A reader following this guidance and adding the suggested comment above the offending import line gets:
circular_dependencies | length does not change.
- The directive lands in
stale_suppressions with origin.is_file_level: false.
Confirmed live against a 2-file cycle. Only // fallow-ignore-file circular-dependency (file-level, at the top of either participating file) actually decrements the count.
This is a self-inconsistency — fallow's own machine-readable output recommends a directive form that fallow then refuses to honor. Anyone consuming actions[] programmatically (e.g. fallow fix --dry-run, IDE integrations, audit scripts) will silently produce no-op patches.
Bug 2 — Asymmetric singular/plural slug between inline directives and config rules
The slug for the rule is singular in the inline directive surface and plural in the config-rule surface, with no alias on either side:
| Surface |
Working slug |
Failing slug |
Inline // fallow-ignore-file ... |
circular-dependency |
circular-dependencies |
.fallowrc.json rules: { ... } |
circular-dependencies |
circular-dependency |
Both surfaces silently accept the wrong slug — no parse error, no warning, no stale_suppressions entry. The directive/rule just has no effect. Schema confirms only circular-dependencies is a valid RulesConfig key:
$ curl -s https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json \
| jq '.["$defs"].RulesConfig.properties | keys[] | select(test("circular"))'
"circular-dependencies"
Evidence that the singular config form is genuinely consumed-and-treated-as-known by some users: #255's "Alternatives considered" section refers to rules.circular-dependency: "off" (singular) as if it were the working-but-too-coarse escape hatch. It isn't — only the plural form decrements the cycle count globally.
Either form should work, or the wrong form should be a parse warning. Silent no-op is the worst combination.
Reproduction
Two-file cycle is enough; works in any TS project. Setup:
// src/a.ts
import { B } from './b';
export class A { b?: B }
// src/b.ts
import { A } from './a';
export class B { a?: A }
// src/main.ts
import { A } from './a';
import { B } from './b';
console.log(new A(), new B());
# Baseline
$ fallow dead-code --format json --quiet | jq '{cycles: (.circular_dependencies|length), stale: .stale_suppressions}'
{ "cycles": 1, "stale": [] }
# --- Bug 1: line-level singular (the form actions[].comment recommends) ---
$ printf '// fallow-ignore-next-line circular-dependency\n%s' "$(cat src/a.ts)" > src/a.ts
$ fallow dead-code --format json --quiet | jq '{cycles: (.circular_dependencies|length), stale: .stale_suppressions}'
{ "cycles": 1, "stale": [{ "path": "src/a.ts", "line": 2, "origin": { "type": "comment", "issue_kind": "circular-dependency", "is_file_level": false }}] }
# ❌ count unchanged, directive recognized as stale
# --- Working baseline: file-level singular at top of either file ---
$ git checkout src/a.ts
$ printf '// fallow-ignore-file circular-dependency\n%s' "$(cat src/a.ts)" > src/a.ts
$ fallow dead-code --format json --quiet | jq '{cycles: (.circular_dependencies|length)}'
{ "cycles": 0 }
# ✅ file-level (singular) works
# --- Bug 2a: file-level plural is silently ignored ---
$ git checkout src/a.ts
$ printf '// fallow-ignore-file circular-dependencies\n%s' "$(cat src/a.ts)" > src/a.ts
$ fallow dead-code --format json --quiet | jq '{cycles: (.circular_dependencies|length), stale: .stale_suppressions}'
{ "cycles": 1, "stale": [] }
# ❌ count unchanged, no stale entry
# --- Bug 2b: config rule singular is silently ignored ---
$ git checkout src/a.ts
$ jq '.rules += {"circular-dependency": "off"}' .fallowrc.json > .fallowrc.json.tmp && mv .fallowrc.json.tmp .fallowrc.json
$ fallow dead-code --format json --quiet | jq '{cycles: (.circular_dependencies|length)}'
{ "cycles": 1 }
# ❌ count unchanged
# --- Working: config rule plural ---
$ jq '.rules = {"circular-dependencies": "off"}' .fallowrc.json > .fallowrc.json.tmp && mv .fallowrc.json.tmp .fallowrc.json
$ fallow dead-code --format json --quiet | jq '{cycles: (.circular_dependencies|length)}'
{ "cycles": 0 }
# ✅ config rule (plural) works
Expected behavior
Either:
- Make the line-level form work. Honor
// fallow-ignore-next-line circular-dependency for cycles whose anchor (the line/col reported in the cycle finding) is on the next line. The current actions[].suppress-line.comment then matches reality. This is the form most users will reach for first because (a) it's what fallow itself suggests and (b) it doesn't over-suppress when a file is in multiple cycles.
- Or stop recommending it. Remove the
suppress-line action from circular-dependency cycle findings (or replace it with a suppress-file action that emits // fallow-ignore-file circular-dependency), so the JSON output matches what's honored.
For the slug asymmetry: either accept both singular and plural on each surface (alias), or emit a stale_suppressions warning (or a [2m WARN log) when the unrecognized form is used so the silent failure becomes visible.
Actual behavior
actions[].comment recommends a directive that fallow then refuses to honor.
- Wrong-surface slug (plural inline, singular config rule) is a silent no-op with no diagnostic.
Suggested fix
Smallest patch for #1: when emitting the cycle finding, switch suppress-line to suppress-file and adjust the comment to // fallow-ignore-file circular-dependency. This matches what is honored in 2.57.0 today and removes the false promise.
Bigger fix: implement line-level suppression for circular-dependency by checking, at the cycle-anchor file/line, for an immediately-preceding fallow-ignore-next-line circular-dependency directive and skipping the cycle if found.
For #2: alias the rule key in RulesConfig and the inline-directive parser so both forms resolve to the same enum variant. Cheap, eliminates a whole class of "why isn't this working" support tickets.
Related
Fallow version
2.57.0
Operating system
Linux (Ubuntu)
Configuration
Minimal, shown above.
What happened?
Two related bugs in fallow 2.57.0's handling of
circular-dependencysuppressions, found while documenting the suppression matrix for an in-house audit cleanup:Bug 1 — Line-level inline directive that fallow recommends is a no-op for cycles
Every cycle finding in
fallow dead-code --format jsonships anactions[]array that includes:{ "type": "suppress-line", "auto_fixable": false, "description": "Suppress with an inline comment above the line", "comment": "// fallow-ignore-next-line circular-dependency" }A reader following this guidance and adding the suggested comment above the offending import line gets:
circular_dependencies | lengthdoes not change.stale_suppressionswithorigin.is_file_level: false.Confirmed live against a 2-file cycle. Only
// fallow-ignore-file circular-dependency(file-level, at the top of either participating file) actually decrements the count.This is a self-inconsistency — fallow's own machine-readable output recommends a directive form that fallow then refuses to honor. Anyone consuming
actions[]programmatically (e.g.fallow fix --dry-run, IDE integrations, audit scripts) will silently produce no-op patches.Bug 2 — Asymmetric singular/plural slug between inline directives and config rules
The slug for the rule is singular in the inline directive surface and plural in the config-rule surface, with no alias on either side:
// fallow-ignore-file ...circular-dependencycircular-dependencies.fallowrc.jsonrules: { ... }circular-dependenciescircular-dependencyBoth surfaces silently accept the wrong slug — no parse error, no warning, no
stale_suppressionsentry. The directive/rule just has no effect. Schema confirms onlycircular-dependenciesis a validRulesConfigkey:Evidence that the singular config form is genuinely consumed-and-treated-as-known by some users: #255's "Alternatives considered" section refers to
rules.circular-dependency: "off"(singular) as if it were the working-but-too-coarse escape hatch. It isn't — only the plural form decrements the cycle count globally.Either form should work, or the wrong form should be a parse warning. Silent no-op is the worst combination.
Reproduction
Two-file cycle is enough; works in any TS project. Setup:
Expected behavior
Either:
// fallow-ignore-next-line circular-dependencyfor cycles whose anchor (theline/colreported in the cycle finding) is on the next line. The currentactions[].suppress-line.commentthen matches reality. This is the form most users will reach for first because (a) it's what fallow itself suggests and (b) it doesn't over-suppress when a file is in multiple cycles.suppress-lineaction fromcircular-dependencycycle findings (or replace it with asuppress-fileaction that emits// fallow-ignore-file circular-dependency), so the JSON output matches what's honored.For the slug asymmetry: either accept both singular and plural on each surface (alias), or emit a
stale_suppressionswarning (or a[2m WARNlog) when the unrecognized form is used so the silent failure becomes visible.Actual behavior
actions[].commentrecommends a directive that fallow then refuses to honor.Suggested fix
Smallest patch for #1: when emitting the cycle finding, switch
suppress-linetosuppress-fileand adjust the comment to// fallow-ignore-file circular-dependency. This matches what is honored in 2.57.0 today and removes the false promise.Bigger fix: implement line-level suppression for
circular-dependencyby checking, at the cycle-anchor file/line, for an immediately-precedingfallow-ignore-next-line circular-dependencydirective and skipping the cycle if found.For #2: alias the rule key in
RulesConfigand the inline-directive parser so both forms resolve to the same enum variant. Cheap, eliminates a whole class of "why isn't this working" support tickets.Related
overrides[].rules.circular-dependency: "off"should suppress whole cycles whose files all match the override glob. #255 —overrides[].rules.circular-dependency: "off"should suppress whole cycles whose files all match the override glob. Same area; complementary feature gap. The three together would give users a graduated escape ladder (line → file → folder → global) for cycle false positives.Fallow version
2.57.0
Operating system
Linux (Ubuntu)
Configuration
Minimal, shown above.