Skip to content

eslint-factory: scope-aware Octokit client detection in no-github-request-interpolated-route#44706

Merged
pelikhan merged 8 commits into
mainfrom
copilot/eslint-factory-fix-client-detection
Jul 10, 2026
Merged

eslint-factory: scope-aware Octokit client detection in no-github-request-interpolated-route#44706
pelikhan merged 8 commits into
mainfrom
copilot/eslint-factory-fix-client-detection

Conversation

Copilot AI commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The no-github-request-interpolated-route rule matched .request() calls only on 4 hard-coded identifier names, silently missing off-list aliases (const fallbackClient = getOctokit(token)), const gh = github renames, and context.github.request(...) direct calls.

Changes

New helpers in no-github-request-interpolated-route.ts:

  • isContextGithubExpression(node) — shared predicate for context.github member expressions.
  • isOctokitSourceExpression(node) — syntactically recognizes known names (github, octokit, githubClient, octokitClient), any getOctokit(...) call, and context.github.
  • isIdentifierBoundToOctokitClient(name, scopeNode) — walks ESLint's scope chain to check if a variable is bound to an Octokit source via a simple const/let assignment (single level; does not broaden to all .request() callers).
  • resolveOctokitClientName(calleeObject, callNode) — dispatcher returning the display name for the error message, or null to skip.

The CallExpression visitor now uses resolveOctokitClientName instead of the hard Identifier-only guard, so these are now flagged:

// Alias of a known name
const gh = github;
gh.request(`GET /repos/${owner}/${repo}`, params);  // ← flagged

// getOctokit() result alias
const fallbackClient = getOctokit(token);
fallbackClient.request(`POST /repos/${owner}/${repo}/issues/${n}/comments`, body);  // ← flagged

// context.github direct and aliased
context.github.request(`GET /repos/${owner}/${repo}`, params);  // ← flagged
const myClient = context.github;
myClient.request(`GET /repos/${owner}/${repo}`, params);  // ← flagged

http.request(...) / https.request(...) and aliases bound to unrelated sources remain unflagged — the fix is scope-aware, not name-broadening.

Test & docs updates:

  • context.github.request(...) moved from valid → invalid test.
  • False-positive guards added for http.request(...), https.request(...), and non-Octokit aliases.
  • README updated to document expanded detection scope and remaining out-of-scope patterns.

Generated by 👨‍🍳 PR Sous Chef · 11.3 AIC · ⌖ 6.74 AIC · ⊞ 4.7K ·
Comment /souschef to run again

…ction

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix client detection in no-github-request-interpolated-route rule eslint-factory: scope-aware Octokit client detection in no-github-request-interpolated-route Jul 10, 2026
Copilot AI requested a review from pelikhan July 10, 2026 11:02
@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Triage

Field Value
Category feature
Risk 🟡 Medium
Score 52/100 (impact:22 + urgency:18 + quality:12)
Action batch_review (pr-batch:draft-chores)

Adds scope-aware Octokit client detection to eslint rule no-github-request-interpolated-route. 3 files, +217 lines. Review with similar medium-risk draft PRs.

Generated by 🔧 PR Triage Agent · 206.8 AIC · ⌖ 8.74 AIC · ⊞ 5.4K ·

@pelikhan pelikhan marked this pull request as ready for review July 10, 2026 14:38
Copilot AI review requested due to automatic review settings July 10, 2026 14:38
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #44706 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request improves the no-github-request-interpolated-route ESLint rule so it can detect Octokit .request() calls beyond a small hard-coded identifier allowlist, adding scope-aware alias resolution and explicit handling for context.github.

Changes:

  • Add helpers to recognize Octokit “source” expressions and resolve simple const/let aliases via ESLint scope analysis.
  • Expand rule matching to flag interpolated routes on context.github.request(...) and on aliased Octokit clients.
  • Update tests and README to reflect the expanded detection behavior and add false-positive guards.
Show a summary per file
File Description
eslint-factory/src/rules/no-github-request-interpolated-route.ts Adds scope-aware client resolution helpers and updates the CallExpression matcher to use them.
eslint-factory/src/rules/no-github-request-interpolated-route.test.ts Moves context.github.request(...) cases to invalid and adds new alias/false-positive-guard test coverage.
eslint-factory/README.md Updates documentation for expanded Octokit client detection and out-of-scope patterns.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment on lines 76 to 80
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.',
Comment thread eslint-factory/README.md
Comment on lines +25 to +30
**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`.
@github-actions github-actions Bot mentioned this pull request Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 5 test(s): 5 design, 0 implementation, 0 violation(s).

📊 Metrics (5 tests)
Metric Value
Analyzed 5 (Vitest + ESLint RuleTester)
✅ Design 5 (100%)
⚠️ Implementation 0 (0%)
Edge/error coverage 5 (100%)
Duplicate clusters 0
Inflation 1.29:1 ✅ (threshold: 2:1)
🚨 Violations 0

Test Breakdown

All 5 new tests directly enforce the rule's scope-aware client detection contract:

  1. "valid: simple aliases bound to non-Octokit sources" — Verifies the rule correctly distinguishes unknown aliases and http module clients from Octokit clients. Scope resolution is the core behavioral contract.

  2. "invalid: context.github.request() flagged" — Ensures context.github is recognized as an Octokit source and flagged when used with interpolations. Tests both template literals and string concatenation.

  3. "invalid: simple alias of known Octokit client" — Validates that const aliases of direct known client names (github, octokit, etc.) are properly tracked in scope and flagged.

  4. "invalid: const alias of getOctokit() result" — Tests that clients created via getOctokit(token) are recognized through scope resolution and flagged when used with interpolations.

  5. "invalid: const alias of context.github" — Confirms that aliases of context.github propagate the Octokit client binding through scope chain.

All tests use ESLint RuleTester's built-in assertion framework (valid/invalid arrays with error expectations), providing strong contract verification. Each test covers both interpolation types (template literals and concatenation).

Verdict

Passed. 0% implementation tests (threshold: 30%). Scope-aware client detection is thoroughly covered with high-value behavioral tests. Test inflation at 1.29:1 indicates well-balanced coverage.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

🧪 Test quality analysis by Test Quality Sentinel · 13.6 AIC · ⌖ 7.37 AIC · ⊞ 6.8K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 100/100. 0% implementation tests (threshold: 30%). All 5 new tests enforce scope-aware client detection as a behavioral contract with comprehensive edge-case coverage.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: scope-aware Octokit client detection

The PR correctly upgrades the linter from a hard-coded 4-name allowlist to a scope-aware resolver that handles aliases, getOctokit() results, and context.github. Implementation is clean and the test coverage is thorough.

One non-blocking suggestion: the MemberExpression callee branch in isOctokitSourceExpression (for someModule.getOctokit(token)) has no corresponding test case. All other new paths are covered. See inline comment.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · 24.1 AIC · ⌖ 4.37 AIC · ⊞ 4.8K

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"
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isOctokitSourceExpression function handles module-member getOctokit calls (e.g. actions.getOctokit(token)) via the MemberExpression callee branch here, but there is no test exercising that path. Consider adding an invalid test case such as:

const client = actions.getOctokit(token);
client.request(`GET /repos/${owner}/${repo}`);

to confirm the branch works as intended.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three correctness gaps need to be fixed before merge

The scope-aware detection approach is sound, but there are three issues that can produce incorrect linting results in real code.

📋 Issues summary

High — let reassignment not modeled (line 113): isIdentifierBoundToOctokitClient only inspects the VariableDeclarator init, not subsequent write-references. A let client = getOctokit(token); client = http; pattern still gets flagged (false positive). Restrict alias detection to const-only bindings to eliminate this class of bugs entirely.

High — anyObject.getOctokit() is treated as an Octokit source (line 63): The member-call path in isOctokitSourceExpression accepts any receiver. Code like db.getOctokit(token) will mark the result as an Octokit client. Either drop the member-call path or add a receiver constraint.

Medium — sourceCode.getScope() called with CallExpression, not the Identifier (line 106): Scope resolution is most precise when called with the reference node itself. Pass calleeObject (the Identifier) rather than callNode (the CallExpression) for reliable scope chain walking.

🔎 Code quality review by PR Code Quality Reviewer · 41.4 AIC · ⌖ 4.87 AIC · ⊞ 5.4K
Comment /review to run again

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

** reassignment makes the Octokit check stale**: only the declaration initializer is inspected, not subsequent write-references, so a reassigned binding produces a false positive (or false negative in the reverse direction).

💡 Details and suggested fix

The current logic only checks variable.defs (VariableDeclarator inits):

let client = getOctokit(token);
client = httpModule;                   // ← reassigned to non-Octokit
client.request(`GET /repos/${o}/${r}`); // ← still flagged (false positive)

And the inverse:

let client = http;
client = getOctokit(token);            // ← write reference, not a def
client.request(`GET /repos/${o}/${r}`); // ← NOT flagged (false negative)

To handle mutable bindings correctly, also check write-references via variable.references.filter(r => r.isWrite()). Since full dataflow is expensive in ESLint, the safer option is to restrict detection to const-declared bindings only (check def.node.parent.kind === 'const'), which eliminates mutable aliasing entirely.

if (declarator.init && isOctokitSourceExpression(declarator.init)) {
  // Only trust const bindings — let/var can be reassigned
  const declKind = (def.node as TSESTree.VariableDeclarator).parent?.kind;
  if (declKind === 'const') return true;
}

This keeps the rule's stated contract ("simple const/let aliases") while avoiding the false-positive/negative on reassigned let bindings.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any method named getOctokit on any object is accepted as an Octokit source — will produce false positives when unrelated APIs use the same method name.

💡 Details and suggested fix

The check callee.property.name === 'getOctokit' matches ANY member call:

const client = db.getOctokit();           // ← false positive
const c2 = myHelper.getOctokit(token);    // ← false positive

Only the bare getOctokit(...) identifier call (line 62) is reliably tied to @actions/github. The member-call form has no guard on the receiver object and broadens detection beyond what was intended.

Fix: remove the member-call path unless the receiver is also constrained, or document clearly that all .getOctokit() calls are in-scope:

// Safe: only bare getOctokit() calls
if (callee.type === AST_NODE_TYPES.Identifier && callee.name === 'getOctokit') return true;
// Remove the MemberExpression path below unless receiver is verified

At minimum, add a test case for myHelper.getOctokit(token) that asserts it is NOT flagged.

function isIdentifierBoundToOctokitClient(name: string, scopeNode: TSESTree.Node): boolean {
if (OCTOKIT_CLIENT_NAMES.has(name)) return true;

let scope: SourceCodeScope | null = sourceCode.getScope(scopeNode);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sourceCode.getScope() is called with the CallExpression node, not the Identifier — scope resolution may be imprecise in some ESLint versions.

💡 Details

sourceCode.getScope(callNode) returns the scope at the call site (the CallExpression). The identifier being resolved (calleeObject.name) is a reference at or inside the call expression, so this will usually work, but ESLint's getScope semantics are node-type sensitive — for some node types the returned scope is the parent scope, not the node's own scope.

The safer and more precise call is to pass the Identifier node itself:

function isIdentifierBoundToOctokitClient(
  name: string,
  scopeNode: TSESTree.Node,  // pass the Identifier node, not the CallExpression
): boolean {

And in the caller:

// In resolveOctokitClientName:
return isIdentifierBoundToOctokitClient(calleeObject.name, calleeObject) ? calleeObject.name : null;
//                                                                          ^^^^^^^^^^^^^^^^^^^
//                                                                          Identifier node, not callNode

This is a low-risk but subtle correctness gap that could surface in non-trivial scope nesting.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs, /tdd, and /codebase-design — requesting changes on two correctness/coverage gaps.

📋 Key Themes & Highlights

Key Themes

  • Untested code path: the module.getOctokit() member-expression branch (line 63) has no test coverage.
  • var reassignment false positive: isIdentifierBoundToOctokitClient iterates defs but does not guard against var declarations with a later non-Octokit reassignment — contrary to the PR description promise of const/let only.
  • Long lines: lines 46 and 63 are noticeably wider than the rest of the file.

Positive Highlights

  • ✅ Excellent test expansion — 115 lines covering new patterns and false-positive guards.
  • ✅ Scope-chain walk is clean and conservative (single level only).
  • context.github moved correctly from valid to invalid.
  • ✅ README clearly documents expanded and still-out-of-scope forms.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · 42.1 AIC · ⌖ 4.75 AIC · ⊞ 6.6K
Comment /matt to run again

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The member-expression getOctokit branch matches any object's getOctokit method (e.g. myLib.getOctokit()), not only well-known toolkit shapes. There is no test for this code path.

💡 Suggested test + narrowing

Add a test to confirm the member-expression path works and consider narrowing to known caller objects:

it('invalid: module.getOctokit() alias is flagged', () => {
  cjsRuleTester.run('...', rule, {
    valid: [],
    invalid: [{
      code: 'const c = core.getOctokit(token); c.request(`GET /repos/${owner}/${repo}`);',
      errors: [{ messageId: 'interpolatedRoute', data: { kind: 'template literal with interpolations', client: 'c' } }],
    }],
  });
});

@copilot please address this.

}
scope = scope.upper;
}
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] When the variable is found in scope but has no Variable defs (e.g. a function parameter or a destructured import), the loop hits return false at line 115, which is correct. However, if a variable has multiple defs (e.g. a var that is declared then reassigned), only the first Variable def that passes isOctokitSourceExpression wins — later reassignments to a non-Octokit value are silently ignored, potentially causing false positives.

💡 Suggested test to surface the issue
it('valid: var re-assigned to non-Octokit value is not flagged', () => {
  cjsRuleTester.run('...', rule, {
    valid: [
      // gh was initially octokit but is then overwritten
      'var gh = github; gh = someOtherThing; gh.request(`GET /repos/${owner}/${repo}`);',
    ],
    invalid: [],
  });
});

The PR description scopes this to const/let single-level assignments, so adding a guard if (def.parent?.kind !== 'const' && def.parent?.kind !== 'let') continue; would close this gap.

@copilot please address this.

*/
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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] Line 46 is very long (170+ characters). The existing helpers in this file each fit comfortably on one readable line. Breaking this into multiple lines would match the surrounding style and improve readability in code review and diffs.

💡 Suggested formatting
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'
  );
}

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please run the pr-finisher skill, address the unresolved review feedback below, and rerun checks once the branch is up to date.

Unresolved review feedback (newest first):...

Addressed in 5336e5a. I fixed the unresolved rule/docs/test feedback in scope, ran targeted rule tests (npm test -- no-github-request-interpolated-route), and re-ran local make fmt, make lint, make test-unit, make test, and make recompile. make test-unit/make test still show the pre-existing TestWasmGolden_AllEngines failure in pkg/workflow (golden drift from current branch state), unrelated to this ESLint-rule change. CI on this new HEAD is stale until a maintainer re-triggers workflows.

Copilot AI and others added 4 commits July 10, 2026 15:33
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>
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>
Copilot AI requested a review from gh-aw-bot July 10, 2026 15:41
@pelikhan pelikhan merged commit 48acce4 into main Jul 10, 2026
6 checks passed
@pelikhan pelikhan deleted the copilot/eslint-factory-fix-client-detection branch July 10, 2026 16:02
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please run the pr-finisher skill, address unresolved review comments, and rerun checks once the branch is up to date.

Generated by 👨‍🍳 PR Sous Chef · 10.4 AIC · ⌖ 4.23 AIC · ⊞ 7.1K ·
Comment /souschef to run again

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.82.8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants