Skip to content

v3.30.0: flag retirement report, startup import weight, faster language server, Oxc 0.151

Choose a tag to compare

@BartWaardenburg BartWaardenburg released this 26 Sep 22:28
· 529 commits to main since this release
Immutable release. Only release title and notes can be modified.
v3.30.0
2b0f0b7

Features

  • fallow flags --retirement reports the flags that you can retire. Each row groups every site of one flag and lists the reasons to remove it. The reasons are single-read-site, test-only, literal-constant, identical-branches, empty-branch, guards-dead-code and defined-never-read. The literal-constant reason adds the new kind constant for a flag-prefixed module-level const with a literal value, such as const FEATURE_NEW_UI = true. A guard in the same module must test the flag. These flags are in the retirement report only, not in feature_flags[]. The defined-never-read reason also covers a Vercel flag() definition with an unused export, and an unused member of a flag registry enum. The reasons come from the code only, so they do not show the production state of the flag.
  • The retirement report works in every fallow flags format and gives each flag an age from git. JSON adds a top-level retirement object, and human and markdown output add a "Retirement candidates" section. Compact adds flag-retire: lines, SARIF adds the rule fallow/flag-retirement-candidate at level note, and CodeClimate adds fallow/flag-retirement issues. --flag-age blame (the default), --flag-age pickaxe and --flag-age off set how Fallow measures the age. --reason, --min-age, --sort age|sites|name and --top filter and order the rows. Fallow removes no code. Every action has auto_fixable: false. fallow explain flag-retirement describes the rule.
  • fallow flags --retirement --flag-state <FILE> compares the code with a vendor flag export. The file is a local JSON file with one vendor-neutral schema, and Fallow reads it offline without credentials or network calls. The export adds the reasons fully-rolled-out, archived-in-vendor, missing-in-vendor and vendor-only. The vendor-only rows do not count in summary.distinct_flags. The new flags.vendorKeyPrefix config key removes a prefix from each vendor key before the match. An invalid file, or a file larger than 16 MiB, exits with code 2 and the error code FALLOW_FLAG_STATE_INVALID.
  • fallow flags --retirement can gate CI on flag counts and flag age. --save-regression-baseline <PATH> writes a baseline with a new flags section. --fail-on-regression --regression-baseline <PATH> exits with code 1 when distinct_flags grows more than --tolerance. --max-flag-age <DAYS> exits with code 1 when a flag in scope is older than that limit. Each gate reports a status of pass, exceeded or skipped, and prints the result to stderr in every format. A run with --changed-since or --workspace skips the regression gate and saves no baseline. Without --retirement, the regression options have no effect on fallow flags, and the command prints a warning.
  • The MCP feature_flags tool and the Rust API can return the retirement report. The MCP tool adds the retirement, flag_state and flag_age parameters. FeatureFlagsOptions adds a retirement field. The CLI and the API build the report with the same engine function, so the retirement block is the same on both. The regression gates stay CLI only.
  • fallow flags finds more flag reads. The scan now reports import.meta.env.X reads, and SDK calls with a registry member as the name, such as useFlag(FLAGS.NewCheckout). It also finds flag reads inside a larger if or ternary test, such as if (process.env.FEATURE_X === 'true'). Flags in .js files with JSX now match custom sdkPatterns and envPrefixes. More flag reads now have a guard, so dead_code_overlap can find more unused exports.
  • fallow list --entry-weight reports the startup import weight of each runtime entry point. The report counts the project modules and the source bytes that load before the entry runs. It splits the modules into eager, deferred and out of thread, and lists the packages on the startup path. The unit is source bytes on disk, so the value is not a bundle size. The output is human or JSON (entry_weight in fallow list --format json). The health score does not change.
  • An opt-in regression gate for the startup import weight. fallow list --entry-weight --save-regression-baseline <PATH> writes the eager bytes, modules and packages of each entry into the baseline file. A later run with --regression-baseline <PATH> reports the change of each entry and the new eager packages. Add --fail-on-regression to exit 1 when an entry grows more than --tolerance. A new entry never fails the gate. fallow dead-code --save-regression-baseline <PATH> now keeps the entry weights in the same file.
  • fallow trace --path and fallow viz show dynamic imports. Each hop in the trace JSON has a new dynamic field, and the human output tags such a hop [dynamic]. With --eager-only, the trace follows static value imports only. A new edge flag bit (2) marks a lazy edge. The focus view draws it with a short dash.
  • Built-in Waku plugin. The waku plugin activates from the waku dependency and follows the managed-mode file router. Every module under <srcDir>/pages is an entry, except modules in _components, _hooks and _actions folders. The plugin reads srcDir from waku.config.*, with src as the default. Thanks @aheissenberger for the contribution (#2921).
  • The language server can parse the project before the first open. Set the initialization option prewarm to true. At initialized, the server then loads the project session and parses the files, but analyzes and publishes nothing. The option is off by default and needs kept project sessions and a workspace root with a package.json.
  • --performance reports exact work counts, the time outside the pipeline and a span tree. The dead-code timings gain a counters object with counts that do not change with the thread count or the machine. A standalone dead-code run adds a process object and a Process section with a WALL row. The new spans array gives the parent of each stage and marks concurrent spans. Health --performance adds git_log_bytes, and the parse cache load gets its own parse_cache_load_ms field.
  • Runtime hot paths show where speed work gives the largest gain. Each entry in runtime_coverage.hot_paths now has an optimization_target block. The block holds cost_score, cost_basis, inner_iterations_per_call, cognitive, cyclomatic and line_count. Compare cost_score only between hot paths with the same cost_basis. The human output, --explain and the MCP get_hot_paths tool show the same fields.

Performance

  • Import resolution, stylesheet scans and coverage remaps do less work. Each specifier in a file now resolves once, not once for each imported binding. A stylesheet is masked for comments one time, and the health --css token scans count lines in one pass. Runtime coverage indexes each source-mapped script one time and finds each offset with a binary search. The findings do not change.
  • The language server keeps its project session between saves. A save now parses only the files that changed, and the other modules come from memory. A change to a config input, such as package.json, a lockfile, a tsconfig file or a rule pack, loads the session again. On a platform without a file change time, such as Windows, each run reads the persisted parse cache as before. The server keeps sessions while their estimated memory is at most 512 MB in total. Set FALLOW_LSP_REUSE_SESSION=0 to load a new session on each run.
  • The language server does less work for each analysis run. It publishes only the diagnostics that changed, and converts diagnostic columns in linear time. It reads an open file only when the buffer can differ from disk. Under autosave, a newer event cancels the run in flight, and the run after a cancelled one always finishes and publishes.
  • Typed MCP tool calls share parsed modules. A later call on the same unchanged files takes the modules from memory. For the sequence analyze, find_dupes, check_health, trace_file, run two times, the parse passes go from 6 to 1. The answers do not change. The store keeps at most 4 file lists and about 512 MiB of parsed modules. It does not keep a project with more than about 40 MiB of source. To stop the reuse, set FALLOW_MCP_WARM_SESSION=0.
  • fallow flags parses each file once with a custom flags config. Before, sdkPatterns, envPrefixes or configObjectHeuristics made the command parse every file a second time. The command also matches guarded flags to unused exports by file and line. The dead_code_overlap output does not change.
  • Fallow starts fewer git processes. A run without --diff-file, --diff-stdin or FALLOW_DIFF_FILE no longer calls git rev-parse for the diff base directories. The Impact project identity reads the git common directory and the toplevel with one call in a work tree.
  • The VS Code extension no longer searches the workspace for manifests at startup. It starts when the workspace root has a package.json or a Fallow config file. It also starts when a JavaScript, TypeScript, Vue, Svelte, Astro or MDX file opens. A monorepo folder with no root package.json now starts the extension when the first source file opens.
  • fallow viz pages load and respond faster. The page parses only the file list, the edges and the summary at start. On the Fallow repository, the data parsed at start drops from 2.49 MB to 64 KB. The HTML file drops from 2.8 MB to 1.9 MB. A hover on the treemap now makes 3 draw calls, not one call per tile. The page shows its frame before the script runs, so the layout does not shift.

Changed

  • fallow flags gives medium confidence to generic SDK names without a flag import. The names isEnabled, getValue and useFeature also occur in libraries that are not flag SDKs. A call to one of these names now has confidence: "medium" when its file imports no flag SDK and no flag module. Other SDK names, such as useFlag and checkGate, keep high confidence.
  • fallow flags reports config.features.x as one read. With flags.configObjectHeuristics on, the scan reported the full access and also the object config.features. Now it reports only the full access.
  • Oxc 0.151. The parser and AST crates move from Oxc 0.126 to 0.151, and oxc_coverage_instrument moves to 0.13. The dead-code, duplication and health output of public projects is the same as with 3.29.0.
  • fallow similar-code inspect and fallow audit use the shared test-code definition. Both now treat files under __test__/, spec/, specs/ and e2e/, and files named *.e2e.* and *.e2e-spec.*, as tests. similar-code inspect also adds *.cy.* files, and no longer lists files under __mocks__/. In fallow audit, a mock or a fixture that imports a changed file does not count as a test.

Bug fixes

  • Health hotspots tag more test files. The [test] tag and the JSON field is_test_path now use the shared test-path definition. A test/, tests/, __tests__/ or __mocks__/ directory at the project root now matches. A .test. or .spec. marker now matches only in the file name.
  • Test-path checks now classify paths relative to the project root. Before, a project inside a test-like directory lost three results. The split line with test files and source files did not show under test, tests or fixtures. The start with <file> hint of the bare fallow command named no file under tests or examples. The CVA checks lost every finding under test or tests. The split line now says M in test files, not M in test directories.
  • .cy. files and spec/ directories need more evidence to be tests. A .cy. file is now a test only in a Cypress context, so a locale file such as src/i18n/strings.cy.ts is production code. A spec or specs directory now holds tests only at a test root.
  • A plain fallow list ignores the --tolerance value. Before, an invalid value exited 2 for a plain listing. Now fallow list parses the value only for --entry-weight.
  • pnpm overrides in a two-document lockfile are no longer reported as unused. pnpm 12 writes pnpm-lock.yaml as two YAML documents when package.json sets packageManager. Fallow now reads each project document of the lockfile. Thanks @sgaabdu4 for the report (#2909).
  • On Windows, plugin path rules skip the directories they exclude. A plugin can exclude files by a path segment, such as the TanStack Router routeFileIgnorePattern. Before, the check did not split paths on the native Windows separator, so an excluded route file was still an entry point.

Upgrade notes

  • Building fallow from source now needs Rust 1.96 or later (it was 1.92).
  • The parse, extract, graph and duplication caches rebuild once, so the first run after the upgrade parses and resolves the project again.
  • isEnabled, getValue and useFeature calls without a flag import change from high to medium confidence. To keep high confidence for your own function, add its name to flags.sdkPatterns.
  • With flags.configObjectHeuristics on, object entries such as process.features no longer show in the output.
  • Waku projects: remove manual src/pages/** entries. They credit every export, so a typo such as getconfig goes unreported under includeEntryExports.
  • --max-flag-age needs git history. Without full history the gate is skipped. To get full history, set fetch-depth: 0 in GitHub Actions.
  • The human split line under unused files and unused exports now says M in test files. Update scripts that match the old text.
  • Language server session reuse needs a client that registers watched files.

Full Changelog: v3.29.0...v3.30.0