Rule
eslint-factory/src/rules/no-github-request-interpolated-route.ts
Design gap
getInterpolatedRouteKind() is called directly on firstArg of <client>.request(firstArg, ...). It only recognizes an inline TemplateLiteral/BinaryExpression — it never resolves an Identifier first argument back to its initializer. Contrast with the sibling command rules in the same factory, no-exec-interpolated-command and no-child-process-interpolated-command, which both call resolveWriteOnceInitializerChain() (src/rules/command-initializer-utils.ts) specifically so that const cmd = \git ${branch}`; exec.exec(cmd)is still caught.no-github-request-interpolated-route` has no equivalent step, so:
const route = `GET /repos/${owner}/${repo}${suffix}`;
github.request(route, {});
... is silently unflagged today, even though the inline form github.request(\GET /repos/${owner}/${repo}${suffix}`, {})is correctly flagged asinterpolatedRoute`.
Why this matters / grounding
The "extract a route into a local variable" idiom is already established in this exact codebase — create_project.cjs:230 and update_project.cjs:919 both do:
const route = projectInfo.scope === "orgs" ? "POST /orgs/{org}/projectsV2/{project_number}/views" : "POST /users/{user_id}/projectsV2/{project_number}/views";
...
const response = await github.request(route, params);
This particular instance is fully static (both ternary branches are literals) so it's a correct true-negative today. But it proves the const route = ...; client.request(route, ...) shape is a normal, encouraged pattern here — and the rule's own opaqueWholeRoute message text ("pass a typed route string from the caller when the entire route is dynamic") actively steers developers toward exactly the identifier-bound shape the rule cannot see through. The very first time someone builds that caller-provided route dynamically (e.g. via template literal or concatenation) instead of a static literal, the rule goes blind.
Ask
Give no-github-request-interpolated-route the same write-once scope resolution the exec/child_process rules already have, so an Identifier first argument is resolved to its initializer (reusing or adapting resolveWriteOnceInitializerChain, ideally factored into a rule-agnostic shared util since three rules now need it) before classifying static vs. interpolated vs. concatenated.
Acceptance criteria
Scope
eslint-factory/src/rules/no-github-request-interpolated-route.ts (+ test file). No production code in actions/setup/js/** needs to change — this is a rule-only false-negative fix; the only live call sites found (add_reaction.cjs, add_reaction_and_edit_comment.cjs, add_workflow_run_comment.cjs, assign_agent_helpers.cjs, create_project.cjs, update_project.cjs, route_slash_command.cjs, update_activation_comment.cjs, notify_comment_error.cjs, autofix_code_scanning_alert.cjs, close_issue.cjs, create_agent_session.cjs) already use static or safely-typed routes.
Generated by 🤖 ESLint Refiner · agent · 224.3 AIC · ⌖ 28.6 AIC · ⊞ 4.9K · ◷
Rule
eslint-factory/src/rules/no-github-request-interpolated-route.tsDesign gap
getInterpolatedRouteKind()is called directly onfirstArgof<client>.request(firstArg, ...). It only recognizes an inlineTemplateLiteral/BinaryExpression— it never resolves anIdentifierfirst argument back to its initializer. Contrast with the sibling command rules in the same factory,no-exec-interpolated-commandandno-child-process-interpolated-command, which both callresolveWriteOnceInitializerChain()(src/rules/command-initializer-utils.ts) specifically so thatconst cmd = \git ${branch}`; exec.exec(cmd)is still caught.no-github-request-interpolated-route` has no equivalent step, so:... is silently unflagged today, even though the inline form
github.request(\GET /repos/${owner}/${repo}${suffix}`, {})is correctly flagged asinterpolatedRoute`.Why this matters / grounding
The "extract a route into a local variable" idiom is already established in this exact codebase —
create_project.cjs:230andupdate_project.cjs:919both do:This particular instance is fully static (both ternary branches are literals) so it's a correct true-negative today. But it proves the
const route = ...; client.request(route, ...)shape is a normal, encouraged pattern here — and the rule's ownopaqueWholeRoutemessage text ("pass a typed route string from the caller when the entire route is dynamic") actively steers developers toward exactly the identifier-bound shape the rule cannot see through. The very first time someone builds that caller-provided route dynamically (e.g. via template literal or concatenation) instead of a static literal, the rule goes blind.Ask
Give
no-github-request-interpolated-routethe same write-once scope resolution the exec/child_process rules already have, so anIdentifierfirst argument is resolved to its initializer (reusing or adaptingresolveWriteOnceInitializerChain, ideally factored into a rule-agnostic shared util since three rules now need it) before classifying static vs. interpolated vs. concatenated.Acceptance criteria
<client>.request()is a plainIdentifier, the rule walks its write-once initializer chain (function-scoped, single-definition, no reassignment — matching the existingresolveWriteOnceInitializerChainsemantics) before runninggetInterpolatedRouteKind/isOpaqueWholeRouteInterpolation.const route = \GET /repos/${owner}/${repo}`; github.request(route, {})→ reportsinterpolatedRoute`.const route = cond ? "GET /a" : "GET /b"; github.request(route, {})→ NOT flagged (fully static ternary stays a true negative, matching the livecreate_project.cjs/update_project.cjsidiom).let route = ...; route = otherExpr;) → not resolved through (write-once semantics preserved, matchesresolveInitializer's existing reassignment guard).Scope
eslint-factory/src/rules/no-github-request-interpolated-route.ts(+ test file). No production code inactions/setup/js/**needs to change — this is a rule-only false-negative fix; the only live call sites found (add_reaction.cjs,add_reaction_and_edit_comment.cjs,add_workflow_run_comment.cjs,assign_agent_helpers.cjs,create_project.cjs,update_project.cjs,route_slash_command.cjs,update_activation_comment.cjs,notify_comment_error.cjs,autofix_code_scanning_alert.cjs,close_issue.cjs,create_agent_session.cjs) already use static or safely-typed routes.