Repository navigation
[eslint-miner] eslint-factory: add no-split-filter-trim-without-trim-map rule - #69831
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft. Design Decision Gate 🏗️Outcome: The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible. Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.
|
|
✅ Ponytail Reviewer completed successfully! Ponytail ReviewerOutcome: The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible. Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.
|
|
✅ PR Code Quality Reviewer completed the code quality review.
|
|
🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅
|
Comment MemoryPeek at saved memory (pr-code-quality-reviewer)Note This comment is managed by comment memory.Expand the saved memory block to view or edit the persistent context for this thread.
|
There was a problem hiding this comment.
Request changes
This rule has correctness gaps in the matcher itself: it currently misses optional-chained split() receivers and it also over-matches unrelated binary comparisons, so it can both under-report and over-report the pattern it claims to enforce.
Themes
- optional chaining is not unwrapped before the.split(...) check
BinaryExpressionmatching is too loose for a rule that is supposed to detect blank-entry truthiness filters
Static review only; I reviewed the capped diff and a few adjacent rule implementations, but did not run the rule locally.
🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 59.5 AIC · ⌖ 5.97 AIC · ⊞ 12.7K
Comment /review to run again
| const param = predicate.params[0].name; | ||
|
|
||
| let source: TSESTree.Node = callee.object; | ||
| if (source.type !== "CallExpression" || source.callee.type !== "MemberExpression" || source.callee.computed || source.callee.property.type !== "Identifier" || source.callee.property.name !== "split") return; |
There was a problem hiding this comment.
This misses optional-chained split() receivers, so one of the rule's advertised target shapes can slip through unflagged and the current test suite gives false confidence.
💡 Why this is a real correctness gap
In@typescript-eslint/Espree, x?.split(",") is wrapped in a ChainExpression, so callee.object is not always a bare CallExpression. This branch returns early before you ever inspect the inner .split(...), which means code like process.env.X?.split(",").filter(p => p.trim()) can be missed even though the new test marks it invalid. Please unwrap ChainExpression before the source.type !== "CallExpression" check (same pattern other rules here already use), then keep traversing the inner call.There was a problem hiding this comment.
Handled in 36be333: unwrap ChainExpression before checking the split call and retain the optional-chain regression case.
| if (isTrimCallOn(node, paramName)) return true; | ||
| if (node.type === "UnaryExpression" && node.operator === "!") return isTrimmedTruthiness(node.argument, paramName); | ||
| if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.property.name === "length") return isTrimCallOn(node.object, paramName); | ||
| if (node.type === "BinaryExpression") return isTrimmedTruthiness(node.left, paramName) || isTrimmedTruthiness(node.right, paramName); |
There was a problem hiding this comment.
The BinaryExpression fallback is broad enough to flag legitimate comparisons, which turns this from a focused bug detector into a noisy false-positive generator.
💡 Why the current match is too loose
isTrimmedTruthiness() treats any binary expression with x.trim() on either side as a hit, so predicates like parts.split(",").filter(p => p.trim() !== p) or filter(p => p.trim().length === min) would be reported even though they are not using trim() as a blank-entry truthiness check. That creates merge-blocking lint noise on valid filtering logic. Please narrow the binary handling to the specific truthiness shapes you actually want (x.trim().length > 0, x.trim() !== "", etc.) and add a valid test that preserves an intentional comparison.There was a problem hiding this comment.
Handled in 36be333: matching is limited to nonempty trim truthiness and explicit empty-string/positive-length comparisons; unrelated comparisons now have regression coverage.
There was a problem hiding this comment.
Skills-Based Review 🧠
Applied /tdd — the new rule itself is well-tested and reasonably scoped (newline exclusion, truthiness-shape matching), but the PR is missing a required README update that an existing repo-level test enforces.
📝 Triage
pr-triage classified this as a new_feature adding a custom ESLint rule with comprehensive unit tests; recommended skills /tdd and /grill-with-docs.
📋 Key Themes & Highlights
Key Themes
- Missing README entry (likely CI-breaking):
eslint-factory/src/index-readme-parity.test.tsasserts every rule exported fromindex.tshas a matching### \rule-name`heading inREADME.md. This PR addsno-split-filter-trim-without-trim-maptoindex.tsbut not toREADME.md`, so that parity test should fail as written. - Minor detection gap: the rule only matches
ArrowFunctionExpressionpredicates, missingfunction (v) { ... }callbacks with the identical bug. No current site inactions/setup/jsuses that form, so it is low severity, but worth a follow-up test case for completeness.
Positive Highlights
- ✅ Clear, well-justified exclusion of newline separators with an inline comment explaining the rationale.
- ✅
isTrimmedTruthinesscorrectly recurses through!x.trim(),.lengthchecks, and binary expressions — good anticipation of real-world variants. - ✅ Test suite covers both direct and chained usages (
.trim().length > 0, block-statementreturn), with real evidence cited fromactions/setup/js.
Requesting changes primarily to add the README documentation entry so the parity test passes; the predicate-shape gap is a nice-to-have follow-up.
🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 59.1 AIC · ⌖ 13.9 AIC · ⊞ 10.1K
Comment /matt to run again
| "no-single-char-string-replace": noSingleCharStringReplaceRule, | ||
| "no-unguarded-async-timer-callback": noUnguardedAsyncTimerCallbackRule, | ||
| "no-async-foreach-callback": noAsyncForEachCallbackRule, | ||
| "no-split-filter-trim-without-trim-map": noSplitFilterTrimWithoutTrimMapRule, |
There was a problem hiding this comment.
[/tdd] This PR registers a new rule but does not add a matching ### no-split-filter-trim-without-trim-map`` section to eslint-factory/README.md. The repo has an automated parity check (`src/index-readme-parity.test.ts`) that asserts every exported rule name has a matching README heading — this PR as-is would fail that test.
💡 Details
The PR description claims npm run build, rule unit tests, and npm run lint:setup-js were run and passed, but doesn't mention the full vitest suite. index-readme-parity.test.ts does a structural diff between the rules object in index.ts and ### rule`` headings in README.md (sorted-array equality), so a missing entry fails loudly.
Add a row to the rules table and a ### no-split-filter-trim-without-trim-map`` section (following the existing no-map-parseint-callback pattern) describing the pattern and fix.
@copilot please address this.
There was a problem hiding this comment.
Documented the rule in the linked Rules table and with a supported-pattern/safe-replacement section in 36be333; the README parity test passes.
| const callee = node.callee; | ||
| if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || callee.property.name !== "filter") return; | ||
| const predicate = node.arguments[0]; | ||
| if (!predicate || predicate.type !== "ArrowFunctionExpression" || predicate.params.length !== 1 || predicate.params[0].type !== "Identifier") return; |
There was a problem hiding this comment.
[/tdd] The predicate-shape check only accepts ArrowFunctionExpression; a .filter(function (v) { return v.trim(); }) callback with the same bug is silently skipped. No current site in actions/setup/js uses this form, so it's low-severity, but the rule's own message ("Filtering split() output...") implies general coverage.
💡 Suggested fix
if (!predicate || (predicate.type !== "ArrowFunctionExpression" && predicate.type !== "FunctionExpression") || predicate.params.length !== 1 || predicate.params[0].type !== "Identifier") return;Add a corresponding invalid case to the test file, mirroring the existing no-async-foreach-callback rule which already handles both function shapes.
@copilot please address this.
There was a problem hiding this comment.
Handled in 36be333: both arrow and function-expression filter callbacks are supported, with a function-expression regression case.
There was a problem hiding this comment.
🟡 Changes recommended
False positives can prompt behavior-changing rewrites, and missing README documentation fails the existing parity check.
3 open issues
What changed in this PR
Adds a warning-level rule to gh-aw’s custom ESLint plugin for split/filter chains that test trimmed values but retain surrounding whitespace.
Changes:
- Detects trim-based predicates, excluding newline separators.
- Adds rule tests and documentation-URL validation.
- Registers and enables the rule as a warning.
| File | Description |
|---|---|
eslint-factory/src/rules/no-split-filter-trim-without-trim-map.ts |
Implements pattern matching and diagnostics. |
eslint-factory/src/rules/no-split-filter-trim-without-trim-map.test.ts |
Tests accepted and reported patterns and the documentation URL. |
eslint-factory/src/index.ts |
Registers the new rule. |
eslint-factory/eslint.config.cjs |
Enables warning-level diagnostics. |
Review effort: Max
| if (node.type === "UnaryExpression" && node.operator === "!") return isTrimmedTruthiness(node.argument, paramName); | ||
| if (node.type === "MemberExpression" && !node.computed && node.property.type === "Identifier" && node.property.name === "length") return isTrimCallOn(node.object, paramName); | ||
| if (node.type === "BinaryExpression") return isTrimmedTruthiness(node.left, paramName) || isTrimmedTruthiness(node.right, paramName); |
There was a problem hiding this comment.
Handled in 36be333: the matcher now restricts negation to even counts and comparisons to explicit nonempty checks. The cited counterexamples are valid regression cases.
| const expression = body.type === "BlockStatement" ? (body.body.length === 1 && body.body[0].type === "ReturnStatement" ? body.body[0].argument : null) : body; | ||
| if (!expression || !isTrimmedTruthiness(expression, param)) return; | ||
|
|
||
| context.report({ node, messageId: "filterTrimKeepsWhitespace", data: { param } }); |
There was a problem hiding this comment.
Handled in 36be333: the rule skips a directly following synchronous trim map, covered by expression-body and single-return-block mapper tests.
| "no-single-char-string-replace": noSingleCharStringReplaceRule, | ||
| "no-unguarded-async-timer-callback": noUnguardedAsyncTimerCallbackRule, | ||
| "no-async-foreach-callback": noAsyncForEachCallbackRule, | ||
| "no-split-filter-trim-without-trim-map": noSplitFilterTrimWithoutTrimMapRule, |
There was a problem hiding this comment.
Added the linked Rules-table row and backtick-quoted heading with the supported pattern and safe replacement in 36be333; README parity tests pass.
…lter-trim-20261018-6b26026061e0b8ab Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
…map rule (#69831) Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>



Adds
gh-aw-custom/no-split-filter-trim-without-trim-map(warn).Pattern:
x.split(",").filter(v => v.trim())drops blank entries but keeps surrounding whitespace on the rest ("a, b"gives" b"). Newline separators are excluded to limit false positives.Evidence in
actions/setup/js(lint output):check_permissions_utils.cjs(GH_AW_REQUIRED_ROLES,GH_AW_ALLOWED_BOTS),redact_secrets.cjs(secret names, 2 sites),extra_empty_commit.cjs. Env-list values with spaces would silently fail matching.Validation:
npm run build, rule unit tests, andnpm run lint:setup-jsrun; the rule reports the sites above. No files outsideeslint-factorychanged.