Skip to content

eslint-factory: no-core-setoutput/exportvariable-non-string miss aliased and destructured core (inconsistent with require-retu [Content truncated due to length] #46356

Description

@github-actions

Summary

The @actions/core rule family detects the core object inconsistently:

  • no-core-setoutput-non-string and no-core-exportvariable-non-string only match when the callee object is a literal identifier whose name is in CORE_ALIASES (core / coreObj):

    // no-core-setoutput-non-string.ts:36 and no-core-exportvariable-non-string.ts:36
    if (callee.object.type !== AST_NODE_TYPES.Identifier || !CORE_ALIASES.has(callee.object.name)) return;

    There is no scope resolution — a single-assignment alias (const c = core) or a destructured binding (const { setOutput } = core) is invisible to these rules.

  • require-return-after-core-setfailed (the newest member of the family) does scope-resolve both forms — see isCoreAliasIdentifier (handles const c = core; c.setFailed(...)) and isDestructuredSetFailedIdentifier (handles const { setFailed } = core and const { setFailed: sf } = core), with let-reassignment and local-shadow rejection.

Because of this split, the following escape the non-string rules entirely (false negatives):

const c = core;
c.setOutput("count", items.length);      // .length (number) — NOT flagged

const { exportVariable } = core;
exportVariable("READY", true);            // boolean literal — NOT flagged

The equivalent core.setOutput("count", items.length) / core.exportVariable("READY", true) is flagged. Same footgun (implicit coercion to 'true' / a numeric string in a downstream Actions expression), different detection outcome purely based on how core was bound.

Why it matters

require-return-after-core-setfailed establishes the intended detection contract for this family: resolve single-assignment const aliases and destructured bindings, reject let-reassignment and local shadows. The two older non-string rules predate that contract and only handle the literal-name case, so the family is internally inconsistent and the non-string rules are weaker than a reader would expect given the sibling rule.

Proposed refinement

  • Extract the alias/destructuring resolution already implemented in require-return-after-core-setfailed (isCoreAliasIdentifier + isDestructuredSetFailedIdentifier, generalized over the method name) into a shared helper alongside core-aliases.ts (e.g. resolveCoreMethodCall(sourceCode, callNode, methodName)).
  • Use it in no-core-setoutput-non-string and no-core-exportvariable-non-string so const c = core; c.setOutput(...) and const { setOutput } = core; setOutput(...) (and the { setOutput: alias } rename form) are covered.
  • Preserve the existing safeguards: single-assignment const only; reject let reassignment (any non-init write) and locally-shadowed / non-core bindings (def.type !== "Variable" guard), matching the sibling rule's tests.

Acceptance criteria

  • const c = core; c.setOutput("n", 1) and const { setOutput } = core; setOutput("n", 1) are flagged, with the same messages/suggestions as the direct core.setOutput("n", 1) form.
  • Same for no-core-exportvariable-non-string (const c = core; c.exportVariable(...), destructured, and { exportVariable: alias }).
  • let c = core; c = other; c.setOutput("n", 1) is NOT flagged; a locally-declared function setOutput(){} or non-core alias (const c = other) is NOT flagged.
  • Shared resolver is unit-tested and reused by require-return-after-core-setfailed (no behavior change there) to prevent future drift.

Grounding / verification

  • Detection code confirmed at no-core-setoutput-non-string.ts:36 and no-core-exportvariable-non-string.ts:36 (literal-name only, no scope walk).
  • Sibling resolution confirmed in require-return-after-core-setfailed.ts (isCoreAliasIdentifier, isDestructuredSetFailedIdentifier) with tests in require-return-after-core-setfailed.test.ts (aliased/destructured valid+invalid cases).
  • Not currently grounded in the live corpus: a grep of non-test actions/setup/js/**/*.cjs found no aliased/destructured core binding used with setOutput/exportVariable (all call sites use literal core./coreObj.). This is a latent false-negative + consistency gap, not an active miss — it activates the moment such a binding is introduced. Prioritize accordingly (MEDIUM).

Generated by 🤖 ESLint Refiner · 626.4 AIC · ⌖ 13.1 AIC · ⊞ 4.6K ·

  • expires on Jul 24, 2026, 10:13 PM UTC-08:00

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions