Skip to content

v3.33.0: fallow architecture, baseline pruning, faster musl binaries

Choose a tag to compare

@BartWaardenburg BartWaardenburg released this 09 Oct 15:18
· 379 commits to main since this release
Immutable release. Only release title and notes can be modified.
v3.33.0
278dbc6

Features

  • fallow architecture reports import cycles, boundary violations and rule-pack policy violations in one command. Use --cycles, --boundaries or --policy to select one kind. The command takes the same scope and output flags as fallow dead-code. Run fallow guard <files> before an edit and fallow architecture after it. Bare fallow shows these findings in an "Architecture" category after the other dead-code categories. It also shows the category with --quiet, --group-by and --summary. With --group-by, fallow architecture shows one "Architecture" heading per group.
  • fallow architecture --format json writes kind: "architecture" with its own schema_version (1). With --group-by, the kind is architecture-grouped. The arrays, finding ids, actions, exit codes, gate outcomes and severities are the same as on fallow dead-code. next_steps names fallow architecture. With --explain, _meta.docs points to the architecture page, and _meta.rules lists only the architecture rules. --save-baseline writes 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 the ArchitectureOutput and ArchitectureGroupedOutput types.
  • fallow report, MCP, the Node bindings and CI accept the architecture report.
    • fallow report --from renders a saved architecture or architecture-grouped file in every format, the same as the direct run. PR comments and reviews of fallow architecture have the title "Fallow architecture report".
    • The MCP server has a check_architecture tool, and Code Mode exposes it as checkArchitecture. The Node bindings have detectArchitecture, and fallow_api has run_architecture. Each returns the architecture output. detectCircularDependencies, detectBoundaryViolations and the MCP analyze tool keep the dead-code output.
    • Bare fallow accepts architecture in --only and --skip. --only architecture runs the dead-code analysis and reports only the architecture findings, and its Failed: line names architecture. --skip architecture removes these findings from the dead-code section. The health score and the --save-baseline file do not change.
    • The GitHub Action accepts command: architecture, and the GitLab template accepts FALLOW_COMMAND: architecture. For this command, issue-types and FALLOW_ISSUE_TYPES take cycles, boundaries and policy. Another value stops the job with exit code 2 and an error that names the valid values.
    • Telemetry records fallow architecture runs as the architecture workflow.
  • fallow baselines prune removes the baseline entries of fixed findings. The command reads the files that audit.deadCodeBaseline, audit.healthBaseline and audit.dupesBaseline name. It runs one whole-project analysis and removes each entry that --baseline no longer matches. It never adds an entry, so a new finding stays visible to the gates. After a prune, --fail-on-stale-baseline passes, and the baseline hides the same findings as before. --check writes nothing and exits 1 when an entry can be pruned. --dead-code-baseline, --health-baseline and --dupes-baseline override the config paths. --coverage gives 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-baselines prunes the baselines in the pre-commit hook. The hook runs fallow baselines prune before 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 as useSelector.withTypes(), and it counts each use that it cannot resolve. --sites, --specifier, --limit and --cursor page the sites. --callers --depth N adds the files that import the users. The MCP tool trace_dependency accepts 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 older esbuild.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 @jsxImportSource pragma. A package source only marks the package as used. A local JSX runtime no longer shows as unused, and --impact-closure on a runtime file now includes these test files.
    • The edge applies to the files that test.include matches. Fallow uses the root config when it has no test.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.

Changed

  • The Linux musl binaries are about 4 times faster. The malloc of musl is slow when many threads allocate, and fallow analyzes files in parallel. The musl binaries now use mimalloc. This applies to the linux-x64-musl and linux-arm64-musl npm packages and the musl release assets. On a 10-core aarch64 Alpine container, check, dupes and health took 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, set showCascade in the config, or use the show_cascade parameter of the MCP tools. The health vital signs still count them. When an issue-type filter leaves out unused-files, nothing is hidden.
  • fallow viz shows 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 more instead 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 j and k keys 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 reads jest-*.config.* and jest.*.config.* files. Before, a project with such a config next to jest.config.mjs got a report of every integration test and helper as unused.
  • A Jest testRegex that 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 --config file that a script passes to jest, as it already did for vitest. A script config file is now also a used file when the path starts with ./.
  • GraphQL Codegen configs with other names or a .cjs extension now count. Fallow reads codegen*.{ts,js,cjs,mjs} and uses the documents globs of each config as entry points. The codegen run reads those files, so no import points at them.
  • Commands of Nx run-commands targets now count as scripts. Fallow reads the command and commands of every nx:run-commands target in project.json. It replaces the {projectRoot} and {workspaceRoot} tokens and reads the configurations that set their own command. A call to another target of the same project, such as nx 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.patterns in the config now adds workspace packages. Before, fallow parsed the key but did not use it. So a package outside the globs of package.json or pnpm-workspace.yaml got 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-workspaces and --group-by package use them in every command that has a loaded config.
    • The base checkout of audit and security links the generated context of these packages, and the scope-workspaces next step lists them.
    • The error for an unknown --workspace now names workspaces.patterns.
    • fallow init and coverage setup still read only the manifest globs.
  • TypeORM migrations that a DataSource config loads by glob now count. Fallow reads the migrations globs of data-source.ts, *.datasource.ts and ormconfig files. A conditional template such as ${isJest ? 'src/' : 'dist/'}... gives the src/ 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.mjs finds src/define/index.ts, and a .d.ts type target finds its source file. Fallow reads vite.config.*.ts and rollup.config.* files. It also tries the src/ 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, createQueryBuilder or a TypeORM find call is a whole-object use. This needs a TypeORM or MikroORM import.
    • An enum that is passed to registerEnumType from @nestjs/graphql or type-graphql is a whole-object use.
    • The run method of a CommandRunner subclass counts when nest-commander is a dependency.
  • A package that a config names only in a string is no longer unused. Fallow credits the SWC plugins of @vitejs/plugin-react-swc and of experimental.swcPlugins in a Next.js config. It also credits the Babel plugins and presets in the babelOptions of @wyw-in-js/vite, and the collection of nest-cli.json.
  • A JavaScript array in a CI run: block no longer becomes an entry pattern. For example, a node <<'NODE' heredoc that holds ['packages/apps/public'] gave the token [packages/apps/public]. That token failed glob compilation and logged invalid entry pattern on 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 :local or 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 :global or :local no longer hides its local classes. This applies to .module.css, .module.scss, .module.sass and .module.less files. Thanks @peterssonjonas for the report (#3311).

  • 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-closure on a runtime file did not include the .tsx files 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 extends chain. With Child extends Mid extends Base, fallow reported Base.start as an unused class member when the code called new 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 --baseline now 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 -rN suffix 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 JSON baseline_staleness.format is "legacy". Thanks @aleksik for the report (#3290).

  • fallow audit --gate new-only compares 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 from both to cyclomatic failed 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_introduced and complexity_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.sh now reads the session directory from the cwd field of the hook input.
    • From there, it walks up to the nearest directory that holds the gate script and stops at the first .git entry. 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.

Runtime coverage

  • fallow coverage upload-source-maps no longer loses maps that share a file name. With the default --strip-path=true, a tsc output tree with more than one index.js.map sent 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-maps now warns when the sources of a map do not point at files in the repository. For example, a sourceRoot of "/" in tsconfig.json removes the src/ segment from each source path, so runtime coverage cannot resolve to source. The warning names the map, the source and the sourceRoot. 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_NONBLOCK on a pipe that it shares with a child process. A large report write then failed with EAGAIN. 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. The list, schema, config, fix and text viz output use the same writer. A closed reader, for example fallow | 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 fallow launcher 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 of process.stdout or process.stderr in 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-dependency now lists each importing file once. Before, a file showed one time for each imported name, so import { a, b } from "pkg" listed the file twice and doubled import_count. import_count is now the number of importing files. type_only_imported_by now 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 --summary now lists the rule-pack "Policy violations" row. The total already counted these findings.
  • The human status line of fallow dead-code now 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 bare fallow now says "1 issue" and "1 clone group" in the singular.
  • The published JSON schema now lists the similar-code-status and similar-code-cache-clear kinds in the root FallowOutput union. Before, a validator that used the schema rejected the valid output of fallow similar-code status --format json and fallow similar-code cache clear --format json.
  • The copy-paste commands that fallow agent install and fallow baselines prune print 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 viz no 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-code keeps the flags --circular-deps, --re-export-cycles, --package-cycles, --boundary-violations and --policy-violations as deprecated aliases of fallow architecture --cycles, --boundaries and --policy. The flags keep working in v3. fallow dead-code keeps reporting these findings until the next major version. When it reports one, its human output points to fallow architecture.
  • To rewrite a duplication baseline with the old shared keys, run fallow dupes --save-baseline once. An ignoredClones entry 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 agent again. 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-cli on a musl host now needs a C compiler.

Full Changelog: v3.32.0...v3.33.0