Skip to content

v3.35.0: worsened complexity in audit, Vite+ and Egg.js plugins, mutation progress

Choose a tag to compare

@BartWaardenburg BartWaardenburg released this 22 Oct 12:17
· 138 commits to main since this release
Immutable release. Only release title and notes can be modified.
v3.35.0
1f5a08d

Features

  • fallow audit reports inherited complexity findings that got worse. A complexity finding from the base snapshot is now worsened when its cyclomatic, cognitive, CRAP or line count value is higher than at the base. CRAP counts only when both sides have a score. The line count must grow by at least 5 lines, so a short comment or a formatter rewrap keeps a finding inherited. A function that grows from 80 to 300 lines with the same complexity values is now worsened. The JSON output adds attribution to each complexity finding, worsened_metrics to each finding with a grown value, and attribution.worsened and attribution.complexity_worsened. The human, compact and markdown output add a "Worse than base" section. SARIF and CodeClimate show the values, and the GitHub summary marks each finding as worsened. The --worsened flag and the audit.worsened config key select the effect: warn (default, exit code 0), error (counts like an introduced finding) or off (only growth of an exceeded metric is worsened). The GitHub Action has a worsened input, the GitLab template has FALLOW_AUDIT_WORSENED, and the MCP audit tool has a worsened parameter.

  • The VS Code extension has a fallow.audit.worsened setting. The values are warn, error and off, the same as fallow audit --worsened. The default auto uses audit.worsened from the project config. The extension sends the flag only to a CLI that supports it. With an older CLI, the audit runs without the flag and the output channel shows a note.

  • A human fallow mutation run shows phase progress on stderr. The lines name the runner setup, the baseline run and the mutant runs with done and total counts. They also name the retries, the final worktree check and the report. A long phase prints a line at a fixed interval, also in non-TTY output, so a slow run does not look stalled. --quiet and --format json print no progress lines, and stdout does not change. The programmatic API has run_mutation_with_progress with MutationProgress events. (#3599)

  • fallow mutation --runner-config <path> selects the Vitest config of a run. A project that keeps its integration, UI or browser tests in a separate Vitest config can now run mutants against those tests. The mutation.runnerConfig config key sets the same file, and the flag wins. The file must exist inside the project root, and only the Vitest runner accepts it. A static test file that the selected config does not include stays visible as a test-files-not-discovered error. --config still selects the Fallow config. (#3598)

  • Mutation JSON shows the edit of each mutant and the history of each retry. Each mutant in the mutation report and the mutation-plan document has a change object, for example {"from": "??", "to": "&&"}. The values come from a fixed vocabulary of operator tokens, literals and placeholders, so the report still holds no source text (#3658). A retried mutant has a new attempts array with the status, the duration and the killing test, failure kind or runtime-error reason of each run. You can now see if a flaky result was timeout -> survived or survived -> killed (#3656).

  • fallow mutation mutates fluent chains in module-level const initializers. The new removed-chain-link operator removes one call link of a chain. For example, z.number().int().positive() gives a mutant without .int() and a mutant without .positive(). A validation schema with no literal or operator now has mutants. The operator is on by default, and --operators and mutation.operators accept its name. (#3662)

  • Fallow reads Vite+ projects. A vite-plus dependency, or a root vite.config.* that imports vite-plus, enables the new vite-plus plugin. The pack.entry values are entry points, and the lint.jsPlugins, lint.overrides[].jsPlugins and lint.extends packages count as used. The vite-plus dependency also enables the Vitest plugin. The vp, vpr and vpx binaries credit vite-plus. Fallow reads vp test -c <file>, vp exec <binary> and vp run <task>. It also reads the commands of the staged and run.tasks blocks and the .vite-hooks/ scripts. The file targets of vp lint and vp fmt are not entry points. A vi.mock call through vite-plus/test counts as a Vitest mock.

  • Fallow detects Egg.js and tegg projects. The new egg plugin activates on egg, @eggjs/core or @eggjs/tegg, or on an egg, eggPlugin or eggModule key in package.json. Entry points are the files that the Egg loader loads by convention: app.js, agent.js, the router, controllers, services, middleware, extensions, schedules and config files, also in nested example and test fixture apps. Every file of a tegg module is an entry point. The plugin credits the packages that config/plugin.* names, the egg.framework package and the default egg framework to the package.json that owns the app.

  • Fallow reads the entry files of a JavaScript GitHub Action. When action.yml or action.yaml sets runs.using to a node runtime, the runs.pre, runs.main and runs.post files are entry points. A path in dist/ maps to its source file, with the same rules as a package.json main field. A composite or Docker action adds no entry points.

  • A dead-code baseline records if node_modules existed. A --save-baseline run writes analysis_identity.node_modules as present or missing. A later run in the other install state adds a baseline-install-state-mismatch entry to workspace_diagnostics and prints a warning, also under --quiet. Findings that need installed packages can differ between the two states, so such a run can report baseline findings as new.

  • External plugins can opt a config pattern out of the default export credit. List the pattern in configDefaultExportOptOut when the tool reads named exports, or when the pattern also matches ordinary source files.

  • Fallow reports a missing workspace directory and analyzes nested pnpm projects. A positive workspace pattern whose directory does not exist now gets the workspace-pattern-dir-missing diagnostic, with degrades_analysis: true. While such a pattern exists, the run does not report unused-catalog-entries for any catalog, because the missing packages can use catalog entries. This also applies to a pattern for a directory that does not exist yet, such as packages/* in a template. A directory that exists but holds no package does not turn off the check. Create the directory before the run or remove the pattern to get the check back. A directory with its own package.json and pnpm-workspace.yaml that no root pattern declares is an independent pnpm project. Fallow analyzes it as a workspace and reports the information-only nested-project-root diagnostic. (#3710)

  • Each release publishes a CycloneDX SBOM for every target. The files are named fallow-<target>.cdx.json. .vex/openvex.json states why fallow is not affected by each advisory that the repository ignores. SECURITY.md lists the supported versions and the code scanning policy.

Changed

  • fallow audit --gate new-only reports growth of an already exceeded complexity metric as worsened. Before, such a finding was introduced. A function that exceeded a threshold on the base and grows a little in the PR no longer fails the audit by default. With the default audit.worsened: warn, the verdict is warn and the exit code is 0. The finding stays introduced when the growth moves it to a higher severity tier. It also stays introduced when it now exceeds a metric that it did not exceed on the base, and that metric grew. The exceeded flags and the tier come from the base run. A threshold override in the same PR does not make an old finding introduced.

  • fallow audit --gate new-only no longer fails on a CRAP score from estimated coverage alone. Without a coverage map, fallow estimates CRAP from static test reachability, and this estimate does not measure the change. A finding that exceeds only the CRAP threshold with estimated coverage now has effective_severity: warn and SARIF level warning. A finding that also exceeds the cyclomatic or cognitive threshold still gates. CRAP from measured coverage still gates, and --gate all does not change.

  • fallow audit no longer fails on a complexity finding in a test file. A long it(...) or describe(...) callback often exceeds the cognitive threshold. Such a finding now has effective_severity: warn and the new field gate_exemption: "test-file", with a verdict of at least warn and exit code 0. This applies to --gate new-only and --gate all. The human, markdown and compact views list these findings under "Test files, not gated". fallow health does not change. (#3753)

  • The GitHub Action requests the branded token only when it posts. The token step now runs only on a pull_request event with comment: true or review-comments: true. The branded-token input now defaults to auto. auto posts as the Fallow app when the workflow grants id-token: write. Without that permission, it falls back to github-token without a log line. An explicit branded-token: true keeps one short fallback line, and false behaves as before. (#3755)

  • --type-aware class-member refinement is faster on large projects. The symbol-use scan now checks the name first and keeps one alias answer for each symbol. On a large TypeScript project, the scan takes 4 to 12 s instead of 35 to 70 s, with the same decisions. The scan now also sends only the names that a query can reach to the checker for TypeScript files. JavaScript files keep the full scan, because require bindings also create aliases. (#3765)

  • fallow mutation with Vitest isolate: false no longer repeats the setup of unrelated test files. Each static mutant runs in a new runner process, and that process now dry-runs only the test files that cover the mutant. A covering test that fails its dry run is still a baseline failure. (#3650)

  • The fallow skill shows the review loop that fallow checks. One short example runs fallow audit, reads the walkthrough guide, writes a judgment, and validates it with --walkthrough-file. The example saves the guide and the judgment in the temporary folder, because a new file in the project tree makes the judgment stale.

Bug fixes

  • fallow mutation makes fewer equivalent mutants. No test can kill these mutants, so they no longer appear:

    • an empty-string or swapped-boolean case label with the same body as default, only when all labels are literals and both bodies exit unconditionally (#3639);
    • i != N for the test of a counted for loop with an integer let counter and no other write (#3649);
    • a zero comparison on the length of a known array, such as items.length > 0 to items.length != 0 (#3651);
    • the constant true for an optional lookup before .filter(Boolean), such as ["base", align ? alignment[align] : ""].filter(Boolean) (#3667).
  • fallow mutation runs the tests that can kill a mutant. A mutant in module-level code now runs the tests of static importers when another test also loads the module through import() (#3652). The Vitest setup file serves the mutant to a createRequire(import.meta.url) load on Node 22.15 and later (#3653). With node:test, a Node child process of a test (spawn, exec, execFile or fork) loads the active mutant (#3680). The node-test and bun-test runners now run a mapped test file in a dot directory such as .github/ (#3678).

  • Fewer false survivors in fallow mutation, and Vitest inline projects stop at the first failure. The retry of a survivor now runs each test of the files of its covering tests. A test that reads a cached value can now kill the mutant. Such a kill is killed when the same tests pass in a control run with no active mutant (#3670). Each Vitest inline project in test.projects now stops at the first failed test, and a control run still runs the full selection (#3701).

  • fallow mutation marks undiscovered test files, keeps time for retries, and shows the assertion of a failed control run. A run in which the runner does not list a static test file is now partial with the reason test-files-not-discovered (#3596). With --time-budget, the first pass keeps time for the retries of its survivors and timeouts, and the cache stores the confirmed results. A repeat run continues with the next mutants (#3665). When a test fails in a control run, the FALLOW_MUTATION_BASELINE_FAILED error and the human footer show the assertion. baseline_failures also shows the project stack frames (#3690).

  • Writes through an ORM or a query builder are now side-effect sinks in fallow mutation. A safe write such as db.insert(records).values({ value }) was not a side-effect sink, so a run could write to a real database. Database write method names, SQL text that starts with a write verb, and prepared statement runs are now sinks. These sinks apply only in a module that loads a database client package, or that loads a project module that loads one. Examples are drizzle-orm, @prisma/client, kysely, knex, mongodb and pg. A prepared SELECT and a route registration such as app.delete("/items/:id", handler) are not sinks. --allow-side-effects keeps these mutants. (#3641)

  • fallow audit --gate new-only no longer reads a missing base CRAP score as growth. A matched finding with a head CRAP score and no base CRAP score is now worsened or inherited, not introduced. A CRAP score from estimated coverage also no longer makes a matched finding introduced.

  • The default export of a tool config file is no longer an unused export under includeEntryExports. The rule applies to each config pattern of an active plugin, built-in or external, for example postcss.config.*, tailwind.config.js, jest.config.js and playwright.config.ts. The credit now also covers Capacitor, Sanity, Nitro, SVGO, SVGR, Kysely, OpenAPI TS, Remix, i18next, VitePress, CSpell, @sanity/pkg-utils, Electron Forge and electron-builder configs. A root codegen* and a root serverless.* file keep their default export, and cucumber-js and @vercel/config named exports count as used. Gatsby gatsby-node, gatsby-browser and gatsby-ssr and Vercel vercel.* patterns get no default export credit. Named exports of a config file stay reportable. Output without includeEntryExports does not change. Thanks to @osazemeu for the report (#3691) and the PostCSS patch (#3694).

  • Serverless handler exports and .tsx and .jsx Sanity configs are entry points. fallow dead-code --include-entry-exports no longer reports the export that a Serverless handler names, such as main for src/users.main. A nested serverless.ts or serverless.js module, such as src/lib/serverless.ts, is an ordinary source file again. Fallow now treats sanity.config.tsx and sanity.config.jsx as Sanity entry points, so the modules that they import are no longer unused. For a .tsx or .jsx Sanity config, the output changes in all modes.

  • Fallow finds the test files of more Vitest setups. A Vitest config below the project root, for example a fixture config, now applies its test.include globs only below its own directory. Fallow reads test.root, test.dir and the Vite root, and resolves setupFiles, globalSetup and reporter paths from the Vitest root. A project file such as packages/a/vitest.unit.config.ts in test.projects is the root of its project. An inline project gets the values that it merges from its base config. The default test patterns match .mts, .cts, .mjs and .cjs files.

  • Vitest bench files and reporter paths are used files. A config that sets test.include no longer makes bench files unused. Fallow reads test.benchmark.include, and the default bench pattern now matches .benchmark. files and the .mts and .cts extensions. A reporters entry such as './scripts/ci-reporter.ts' marks that file as used, also inside a spread array. The config reader of the other plugins now also reads spread arrays and both branches of a condition.

  • The napi-rs plugin works in a source repository. The plugin activates on the napi field and its targets, without optionalDependencies. In a generated loader, the platform packages and @napi-rs/wasm-runtime are no longer unlisted, and the .wasi.cjs, .wasi-browser.js and .node files are no longer unresolved imports. The generated loader and browser.js are support entry points, so a hand-written wrapper is no longer a duplicate export. The napi and napi-raw binaries credit @napi-rs/cli. You can remove the ignorePatterns and ignoreDependencies entries for these files.

  • Fallow credits more dependencies. These cases no longer give unused, unlisted or unresolved findings:

    • a resolve specifier from a conditional const with ?:, || or ?? and string literal branches;
    • a <workspace-package>/package.json import without node_modules;
    • require.resolve on a function-scoped createRequire(import.meta.url) or createRequire(__filename);
    • require('..') and require('.') when main points at missing build output;
    • a script that calls a binary from the bin field of a workspace package, without node_modules;
    • the taplo, oxnode, ut and utx binaries;
    • a peer of a package that a script runs or that a plugin credits, for example @emnapi/runtime next to @napi-rs/cli;
    • the vite.optimizeDeps, vite.ssr.external and vite.ssr.noExternal packages of a VitePress config, and a nested Vite optimizeDeps entry such as 'mermaid > @braintree/sanitize-url'.
  • GitHub Actions steps resolve file arguments in their working-directory. Fallow reads the working-directory of the step and the defaults.run.working-directory of the workflow and the job. Paths that start with $GITHUB_WORKSPACE/, ${{ github.workspace }}/ or GitLab $CI_PROJECT_DIR/ resolve from the project root. Fallow now also reads the run steps of composite actions in .github/actions/ and the root action.yml.

  • css-broken-reference no longer suggests a "did you mean" fix for a defined class. Fallow skips tokens with the shape of a Tailwind utility, such as w-full, when a stylesheet loads Tailwind or the root package.json declares it. As a result, an authored class that starts with a utility root, such as text- or list-, gets no typo suggestion. A class that an Astro, Vue or Svelte <style> block or a CSS module defines only through :global now counts as defined. A <style> block in an .html file now adds its classes. Thanks to @zaphir for the report. (#3588)

  • A < in template text no longer hides a later component invocation. The caller scan for Vue, Svelte, Angular and Ember templates read each < as a tag start. In <p>{{ a < b }}<Button size="lg" /></p>, the Button invocation was lost. The scan now skips text expressions as a unit and reads a < as a tag start only before a letter, /, ! or ?.

  • Fallow now reads a JSON or JSONC config file that ends with a // line comment and no final newline. Before, the parse failed. For example, a tsconfig.json lost its extends package, and an .oxlintrc.jsonc lost its jsPlugins packages, which fallow then reported as unused.

  • Glob expansion on disk works when the project path contains [, * or ?. Under a root such as ~/work/app [1], plugin fileExists detection, fallow plugin-check entry seeds and workspace globs matched nothing. A plugin did not activate, and fallow reported the files of a workspace package as unused.

  • A module that imports X under an alias and exports its own X is no longer a duplicate export. Such a module is an adapter, for example import { configureAuth as core } from "../ci/auth" followed by export function configureAuth(). Fallow still reports two modules that import X from each other under aliases.

  • A nested package.json without dependency fields no longer causes unlisted dependencies. A metadata-only manifest, such as a tegg module with { "name": "x", "eggModule": {} }, now inherits the dependency declarations of its ancestor manifests. A manifest with an explicit empty field, such as "dependencies": {}, stays the owner of its files.

  • --trace no longer calls an export unused when a framework plugin marks it as used. --trace and --trace-file list these credits in a new plugin_credits field with the plugin and its file pattern. The human output shows the same credit.

  • ignoreFindings covers the parse errors of the files that it hides. A syntax-error test fixture that ignoreFindings covers no longer reports source-parse-degraded, prints a parse warning, or fails the parse-error gate. Its partial parse no longer adds the incomplete-import-graph caveat to other findings.

  • Warnings on stderr have color codes only on a terminal. NO_COLOR turns them off, and CLICOLOR_FORCE turns them on. --quiet, --ci and FALLOW_QUIET hide the analysis warnings that repeat a workspace_diagnostics entry. Set RUST_LOG=warn to show them again. Config warnings still print.

  • The setup hints name the current commands. A bare run names fallow init and fallow agent install --dry-run, and the JSON setup next step uses fallow agent install --dry-run. fallow init tells you to run fallow next. A dry run of fallow agent install that plans writes puts agent-install-apply first in next_actions[], with the flags that change the plan. The command repeats a detected harness as --harness. The review focus reads fan-in (1 importer) for one importer.

  • An older fallow no longer drops data from an Impact store that a newer fallow wrote. Every write path now leaves such a store unchanged. fallow impact enable, fallow impact disable and fallow init --decline exit with code 2 and name the store schema and the fallow version of the newest record. Reads stay available. (#3597)

  • Nested pnpm projects and catalogs give correct dependency findings. A nested independent pnpm project owns its files and dependencies, and its catalog: references are not checked against the root catalog. A package.json with no name and no dependencies, such as one with only {"type":"module"}, is no longer an undeclared workspace. A pnpm override with no version selector now takes precedence over a catalog: reference. Under audit --gate new-only, catalog findings that the base snapshot could not check are inherited, not introduced. A new unused catalog entry in such a run is also inherited. (#3710)

  • The fallow-setup skill detects all Oxlint and Oxfmt config file names. The table now also lists .oxlintrc.jsonc, oxlint.config.{ts,mts}, .oxfmtrc.jsonc and oxfmt.config.*.

  • Fallow links patched rustls-webpki and webpki-roots, and the Docker image pins its base images. Fallow and the similar-code sidecar link rustls-webpki 0.103.16 (RUSTSEC-2026-0382) and webpki-roots 1.0.10 (RUSTSEC-2026-0381). The Docker image runs on Node.js 26.12.1, pins each base image by its multi-platform digest, and installs corepack from a lockfile with an integrity hash.

Upgrade notes

  • fallow audit verdicts can change. An inherited complexity finding that got worse now gives a warn verdict, with exit code 0. Growth of an exceeded metric under --gate new-only no longer fails the audit. To keep the earlier gate, set audit.worsened: "error" or pass --worsened error. With audit.worsened: off, growth of an exceeded metric now also gives a warn verdict. Fallow does not reuse cached base snapshots of earlier versions.

  • A missing workspace directory turns off the unused catalog entry check for all catalogs. Create the directory before the run, or remove the pattern, to get the check back.

  • fallow dead-code can report new unused files after the Vitest change. Files that only a fixture Vitest config marked as used can now show up. You can remove an entry workaround for test files that Vitest finds.

  • Mutation scores can change. Projects with ORM writes can see more pruned mutants and a different score.

  • Save the dead-code baseline again to get the node_modules install state check. A baseline without the key loads as before.

  • The first run after the upgrade parses each file again. The extraction and template scan cache versions changed.

Full Changelog: v3.34.1...v3.35.0