feat(rules): prefer node:process over the process global, and sweep packages/cli/src - #348
Conversation
…:process An ast-grep rule, prefer-node-process, reports a bare `process` identifier in packages/cli/src when the file has no `import … from "node:process"` and declares no binding named `process` (const/let/var, parameter, or catch clause). A file that shadows the name anywhere is treated as clean for the whole file, and `globalThis.process` is a property access and out of scope. Reference behaviour is ESLint n/prefer-global/process: ["error", "never"]. No fix is attempted: an import needs placement.
Add `import process from "node:process";` to the 27 files under packages/cli/src that reached for the global, placed in the Node built-ins group per .conventions/STYLEGUIDE-CODE.md. Nothing else changes in these files; this is what makes the prefer-node-process rule land green.
…balThis.process The shadow check now walks the reference's ancestors instead of the whole file: an enclosing function, arrow, or method binding a `process` parameter, an enclosing `catch (process)`, or an enclosing block (or the file) whose direct child declares `process`, including an exported one. A binding in a sibling scope no longer silences a global reach elsewhere in the file. `globalThis.process` is now an independent finding regardless of imports or shadowing: it is the same reach with the global spelled out.
|
@claude /review |
|
Claude finished @thecodedrift's task in 4m 27s —— View job Review completeReview mode: incremental — read 0 prior review thread(s) before reviewing.
Rule logic ( The 27-file sweep: Confirmed all 27 files add exactly one One finding, posted inline (cleanup only, not a correctness bug):
Nothing else surfaced — the rule's documented limitations (destructured-parameter shadows, |
Read and acknowledged. The one inline finding (unreachable — AI Coding Agent |
What
Three commits, one PR, so the rule lands green:
feat(rules): a Taskless ast-grep rule,prefer-node-process, under.taskless/rules/sg/prefer-node-process/, authored by followingpnpm cli agent create-sg-rule(topic v5) against a freshpnpm buildof this tree.refactor(cli): the sweep.import process from "node:process";added to every file underpackages/cli/srcthe rule flagged. 27 files, not the 25 the issue estimated (measured by running the rule over the unswept tree: 91 findings across 27 files). One added line per file, two in the four files that had no Node built-in import yet (the import plus the blank line that separates the group). Nothing else changes in those files.feat(rules): two refinements from review. Shadowing is ancestor-scoped rather than file-scoped, andglobalThis.processis always a finding.What the rule catches
Two independent forms (a top-level
any:), bothseverity: error, inpackages/cli/src/**/*.ts:1. A bare
processidentifier (kind: identifier,regex: ^process$) when the file has noimport … from "node:process"(any import form: default, named, namespace; matched on the specifier string), unless an ancestor of the reference binds the name:function, function expression, generator, arrow, or method whose parameter list has a required or optional parameter namedprocess(also the unparenthesisedprocess => …arrow form);catch (process);statement_block, or theprogramitself, whose direct child is aconst/let/varwith a declarator namedprocess, or anexportof one.Because the walk is over ancestors, a binding in a sibling scope does not count:
function kill(process) {…}next tofunction cwd() { return process.cwd(); }reports the second, and{ const process = …; }followed byprocess.cwd()outside the block reports the outside reach. Measured withast-grep scanon each such fixture: exactly one finding, on the global reach only.Property accesses (
foo.process,{ process: x },settings.process) areproperty_identifiernodes, notidentifier, so they never match. ESLint'sn/prefer-global/process: ["error", "never"]is the reference behaviour. There is nofix: an import needs placement, so the finding names the file and the line gets added by hand.2.
globalThis.process, always. No import and no shadow excuses it: it is the same reach with the global spelled out, and it is never the module-graph dependency the rule asks for. The recipe's rule shape has onemessageper rule, so thenoteexplains both forms rather than a per-match message.What it does not catch (deliberately, and written into the rule's
note)for (const process of …)head, a destructured parameter({ process }), a class field) are not treated as shadows and a reach under them would be reported. None exist inpackages/cli/srctoday.import process from "process"(nonode:prefix) is still flagged at the usage sites, because only thenode:processspecifier satisfies the rule. That is the convention the rest of ournode:imports follow, so this is intended.packages/cli/src/**/*.tsonly..github/scripts/*.cjs(CommonJS) are excluded and would not match theTypeScriptparser anyway.Tests
.tests/prefer-node-process-20260918-test.yml: 14 valid, 11 invalid. Beyond the four cases the issue asks for: destructuring (const { env } = process), a named import (import { env } from "node:process"),node:child_process(must not match), optional and single-arrow parameters, method parameters,catch (process), an exportedconst process, a reach from a nested arrow inside a shadowing function (valid), a shadowing parameter in one function with a global reach in another (invalid), a block-scopedconst processwith a reach outside the block (invalid), acatch (process)followed by a top-level reach (invalid), andglobalThis.processbare, next to anode:processimport, and under a shadowing parameter (all invalid).Mutation standard, each run separately with the file restored between runs: moving any one of the six new invalid fixtures (sibling parameter, block const, catch-then-global, the three
globalThis.processforms) intovalidmakespnpm cli testreport1 test case(s) failedand exit 1. Regressing the rule itself to file-scoped shadowing (stopBy: endon theprogramdeclaration check) also fails the suite. Restored, it passes.Why no changeset
A repo-local rule plus an import sweep changes nothing a consumer of
@taskless/clican observe: the built bundle externalisesnode:*, andpnpm build(whoseassert-library-graphsplugin reads rollup's resolved chunk graph) still emits every entry, so no host-free entry picked up a Node import.skip-changesetlabel applied.Verification (at the tip commit)
pnpm cli verify .taskless/rules/sg/prefer-node-process --json→ok: truepnpm cli test .taskless/rules/sg/prefer-node-process --json→ok: truepnpm cli check(with runtime rules) andpnpm cli check --anonymous→No issues found.(0 results)pnpm typecheck→ passpnpm lint(builds first, thenpnpm cli check) → exit 0pnpm --filter @taskless/cli test→ 94 files, 1528 tests passedgit diff --stat origin/main→ 29 files: 2 rule files + 27 swept sources, each with one added import lineFixes #347