From 99e71c55c8fbcdc030410f20061b9846e22fa842 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:58:23 +0000 Subject: [PATCH 1/2] Initial plan From b5f9f26681ab8913d6d02123301c3615ed3de3fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 08:12:05 +0000 Subject: [PATCH 2/2] fix(eslint): resolve identifier-bound route args in no-github-request-interpolated-route Add write-once initializer chain resolution to the route rule, matching the existing pattern in no-exec-interpolated-command and no-child-process-interpolated-command. When the first argument to .request() is an Identifier, resolve it to its initializer before classification so that patterns like: const route = `GET /repos/${owner}/${repo}`; github.request(route, {}); are correctly flagged as interpolatedRoute. - Import resolveWriteOnceInitializerChain from command-initializer-utils - Resolve firstArg via resolveWriteOnceInitializerChain before calling getInterpolatedRouteKind / isOpaqueWholeRouteInterpolation - Update tests: remove old "not resolved" valid case, add new cases for identifier-resolved routes, static ternary true-negative, and write-once reassignment guard Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- ...-github-request-interpolated-route.test.ts | 48 ++++++++++++++++++- .../no-github-request-interpolated-route.ts | 9 ++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/eslint-factory/src/rules/no-github-request-interpolated-route.test.ts b/eslint-factory/src/rules/no-github-request-interpolated-route.test.ts index b97665b098a..d819b72d5b3 100644 --- a/eslint-factory/src/rules/no-github-request-interpolated-route.test.ts +++ b/eslint-factory/src/rules/no-github-request-interpolated-route.test.ts @@ -72,8 +72,6 @@ describe("no-github-request-interpolated-route", () => { valid: [ // `this.github` is not resolved — `this` is not an Identifier "this.github.request(`GET /repos/${owner}/${repo}`, { owner, repo });", - // Variable indirection for the route argument is not resolved - "const route = `GET /repos/${owner}/${repo}`; github.request(route, { owner, repo });", `github.request("GET /repos/".concat(owner, "/", repo), { owner, repo });`, `github.request("GET /repos" + "/{owner}/{repo}", { owner, repo });`, ], @@ -345,4 +343,50 @@ describe("no-github-request-interpolated-route", () => { ], }); }); + + it("invalid: identifier-bound interpolated route is resolved and flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [], + invalid: [ + { + code: "function f(owner, repo) { const route = `GET /repos/${owner}/${repo}`; github.request(route, {}); }", + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "template literal with interpolations", client: "github" }, + }, + ], + }, + { + code: `function f(owner, repo) { const route = "GET /repos/" + owner + "/" + repo; github.request(route, {}); }`, + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "string concatenation expression", client: "github" }, + }, + ], + }, + ], + }); + }); + + it("valid: identifier-bound fully-static ternary route is not flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [ + // Fully static ternary — both branches are literals, so this is a true negative + `function f(cond) { const route = cond ? "GET /a" : "GET /b"; github.request(route, {}); }`, + ], + invalid: [], + }); + }); + + it("valid: reassigned identifier is not resolved (write-once semantics)", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [ + // let binding reassigned after declaration — write-once chain does not apply + "function f(owner, repo, suffix) { let route = `GET /repos/${owner}/${repo}`; route = suffix; github.request(route, {}); }", + ], + invalid: [], + }); + }); }); diff --git a/eslint-factory/src/rules/no-github-request-interpolated-route.ts b/eslint-factory/src/rules/no-github-request-interpolated-route.ts index eff4c298f74..1a2496a7856 100644 --- a/eslint-factory/src/rules/no-github-request-interpolated-route.ts +++ b/eslint-factory/src/rules/no-github-request-interpolated-route.ts @@ -1,4 +1,5 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils"; +import { resolveWriteOnceInitializerChain } from "./command-initializer-utils"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); @@ -219,12 +220,14 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ if (!clientName) return; const firstArg = node.arguments[0]; - if (!firstArg) return; + if (!firstArg || firstArg.type === AST_NODE_TYPES.SpreadElement) return; - const routeKind = getInterpolatedRouteKind(firstArg); + const routeExpr = resolveWriteOnceInitializerChain(firstArg as TSESTree.Expression, context.sourceCode); + + const routeKind = getInterpolatedRouteKind(routeExpr); if (!routeKind) return; - if (isOpaqueWholeRouteInterpolation(firstArg)) { + if (isOpaqueWholeRouteInterpolation(routeExpr)) { context.report({ node: firstArg, messageId: "opaqueWholeRoute",