Repository navigation
v3.33.0: fallow architecture, baseline pruning, faster musl binaries
Features
fallow architecturereports import cycles, boundary violations and rule-pack policy violations in one command. Use--cycles,--boundariesor--policyto select one kind. The command takes the same scope and output flags asfallow dead-code. Runfallow guard <files>before an edit andfallow architectureafter it. Barefallowshows these findings in an "Architecture" category after the other dead-code categories. It also shows the category with--quiet,--group-byand--summary. With--group-by,fallow architectureshows one "Architecture" heading per group.fallow architecture --format jsonwriteskind: "architecture"with its ownschema_version(1). With--group-by, the kind isarchitecture-grouped. The arrays, finding ids, actions, exit codes, gate outcomes and severities are the same as onfallow dead-code.next_stepsnamesfallow architecture. With--explain,_meta.docspoints to the architecture page, and_meta.ruleslists only the architecture rules.--save-baselinewrites a dead-code baseline (kind: "dead-code"), so the two commands read each other's baselines. The JSON schema and the generated TypeScript types have theArchitectureOutputandArchitectureGroupedOutputtypes.fallow report, MCP, the Node bindings and CI accept the architecture report.fallow report --fromrenders a savedarchitectureorarchitecture-groupedfile in every format, the same as the direct run. PR comments and reviews offallow architecturehave the title "Fallow architecture report".- The MCP server has a
check_architecturetool, and Code Mode exposes it ascheckArchitecture. The Node bindings havedetectArchitecture, andfallow_apihasrun_architecture. Each returns thearchitectureoutput.detectCircularDependencies,detectBoundaryViolationsand the MCPanalyzetool keep thedead-codeoutput. - Bare
fallowacceptsarchitecturein--onlyand--skip.--only architectureruns the dead-code analysis and reports only the architecture findings, and itsFailed:line namesarchitecture.--skip architectureremoves these findings from the dead-code section. The health score and the--save-baselinefile do not change. - The GitHub Action accepts
command: architecture, and the GitLab template acceptsFALLOW_COMMAND: architecture. For this command,issue-typesandFALLOW_ISSUE_TYPEStakecycles,boundariesandpolicy. Another value stops the job with exit code 2 and an error that names the valid values. - Telemetry records
fallow architectureruns as thearchitectureworkflow.
fallow baselines pruneremoves the baseline entries of fixed findings. The command reads the files thataudit.deadCodeBaseline,audit.healthBaselineandaudit.dupesBaselinename. It runs one whole-project analysis and removes each entry that--baselineno longer matches. It never adds an entry, so a new finding stays visible to the gates. After a prune,--fail-on-stale-baselinepasses, and the baseline hides the same findings as before.--checkwrites nothing and exits 1 when an entry can be pruned.--dead-code-baseline,--health-baselineand--dupes-baselineoverride the config paths.--coveragegives the coverage input for CRAP scores. The command skips a file with an older key form or with fields from a newer version. It prints the command that saves that file again. The stale-baseline warning and the gate message now name the prune command. Thanks @BenMcGit for the request (#3281).fallow hooks install --target git --prune-baselinesprunes the baselines in the pre-commit hook. The hook runsfallow baselines prunebefore the audit and stages the baseline files that the prune wrote. The prune reads the working tree, but the commit uses the index. So the hook skips the prune when the working tree has unstaged or untracked changes. It also skips the prune for a commit with paths (git commit <path>). A failed prune never blocks the commit. The hints for Lefthook and for an existing hook include the same step.fallow trace --dependency <package>reports how the code uses each imported name of a package. The report gives file and call counts, and sites with line and column. It follows one hop through project wrappers such asuseSelector.withTypes(), and it counts each use that it cannot resolve.--sites,--specifier,--limitand--cursorpage the sites.--callers --depth Nadds the files that import the users. The MCP tooltrace_dependencyaccepts the same options.- A JSX import source in a Vitest config now adds a module graph edge. With the automatic JSX runtime,
oxc.jsx.importSource(or the olderesbuild.jsxImportSource) makes each test file with JSX import<source>/jsx-dev-runtime. Fallow now adds this edge when the source is a path and the test file has JSX and no@jsxImportSourcepragma. A package source only marks the package as used. A local JSX runtime no longer shows as unused, and--impact-closureon a runtime file now includes these test files.- The edge applies to the files that
test.includematches. Fallow uses the root config when it has notest.projects, else each inline project config. - A project inherits the root source only with
extends: true. The classic runtime adds no edge. - A relative source resolves from the test file first, then from the config directory. A source that does not resolve adds no edge and no unresolved import.
- The edge applies to the files that
Changed
- The Linux musl binaries are about 4 times faster. The
mallocof musl is slow when many threads allocate, and fallow analyzes files in parallel. The musl binaries now use mimalloc. This applies to thelinux-x64-muslandlinux-arm64-muslnpm packages and the musl release assets. On a 10-core aarch64 Alpine container,check,dupesandhealthtook 77% less time over 15 fixture cases, with identical output. Peak memory is about a third higher. The Node addon keeps the default allocator. Glibc, macOS and Windows builds do not change. - Fallow hides the findings in an unused file by default. Fallow no longer lists an unused export, type, class member or enum member in a file that it reports as unused. Deleting the file removes these findings too. The JSON output has the count in
cascade_hidden, and SARIF and markdown (grouped or not) show the count too. To list the findings, pass--show-cascade, setshowCascadein the config, or use theshow_cascadeparameter of the MCP tools. The health vital signs still count them. When an issue-type filter leaves outunused-files, nothing is hidden. fallow vizshows where to start and says what is wrong.- The Overview panel opens with the health grade and one card per lens. Each card shows the number of files, the number of high findings, and the worst file with its reason. The Overview map colors files that have findings in any lens. Folder labels show how many of their files have findings.
- Each lens list opens with a short summary and a severity split. The list groups rows into high, medium and low. The full file name comes first, then a plain reason. For example, "File path built from input" replaces
path-traversal, and "High complexity, no test coverage" replaces a CRAP score. Security lists each file once with all of its candidates. - Folders with no imports to or from other folders now show when the active lens has findings in them, for example unused files. Health colors only files above the review threshold, so high-risk files stand out. Folders without findings fade.
- The Overview tab of the file panel opens with a list of the lenses that have findings in the file, each with its reason. The Health tab shows maintainability, change risk and importers. Then it lists each function to fix with its branches, lines, test coverage and next step. The treemap rings and names the selected file.
- Folder and import-group labels have two lines (the name, then the size and findings). The labels stay over their cluster and keep clear of the controls and the legend. Import groups that span folders read as
site/src + 5 moreinstead of(mixed). The status line shows the grouping or the path of the file in focus. The map hides disabled controls. - A click on a line between two folders lists the target files in use. Then it lists every import, with forbidden and cyclic imports first.
- Hover tooltips use the same plain reasons as the panel. During a search, the legend explains the match and importer rings.
- Below 700 px, the panel docks under the map as a sheet, so the map stays visible at full width. The legend becomes one line, crowded labels drop, and the file focus view fits both columns.
- The map now follows the design of fallow.tools and fallow.cloud: paper and ink, Barlow type, hairline rules, square panels and flat controls. Day paper is the default. The night theme follows the system or the stored choice. The report embeds the fonts, so it looks the same offline.
- On the graph, folders are octagons, the imports between them run at 0, 45 and 90 degrees, and flagged files have a ring. When you hover over an import line, the map picks the line under the pointer, gives it a pale blue halo and fades the others. Lines that are hidden at the current zoom no longer react. A hover over a file draws its imports the same way, in ink.
- With a file open, the panel shows its place in the active lens ("3 of 61 in Security") with previous and next buttons. The
jandkkeys do the same. - High findings, sparse medium findings, and the copies of an open duplicated block get a halo on the graph.
- The Architecture panel names the folders in an import loop that the map outlines, and says why the loop matters.
- Folders with high findings get a label first. Labels no longer cover the dots of high findings. A label that had to move away from its folder points back to it.
- In a finding lens, each treemap folder header shows how many of its files the lens flags.
- The Overview card for Security shows the files and the candidate count of the tab. The map legend keys each level once. The Architecture tab counts every finding, import cycles included, not only boundary violations. When a card and its tab count the same unit, the card shows its count as a part of the tab count. An example is "528 of 6,135 files".
- On a large project, the first graph layout takes many seconds. A loading screen now shows a progress bar and the current step, in place of an empty stage.
- The light or dark choice applies again the next time a report opens.
- A lens change through the browser history opens or closes the strip of unconnected folders, as a tab click does.
- A security candidate card shows the plain label, the severity, the source-to-sink flow and what to check. The rule id, the trace and other evidence move into a collapsed section.
- A treemap drill flies one camera into the folder and back out. A lens switch sweeps the new colors across the map. With reduced motion, movement stops and changes fade.
Bug fixes
Entry points and frameworks
- Jest configs with a custom name, such as
jest-integration.config.ts, now count as test entries. Fallow readsjest-*.config.*andjest.*.config.*files. Before, a project with such a config next tojest.config.mjsgot a report of every integration test and helper as unused. - A Jest
testRegexthat is only a literal suffix, such as\.integration-spec\.ts$, now maps to**/*.integration-spec.ts. A prefixed regex keeps its own suffix and no longer falls back to*.test. The patterns of every Jest config now add up. Before, one config with its own pattern dropped the default test files of the others. - Fallow now reads a
--configfile that a script passes tojest, as it already did forvitest. A script config file is now also a used file when the path starts with./. - GraphQL Codegen configs with other names or a
.cjsextension now count. Fallow readscodegen*.{ts,js,cjs,mjs}and uses thedocumentsglobs of each config as entry points. The codegen run reads those files, so no import points at them. - Commands of Nx
run-commandstargets now count as scripts. Fallow reads thecommandandcommandsof everynx:run-commandstarget inproject.json. It replaces the{projectRoot}and{workspaceRoot}tokens and reads theconfigurationsthat set their own command. A call to another target of the same project, such asnx ts-node -- src/run.ts, uses the command of that target. Fallow skips a target that runs from another directory, because its relative paths would point at the wrong files. workspaces.patternsin the config now adds workspace packages. Before, fallow parsed the key but did not use it. So a package outside the globs ofpackage.jsonorpnpm-workspace.yamlgot no plugin, no entry points and no dependency check of its own.- Fallow now adds these globs to the manifest globs for analysis and for
fallow list --workspaces. --workspace,--changed-workspacesand--group-by packageuse them in every command that has a loaded config.- The base checkout of
auditandsecuritylinks the generated context of these packages, and thescope-workspacesnext step lists them. - The error for an unknown
--workspacenow namesworkspaces.patterns. fallow initandcoverage setupstill read only the manifest globs.
- Fallow now adds these globs to the manifest globs for analysis and for
- TypeORM migrations that a
DataSourceconfig loads by glob now count. Fallow reads themigrationsglobs ofdata-source.ts,*.datasource.tsandormconfigfiles. A conditional template such as${isJest ? 'src/' : 'dist/'}...gives thesrc/branch. Entities and subscribers stay out, because their files do not run as a whole. - A package entry that points to build output now maps to its source. For example,
dist/define/index.mjsfindssrc/define/index.ts, and a.d.tstype target finds its source file. Fallow readsvite.config.*.tsandrollup.config.*files. It also tries thesrc/file when a tsconfig maps the output to no existing file. - Class members and enum values that a framework reads by reflection now count.
- An entity class that is passed to
getRepository,createQueryBuilderor a TypeORMfindcall is a whole-object use. This needs a TypeORM or MikroORM import. - An enum that is passed to
registerEnumTypefrom@nestjs/graphqlortype-graphqlis a whole-object use. - The
runmethod of aCommandRunnersubclass counts whennest-commanderis a dependency.
- An entity class that is passed to
- A package that a config names only in a string is no longer unused. Fallow credits the SWC plugins of
@vitejs/plugin-react-swcand ofexperimental.swcPluginsin a Next.js config. It also credits the Babel plugins and presets in thebabelOptionsof@wyw-in-js/vite, and thecollectionofnest-cli.json. - A JavaScript array in a CI
run:block no longer becomes an entry pattern. For example, anode <<'NODE'heredoc that holds['packages/apps/public']gave the token[packages/apps/public]. That token failed glob compilation and loggedinvalid entry patternon every run. The scanner now rejects a bracket group that contains/. Route segments such as[id]and[...slug]still pass.
Exports, classes and the module graph
-
A class in a CSS Modules global scope is no longer an unused export. CSS Modules do not put a global class in the class map of the module, so you cannot import it. Fallow now skips these classes:
- the classes in
:global(.a .b), - the classes after a bare
:global, up to:localor the next selector, - the classes of rules nested in a global rule.
A class in
:local(.a)stays an export. A rule with a bare:globalor:localno longer hides its local classes. This applies to.module.css,.module.scss,.module.sassand.module.lessfiles. Thanks @peterssonjonas for the report (#3311). - the classes in
-
A
/** @jsxImportSource <source> */pragma now adds a module graph edge. With the automatic JSX runtime, the pragma makes the file import<source>/jsx-runtime. Before, a local JSX runtime could show as unused, and--impact-closureon a runtime file did not include the.tsxfiles that use it. The edge now resolves like a normal import, for relative and package values. As with the compiler, the edge needs JSX in the file, and the pragma must be in a comment before the first statement. -
A member that a leaf class calls now counts for the base of a long
extendschain. WithChild extends Mid extends Base, fallow reportedBase.startas an unused class member when the code callednew Child().start(). Fallow moved an access one level for each pass over an unordered map. It now repeats the pass until nothing changes. An access through one child still does not count for a sibling class. -
A warm run no longer reuses the analysis of the previous content of a file. The caches trusted a file when its modification time, change time and size matched. This applied to the parse cache, the duplication token cache and the kept modules of a long-lived session.
- A same-length save in the same filesystem timestamp tick keeps all three values.
- A save during the parse of the file gave the cached analysis the metadata of the new content.
In both cases, the next run reported findings for the old content. Now a cache trusts the metadata only when both timestamps are at least three seconds older than the read of the cached content. For a more recent file, the next run reads the file and compares the content hash.
Baselines and gates
-
fallow dupes --baselinenow matches each clone group by its own content. A clone can stop inside a block, so its code does not parse on its own. Before, all such groups got the same fingerprint with a-rNsuffix in report order. A new clone group could then take the baseline entry of an old group, and an unchanged group could show as new. Now these groups get a fingerprint from their tokens, without whitespace and comments. A baseline that still has the old shared keys prints a note, and its JSONbaseline_staleness.formatis"legacy". Thanks @aleksik for the report (#3290). -
fallow audit --gate new-onlycompares complexity values with the base. Before, the gate matched complexity findings by path, function name and exceeded category, and did not compare the values. So a function that got worse above the limit passed as inherited, and a function that improved frombothtocyclomaticfailed as introduced.- Now a finding matches its base finding by path and function name.
- A finding is introduced when no base finding matches. It is also introduced when a metric that it exceeds has a higher value than in the base finding.
- Unchanged and decreased values stay inherited. Line shifts and renamed files keep the match.
- Same-named findings in one file, for example class methods, now count separately in
complexity_introducedandcomplexity_inherited.
Thanks @rodrigouroz for the report and the reproduction (#3277).
-
The agent gate now audits the tree of the session, also in a nested git worktree. The hook process can start in a directory that is not the session directory. For example, a session in a worktree below the main checkout can get a hook process in the main checkout. The gate then audited the main checkout. Findings there blocked a clean commit in the worktree, and findings in the worktree did not block.
- The generated
fallow-gate.shnow reads the session directory from thecwdfield of the hook input. - From there, it walks up to the nearest directory that holds the gate script and stops at the first
.gitentry. The handler uses the same rule. - Sometimes the gate finds no script, for example with the user-scope gate in
$HOME. Then it keeps the directory of the hook process if that directory is in the git work tree of the session, as before. Else it audits the git top level of the session directory, or the session directory outside git. It never audits$HOME. - When the hook input has no usable
cwd, the gate audits the directory of the hook process, as before.
- The generated
Runtime coverage
fallow coverage upload-source-mapsno longer loses maps that share a file name. With the default--strip-path=true, atscoutput tree with more than oneindex.js.mapsent each map under the same name. The cloud then kept only the last map. Now the command sends maps that share a basename under their path in the build directory, and prints which maps it renamed. Thanks @voslartomas for the report (#3298).fallow coverage upload-source-mapsnow warns when thesourcesof a map do not point at files in the repository. For example, asourceRootof"/"intsconfig.jsonremoves thesrc/segment from each source path, so runtime coverage cannot resolve to source. The warning names the map, the source and thesourceRoot. It also shows with--dry-run.
Output and CLI
- The report on stdout is now complete when the parent process makes the stdout pipe non-blocking. Bun sets
O_NONBLOCKon a pipe that it shares with a child process. A large report write then failed withEAGAIN. Fallow stopped the output at the size of the pipe buffer and exited with the normal exit code. Now fallow waits until stdout accepts more bytes and writes the remaining part of the report. Thelist,schema,config,fixand textvizoutput use the same writer. A closed reader, for examplefallow | head, still stops the output without an error. Any other stdout write error now prints a message on stderr and gives exit code 2. Thanks @codingthat for the report (#3276). - The npm launcher no longer cuts the output of the binary short under Bun. When Bun ran the
fallowlauncher with stdout on a pipe, the JSON output stopped after approximately 64 to 150 KB, with exit code 0. The VS Code extension then failed to parse the JSON. The first use ofprocess.stdoutorprocess.stderrin the launcher set the shared pipe to non-blocking mode under Bun, and the binary inherited that mode. A large write from the binary then failed. Now the launcher does not use these streams before the binary exits. It writes the verification warnings directly to the stderr descriptor. Thanks @codingthat for the report and the reproduction (#3276). fallow dead-code --trace-dependencynow lists each importing file once. Before, a file showed one time for each imported name, soimport { a, b } from "pkg"listed the file twice and doubledimport_count.import_countis now the number of importing files.type_only_imported_bynow lists only files whose every import of the package is type-only. Before, a file with a type-only import and a value import showed as "(type-only)".fallow dead-code --summarynow lists the rule-pack "Policy violations" row. The total already counted these findings.- The human status line of
fallow dead-codenow counts policy violations, boundary coverage violations and boundary call violations. Boundary violations show as "boundary violation" instead of "violation". Before, a run with only policy violations printed a status line with no count. - The
Failed:line of barefallownow says "1 issue" and "1 clone group" in the singular. - The published JSON schema now lists the
similar-code-statusandsimilar-code-cache-clearkinds in the rootFallowOutputunion. Before, a validator that used the schema rejected the valid output offallow similar-code status --format jsonandfallow similar-code cache clear --format json. - The copy-paste commands that
fallow agent installandfallow baselines pruneprint now quote each path for the shell of the platform. A path with;,|,&or similar characters stays one argument and cannot add a command. On Windows, a quoted path keeps single backslashes. fallow vizno longer stops drawing the graph on a narrow stage. On a stage narrower than 200 px, the camera fit gave a negative zoom. The canvas then threw an error for each negative ring radius. The fit now keeps a minimum usable width.
Upgrade notes
- The parse cache version and the duplication cache version change, so the first run after the upgrade parses all files again. The extraction cache and the graph cache rebuild once. The audit base snapshot cache version also changes, so the first audit after the upgrade analyzes the base again.
fallow dead-codekeeps the flags--circular-deps,--re-export-cycles,--package-cycles,--boundary-violationsand--policy-violationsas deprecated aliases offallow architecture --cycles,--boundariesand--policy. The flags keep working in v3.fallow dead-codekeeps reporting these findings until the next major version. When it reports one, its human output points tofallow architecture.- To rewrite a duplication baseline with the old shared keys, run
fallow dupes --save-baselineonce. AnignoredClonesentry with the old shared handle no longer hides a group. Review the group and copy its new handle. - To update an installed agent gate script, run
fallow hooks install --target agentagain. Run it in the checkout where the hook process starts (for a nested worktree, the main checkout). Also run it in each worktree that tracks its own copy of the script. - A baseline saved before the change that hides findings in unused files still matches. A baseline that you save now does not contain the hidden findings, so save a regression baseline again.
- A
cargo install fallow-clion a musl host now needs a C compiler.
Full Changelog: v3.32.0...v3.33.0