From c3be213d764dad705490258ab68d4a94f3b5800c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:42:41 +0000 Subject: [PATCH 1/7] Initial plan From c0f178e803e0be5256a21ab214b2b7620ca224ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:02:04 +0000 Subject: [PATCH 2/7] no-github-request-interpolated-route: scope-aware Octokit client detection Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- eslint-factory/README.md | 16 ++- ...-github-request-interpolated-route.test.ts | 117 +++++++++++++++++- .../no-github-request-interpolated-route.ts | 97 +++++++++++++-- 3 files changed, 217 insertions(+), 13 deletions(-) diff --git a/eslint-factory/README.md b/eslint-factory/README.md index adca5f865ca..5ae6e43c067 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -18,17 +18,27 @@ This project hosts custom ESLint linters for `/actions/setup/js`. ### `no-github-request-interpolated-route` -Disallow template literals with interpolations or string concatenation expressions as the route argument of Octokit `github` / `octokit` / `githubClient` / `octokitClient` `.request()` calls. +Disallow template literals with interpolations or string concatenation expressions as the route argument of Octokit `.request()` calls. Using an interpolated route bypasses Octokit's typed route dispatch, can silently produce malformed paths when values contain special characters, and prevents static analysis of the route string. +**Detected Octokit clients:** +- Well-known names: `github`, `octokit`, `githubClient`, `octokitClient`. +- `context.github` — the GitHub context object's client property. +- `getOctokit(...)` return values (bare call or module-member call). +- Simple `const`/`let` aliases of any of the above: + `const gh = github`, `const client = getOctokit(token)`, `const myClient = context.github`. + **Flagged forms:** - `` github.request(`GET /repos/${owner}/${repo}`, ...) `` — template literal with interpolations. - `github.request("GET /repos/" + owner + "/" + repo, ...)` — string concatenation. +- `` context.github.request(`GET /repos/${owner}/${repo}`, ...) `` — `context.github` client. +- `` const gh = github; gh.request(`GET /repos/${owner}/${repo}`, ...) `` — aliased client. +- `` const client = getOctokit(token); client.request(`GET /repos/${owner}/${repo}`, ...) `` — `getOctokit` result alias. **Out of scope:** -- `this.github.request(...)` / `context.github.request(...)` — only direct identifier clients are matched today. -- `github.request(route, ...)` — variable indirection is not resolved. +- `this.github.request(...)` — `this`-based member expressions are not resolved. +- `github.request(route, ...)` — variable indirection for the route argument is not resolved. - `github.request("GET /repos/".concat(owner), ...)` — `.concat()`-built routes are not inspected. - `github.request("GET /repos" + "/{owner}/{repo}", ...)` — compile-time constant concatenations are accepted. 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 90b2f46d7c9..68a9e464bb4 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 @@ -46,10 +46,15 @@ describe("no-github-request-interpolated-route", () => { it("valid: request() on an unknown client name is not flagged", () => { cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { valid: [ - // 'client' is not in the allow-list + // 'client' is not in the allow-list and not bound to an Octokit source "client.request(`GET /repos/${owner}/${repo}`);", // Dynamic computed access on known client name is not flagged "github['request'](`GET /repos/${owner}/${repo}`);", + // Node.js http/https request — must not be flagged (false-positive guard) + "http.request(`http://example.com/${path}`, callback);", + "https.request(`https://example.com/${path}`, options, callback);", + // Client bound to an unrelated value — must not be flagged + "const client = http.createServer(); client.request(`GET /repos/${owner}/${repo}`);", ], invalid: [], }); @@ -65,8 +70,9 @@ describe("no-github-request-interpolated-route", () => { it("valid: intentionally out-of-scope route forms are accepted", () => { cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { valid: [ + // `this.github` is not resolved — `this` is not an Identifier "this.github.request(`GET /repos/${owner}/${repo}`, { owner, repo });", - "context.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 });`, @@ -162,4 +168,111 @@ describe("no-github-request-interpolated-route", () => { ], }); }); + + it("valid: simple aliases bound to non-Octokit sources are not flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [ + // Alias of an unknown source — must not be flagged + "const gh = someUnknownClient; gh.request(`GET /repos/${owner}/${repo}`);", + // Alias of http module — must not be flagged + "const client = http; client.request(`GET /repos/${owner}/${repo}`);", + ], + invalid: [], + }); + }); + + it("invalid: context.github.request() with interpolated route is flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [], + invalid: [ + { + code: "context.github.request(`GET /repos/${owner}/${repo}`, { owner, repo });", + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "template literal with interpolations", client: "context.github" }, + }, + ], + }, + { + code: `context.github.request("GET /repos/" + owner + "/" + repo, { owner, repo });`, + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "string concatenation expression", client: "context.github" }, + }, + ], + }, + ], + }); + }); + + it("invalid: simple const alias of a known Octokit client is flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [], + invalid: [ + { + code: "const gh = github; gh.request(`GET /repos/${owner}/${repo}`, { owner, repo });", + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "template literal with interpolations", client: "gh" }, + }, + ], + }, + { + code: `const myClient = octokit; myClient.request("GET /repos/" + owner + "/" + repo, { });`, + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "string concatenation expression", client: "myClient" }, + }, + ], + }, + ], + }); + }); + + it("invalid: const alias of getOctokit() result is flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [], + invalid: [ + { + code: "const fallbackClient = getOctokit(token); fallbackClient.request(`POST /repos/${owner}/${repo}/issues/${number}/comments`, { body });", + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "template literal with interpolations", client: "fallbackClient" }, + }, + ], + }, + { + code: `const client = getOctokit(token); client.request("POST /repos/" + owner + "/" + repo, { });`, + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "string concatenation expression", client: "client" }, + }, + ], + }, + ], + }); + }); + + it("invalid: const alias of context.github is flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: [], + invalid: [ + { + code: "const gh = context.github; gh.request(`GET /repos/${owner}/${repo}`, { owner, repo });", + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "template literal with interpolations", client: "gh" }, + }, + ], + }, + ], + }); + }); }); 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 ad9dc58ab10..2d0f8eb9bd5 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,4 @@ -import { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; +import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); @@ -38,6 +38,36 @@ function isStringConcatenation(node: TSESTree.Node): boolean { return node.type === "BinaryExpression" && node.operator === "+" && !isStaticRouteExpression(node); } +/** + * Returns true when `node` is the `context.github` member expression. + */ +function isContextGithubExpression(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === AST_NODE_TYPES.Identifier && node.object.name === "context" && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "github" + ); +} + +/** + * Returns true when a syntactic expression node directly represents a known + * Octokit client source without scope resolution. Recognizes: + * - Direct known names: github, octokit, githubClient, octokitClient + * - `getOctokit(...)` call results (bare or accessed via a module member) + * - `context.github` member expression + */ +function isOctokitSourceExpression(node: TSESTree.Node): boolean { + if (node.type === AST_NODE_TYPES.Identifier && OCTOKIT_CLIENT_NAMES.has(node.name)) return true; + + if (node.type === AST_NODE_TYPES.CallExpression) { + const callee = node.callee; + if (callee.type === AST_NODE_TYPES.Identifier && callee.name === "getOctokit") return true; + if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "getOctokit") return true; + } + + if (isContextGithubExpression(node)) return true; + + return false; +} + export const noGithubRequestInterpolatedRouteRule = createRule({ name: "no-github-request-interpolated-route", meta: { @@ -45,7 +75,8 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ docs: { description: "Disallow template literals with interpolations or string concatenation as the route argument of Octokit " + - "github/octokit/githubClient/octokitClient .request() calls. " + + ".request() calls. Octokit clients are detected by well-known names (github, octokit, githubClient, octokitClient), " + + "getOctokit(...) call results, context.github, and simple const/let aliases of any of these. " + 'Use the typed placeholder form instead: "GET /repos/{owner}/{repo}" with a separate params object.', }, schema: [], @@ -58,23 +89,73 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ }, defaultOptions: [], create(context) { + const sourceCode = context.sourceCode; + type SourceCodeScope = ReturnType; + + /** + * Resolves an identifier name to check if it is bound to a known Octokit + * client source in the visible scope chain. Handles simple single-level + * assignments: + * const x = github + * const x = getOctokit(token) + * const x = context.github + */ + function isIdentifierBoundToOctokitClient(name: string, scopeNode: TSESTree.Node): boolean { + if (OCTOKIT_CLIENT_NAMES.has(name)) return true; + + let scope: SourceCodeScope | null = sourceCode.getScope(scopeNode); + while (scope) { + const variable = scope.set.get(name); + if (variable && variable.defs.length > 0) { + for (const def of variable.defs) { + if (def.type !== "Variable") continue; + const declarator = def.node as TSESTree.VariableDeclarator; + if (declarator.init && isOctokitSourceExpression(declarator.init)) return true; + } + return false; + } + scope = scope.upper; + } + return false; + } + + /** + * Returns the display name (for error messages) if the callee object + * resolves to a known Octokit client, or null otherwise. + * + * Recognized shapes: + * - `.request(...)` where name is in OCTOKIT_CLIENT_NAMES or is a + * simple alias of an Octokit source (scope-resolved) + * - `context.github.request(...)` + */ + function resolveOctokitClientName(calleeObject: TSESTree.Expression | TSESTree.Super, callNode: TSESTree.Node): string | null { + if (calleeObject.type === AST_NODE_TYPES.Identifier) { + return isIdentifierBoundToOctokitClient(calleeObject.name, callNode) ? calleeObject.name : null; + } + + if (isContextGithubExpression(calleeObject)) { + return "context.github"; + } + + return null; + } + return { CallExpression(node) { const callee = node.callee; // Only match .request(...) - if (callee.type !== "MemberExpression") return; + if (callee.type !== AST_NODE_TYPES.MemberExpression) return; if (callee.computed) return; - if (callee.object.type !== "Identifier") return; - if (!OCTOKIT_CLIENT_NAMES.has(callee.object.name)) return; - if (callee.property.type !== "Identifier") return; + if (callee.property.type !== AST_NODE_TYPES.Identifier) return; if (callee.property.name !== "request") return; + const clientName = resolveOctokitClientName(callee.object, node); + if (!clientName) return; + const firstArg = node.arguments[0]; if (!firstArg) return; - const clientName = callee.object.name; - if (isInterpolatedTemplateLiteral(firstArg)) { context.report({ node: firstArg, From 5336e5aa3c04746d5b310bcefe57127696f82951 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:30:31 +0000 Subject: [PATCH 3/7] fix(eslint-factory): tighten Octokit alias detection and docs/tests Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- eslint-factory/README.md | 4 +-- ...-github-request-interpolated-route.test.ts | 18 ++++++++++++ .../no-github-request-interpolated-route.ts | 29 ++++++++++++++----- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/eslint-factory/README.md b/eslint-factory/README.md index 5ae6e43c067..3e18693e53c 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -25,8 +25,8 @@ Using an interpolated route bypasses Octokit's typed route dispatch, can silentl **Detected Octokit clients:** - Well-known names: `github`, `octokit`, `githubClient`, `octokitClient`. - `context.github` — the GitHub context object's client property. -- `getOctokit(...)` return values (bare call or module-member call). -- Simple `const`/`let` aliases of any of the above: +- Identifiers initialized from `getOctokit(...)` return values (bare call, `github.getOctokit(...)`, or `actions.getOctokit(...)`). +- Simple `const` aliases of any of the above: `const gh = github`, `const client = getOctokit(token)`, `const myClient = context.github`. **Flagged forms:** 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 68a9e464bb4..5e3d2883b4a 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 @@ -176,6 +176,8 @@ describe("no-github-request-interpolated-route", () => { "const gh = someUnknownClient; gh.request(`GET /repos/${owner}/${repo}`);", // Alias of http module — must not be flagged "const client = http; client.request(`GET /repos/${owner}/${repo}`);", + // Unknown .getOctokit() owner must not be treated as toolkit Octokit source + "const helperClient = myHelper.getOctokit(token); helperClient.request(`GET /repos/${owner}/${repo}`);", ], invalid: [], }); @@ -255,10 +257,26 @@ describe("no-github-request-interpolated-route", () => { }, ], }, + { + code: "const client = actions.getOctokit(token); client.request(`POST /repos/${owner}/${repo}/issues`, { });", + errors: [ + { + messageId: "interpolatedRoute", + data: { kind: "template literal with interpolations", client: "client" }, + }, + ], + }, ], }); }); + it("valid: mutable aliases are out of scope and not flagged", () => { + cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { + valid: ["let gh = github; gh.request(`GET /repos/${owner}/${repo}`, { owner, repo });", "var client = getOctokit(token); client = http; client.request(`GET /repos/${owner}/${repo}`);"], + invalid: [], + }); + }); + it("invalid: const alias of context.github is flagged", () => { cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { valid: [], 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 2d0f8eb9bd5..8a79cc132c0 100644 --- a/eslint-factory/src/rules/no-github-request-interpolated-route.ts +++ b/eslint-factory/src/rules/no-github-request-interpolated-route.ts @@ -3,6 +3,7 @@ import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils" const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); const OCTOKIT_CLIENT_NAMES = new Set(["github", "octokit", "githubClient", "octokitClient"]); +const GET_OCTOKIT_MEMBER_OBJECT_NAMES = new Set(["github", "actions"]); /** * Returns true when the node is a template literal that contains at least one @@ -51,7 +52,8 @@ function isContextGithubExpression(node: TSESTree.Node): boolean { * Returns true when a syntactic expression node directly represents a known * Octokit client source without scope resolution. Recognizes: * - Direct known names: github, octokit, githubClient, octokitClient - * - `getOctokit(...)` call results (bare or accessed via a module member) + * - `getOctokit(...)` call results (bare or via known module objects, e.g. + * `github.getOctokit(...)` or `actions.getOctokit(...)`) * - `context.github` member expression */ function isOctokitSourceExpression(node: TSESTree.Node): boolean { @@ -60,7 +62,16 @@ function isOctokitSourceExpression(node: TSESTree.Node): boolean { if (node.type === AST_NODE_TYPES.CallExpression) { const callee = node.callee; if (callee.type === AST_NODE_TYPES.Identifier && callee.name === "getOctokit") return true; - if (callee.type === AST_NODE_TYPES.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "getOctokit") return true; + if ( + callee.type === AST_NODE_TYPES.MemberExpression && + !callee.computed && + callee.object.type === AST_NODE_TYPES.Identifier && + GET_OCTOKIT_MEMBER_OBJECT_NAMES.has(callee.object.name) && + callee.property.type === AST_NODE_TYPES.Identifier && + callee.property.name === "getOctokit" + ) { + return true; + } } if (isContextGithubExpression(node)) return true; @@ -74,9 +85,9 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ type: "problem", docs: { description: - "Disallow template literals with interpolations or string concatenation as the route argument of Octokit " + - ".request() calls. Octokit clients are detected by well-known names (github, octokit, githubClient, octokitClient), " + - "getOctokit(...) call results, context.github, and simple const/let aliases of any of these. " + + "Disallow template literals with interpolations or string concatenation as the route argument of Octokit.request() calls. " + + "Octokit clients are detected by well-known names (github, octokit, githubClient, octokitClient), " + + "identifiers initialized from getOctokit(...) call results, context.github, and simple const aliases of any of these. " + 'Use the typed placeholder form instead: "GET /repos/{owner}/{repo}" with a separate params object.', }, schema: [], @@ -110,6 +121,8 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ for (const def of variable.defs) { if (def.type !== "Variable") continue; const declarator = def.node as TSESTree.VariableDeclarator; + const declaration = declarator.parent; + if (declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") continue; if (declarator.init && isOctokitSourceExpression(declarator.init)) return true; } return false; @@ -128,9 +141,9 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ * simple alias of an Octokit source (scope-resolved) * - `context.github.request(...)` */ - function resolveOctokitClientName(calleeObject: TSESTree.Expression | TSESTree.Super, callNode: TSESTree.Node): string | null { + function resolveOctokitClientName(calleeObject: TSESTree.Expression | TSESTree.Super): string | null { if (calleeObject.type === AST_NODE_TYPES.Identifier) { - return isIdentifierBoundToOctokitClient(calleeObject.name, callNode) ? calleeObject.name : null; + return isIdentifierBoundToOctokitClient(calleeObject.name, calleeObject) ? calleeObject.name : null; } if (isContextGithubExpression(calleeObject)) { @@ -150,7 +163,7 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ if (callee.property.type !== AST_NODE_TYPES.Identifier) return; if (callee.property.name !== "request") return; - const clientName = resolveOctokitClientName(callee.object, node); + const clientName = resolveOctokitClientName(callee.object); if (!clientName) return; const firstArg = node.arguments[0]; From 1c20aa43dfaf3d003e1630a77b2d02b81482821f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:33:19 +0000 Subject: [PATCH 4/7] test(eslint-factory): refine mutable-alias coverage and defensive guard Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- .../src/rules/no-github-request-interpolated-route.test.ts | 7 ++++++- .../src/rules/no-github-request-interpolated-route.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) 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 5e3d2883b4a..a731d3684c3 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 @@ -272,7 +272,12 @@ describe("no-github-request-interpolated-route", () => { it("valid: mutable aliases are out of scope and not flagged", () => { cjsRuleTester.run("no-github-request-interpolated-route", noGithubRequestInterpolatedRouteRule, { - valid: ["let gh = github; gh.request(`GET /repos/${owner}/${repo}`, { owner, repo });", "var client = getOctokit(token); client = http; client.request(`GET /repos/${owner}/${repo}`);"], + valid: [ + // Mutable let bindings are not trusted aliases. + "let gh = github; gh.request(`GET /repos/${owner}/${repo}`, { owner, repo });", + // var alias can be reassigned, so it is intentionally out of scope. + "var client = getOctokit(token); client = http; client.request(`GET /repos/${owner}/${repo}`);", + ], 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 8a79cc132c0..086e43acea3 100644 --- a/eslint-factory/src/rules/no-github-request-interpolated-route.ts +++ b/eslint-factory/src/rules/no-github-request-interpolated-route.ts @@ -122,6 +122,7 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ if (def.type !== "Variable") continue; const declarator = def.node as TSESTree.VariableDeclarator; const declaration = declarator.parent; + if (!declaration) continue; if (declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") continue; if (declarator.init && isOctokitSourceExpression(declarator.init)) return true; } From a6b7736ecb3a879ee2ebfd5999074cc4734779a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:35:40 +0000 Subject: [PATCH 5/7] docs(eslint-factory): clarify known getOctokit member sources Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- eslint-factory/README.md | 2 +- .../src/rules/no-github-request-interpolated-route.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eslint-factory/README.md b/eslint-factory/README.md index 3e18693e53c..1b21f746f52 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -25,7 +25,7 @@ Using an interpolated route bypasses Octokit's typed route dispatch, can silentl **Detected Octokit clients:** - Well-known names: `github`, `octokit`, `githubClient`, `octokitClient`. - `context.github` — the GitHub context object's client property. -- Identifiers initialized from `getOctokit(...)` return values (bare call, `github.getOctokit(...)`, or `actions.getOctokit(...)`). +- Identifiers initialized from `getOctokit(...)` calls (bare call or scoped to known module object names: `github.getOctokit(...)` or `actions.getOctokit(...)`). - Simple `const` aliases of any of the above: `const gh = github`, `const client = getOctokit(token)`, `const myClient = context.github`. 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 086e43acea3..ceb51c258cb 100644 --- a/eslint-factory/src/rules/no-github-request-interpolated-route.ts +++ b/eslint-factory/src/rules/no-github-request-interpolated-route.ts @@ -122,8 +122,7 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ if (def.type !== "Variable") continue; const declarator = def.node as TSESTree.VariableDeclarator; const declaration = declarator.parent; - if (!declaration) continue; - if (declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") continue; + if (!declaration || declaration.type !== AST_NODE_TYPES.VariableDeclaration || declaration.kind !== "const") continue; if (declarator.init && isOctokitSourceExpression(declarator.init)) return true; } return false; From e7340a7fb62f3dbbcade4839be627f59e0666527 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:38:07 +0000 Subject: [PATCH 6/7] refactor(eslint-factory): finalize Octokit alias docs and scope checks Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- eslint-factory/README.md | 2 +- .../src/rules/no-github-request-interpolated-route.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eslint-factory/README.md b/eslint-factory/README.md index 1b21f746f52..38641d6c03c 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -25,7 +25,7 @@ Using an interpolated route bypasses Octokit's typed route dispatch, can silentl **Detected Octokit clients:** - Well-known names: `github`, `octokit`, `githubClient`, `octokitClient`. - `context.github` — the GitHub context object's client property. -- Identifiers initialized from `getOctokit(...)` calls (bare call or scoped to known module object names: `github.getOctokit(...)` or `actions.getOctokit(...)`). +- Identifiers initialized by calling `getOctokit(...)` directly or via known module objects (`github.getOctokit(...)`, `actions.getOctokit(...)`). - Simple `const` aliases of any of the above: `const gh = github`, `const client = getOctokit(token)`, `const myClient = context.github`. 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 ceb51c258cb..00241923df4 100644 --- a/eslint-factory/src/rules/no-github-request-interpolated-route.ts +++ b/eslint-factory/src/rules/no-github-request-interpolated-route.ts @@ -141,9 +141,9 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ * simple alias of an Octokit source (scope-resolved) * - `context.github.request(...)` */ - function resolveOctokitClientName(calleeObject: TSESTree.Expression | TSESTree.Super): string | null { + function resolveOctokitClientName(calleeObject: TSESTree.Expression | TSESTree.Super, callNode: TSESTree.Node): string | null { if (calleeObject.type === AST_NODE_TYPES.Identifier) { - return isIdentifierBoundToOctokitClient(calleeObject.name, calleeObject) ? calleeObject.name : null; + return isIdentifierBoundToOctokitClient(calleeObject.name, callNode) ? calleeObject.name : null; } if (isContextGithubExpression(calleeObject)) { @@ -163,7 +163,7 @@ export const noGithubRequestInterpolatedRouteRule = createRule({ if (callee.property.type !== AST_NODE_TYPES.Identifier) return; if (callee.property.name !== "request") return; - const clientName = resolveOctokitClientName(callee.object); + const clientName = resolveOctokitClientName(callee.object, node); if (!clientName) return; const firstArg = node.arguments[0]; From 87f3d14c0bc12415fd6699873ee889d054b1e656 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:40:22 +0000 Subject: [PATCH 7/7] test(eslint-factory): cover var alias out-of-scope behavior Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- eslint-factory/README.md | 2 +- .../src/rules/no-github-request-interpolated-route.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/eslint-factory/README.md b/eslint-factory/README.md index 38641d6c03c..64413851c51 100644 --- a/eslint-factory/README.md +++ b/eslint-factory/README.md @@ -25,7 +25,7 @@ Using an interpolated route bypasses Octokit's typed route dispatch, can silentl **Detected Octokit clients:** - Well-known names: `github`, `octokit`, `githubClient`, `octokitClient`. - `context.github` — the GitHub context object's client property. -- Identifiers initialized by calling `getOctokit(...)` directly or via known module objects (`github.getOctokit(...)`, `actions.getOctokit(...)`). +- Identifiers initialized by calling `getOctokit(...)` directly or via known module objects (`github.getOctokit(...)`, `actions.getOctokit(...)`). (Known module object names currently: `github`, `actions`.) - Simple `const` aliases of any of the above: `const gh = github`, `const client = getOctokit(token)`, `const myClient = context.github`. 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 a731d3684c3..52a5b35b59e 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 @@ -275,6 +275,8 @@ describe("no-github-request-interpolated-route", () => { valid: [ // Mutable let bindings are not trusted aliases. "let gh = github; gh.request(`GET /repos/${owner}/${repo}`, { owner, repo });", + // var aliases are always out of scope, even without reassignment. + "var legacyClient = github; legacyClient.request(`GET /repos/${owner}/${repo}`);", // var alias can be reassigned, so it is intentionally out of scope. "var client = getOctokit(token); client = http; client.request(`GET /repos/${owner}/${repo}`);", ],