From d508d5476dce7b60c3786f412a9ab6d574f03825 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:56:38 +0000 Subject: [PATCH 1/2] Initial plan From 6a3c491c86c01e871270edeaa9d5f621c783e3bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:04:51 +0000 Subject: [PATCH 2/2] feat(eslint-factory): extract nonStringKind to shared module; align setOutput rule with CORE_ALIASES - Create `non-string-kind.ts` with shared `nonStringKind()`, kind constants, and `NonStringKind` type - Update `no-core-setoutput-non-string` to use `CORE_ALIASES` (adds `coreObj` coverage) and import from shared module - Update `no-core-exportvariable-non-string` to import from shared module (removes duplicate) - Add `coreObj.setOutput` valid and invalid test cases Closes #45929 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../no-core-exportvariable-non-string.ts | 43 +------------ .../no-core-setoutput-non-string.test.ts | 62 +++++++++++++++++++ .../src/rules/no-core-setoutput-non-string.ts | 48 ++------------ eslint-factory/src/rules/non-string-kind.ts | 41 ++++++++++++ 4 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 eslint-factory/src/rules/non-string-kind.ts diff --git a/eslint-factory/src/rules/no-core-exportvariable-non-string.ts b/eslint-factory/src/rules/no-core-exportvariable-non-string.ts index cca397d64fd..620c56f527a 100644 --- a/eslint-factory/src/rules/no-core-exportvariable-non-string.ts +++ b/eslint-factory/src/rules/no-core-exportvariable-non-string.ts @@ -1,48 +1,9 @@ -import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils"; import { CORE_ALIASES } from "./core-aliases"; +import { nonStringKind, NULL_KIND, UNDEFINED_KIND } from "./non-string-kind"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); -/** - * Returns a description of the non-string value kind if the node is one of the - * low-false-positive forms targeted by this rule, or null if the value may already - * be a string or cannot be determined without type information. - * - * Targeted forms (low false-positive risk): - * - Numeric literal: 0, 42, 3.14 - * - Boolean literal: true, false - * - Null literal - * - Identifier `undefined` - * - .length member access: commonly numeric in practice - */ -const NUMERIC_LITERAL_KIND = "numeric literal" as const; -const BOOLEAN_LITERAL_KIND = "boolean literal" as const; -const NULL_KIND = "null" as const; -const UNDEFINED_KIND = "undefined" as const; -const LENGTH_KIND = ".length (number)" as const; - -type NonStringKind = typeof NUMERIC_LITERAL_KIND | typeof BOOLEAN_LITERAL_KIND | typeof NULL_KIND | typeof UNDEFINED_KIND | typeof LENGTH_KIND; - -function nonStringKind(node: TSESTree.Node): NonStringKind | null { - if (node.type === AST_NODE_TYPES.Literal) { - if (typeof node.value === "number") return NUMERIC_LITERAL_KIND; - if (typeof node.value === "boolean") return BOOLEAN_LITERAL_KIND; - if (node.value === null) return NULL_KIND; - } - - if (node.type === AST_NODE_TYPES.Identifier && node.name === "undefined") { - return UNDEFINED_KIND; - } - - // expr.length — commonly numeric; computed access (expr["length"]) is intentionally - // excluded because it is far less common and raises the FP risk slightly. - if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "length") { - return LENGTH_KIND; - } - - return null; -} - export const noCoreExportVariableNonStringRule = createRule({ name: "no-core-exportvariable-non-string", meta: { diff --git a/eslint-factory/src/rules/no-core-setoutput-non-string.test.ts b/eslint-factory/src/rules/no-core-setoutput-non-string.test.ts index 8d39562ca5d..1b9d3f8d5e7 100644 --- a/eslint-factory/src/rules/no-core-setoutput-non-string.test.ts +++ b/eslint-factory/src/rules/no-core-setoutput-non-string.test.ts @@ -38,6 +38,13 @@ describe("no-core-setoutput-non-string", () => { }); }); + it("valid: coreObj alias with string value is accepted", () => { + cjsRuleTester.run("no-core-setoutput-non-string", noCoreSetOutputNonStringRule, { + valid: [`coreObj.setOutput("aic", roundedAIC);`, `coreObj.setOutput("result", "hello");`, `coreObj.setOutput("count", String(items.length));`], + invalid: [], + }); + }); + it("valid: computed string-literal setOutput with string value is accepted", () => { cjsRuleTester.run("no-core-setoutput-non-string", noCoreSetOutputNonStringRule, { valid: [`core["setOutput"]("count", "42");`], @@ -185,4 +192,59 @@ describe("no-core-setoutput-non-string", () => { ], }); }); + + it("invalid: coreObj alias with numeric value is flagged", () => { + cjsRuleTester.run("no-core-setoutput-non-string", noCoreSetOutputNonStringRule, { + valid: [], + invalid: [ + { + code: `coreObj.setOutput("aic", 0);`, + errors: [ + { + messageId: "nonStringValue", + data: { kind: "numeric literal", valueText: "0" }, + suggestions: [{ messageId: "wrapWithString", data: { valueText: "0" }, output: `coreObj.setOutput("aic", String(0));` }], + }, + ], + }, + ], + }); + }); + + it("invalid: coreObj alias with boolean value is flagged", () => { + cjsRuleTester.run("no-core-setoutput-non-string", noCoreSetOutputNonStringRule, { + valid: [], + invalid: [ + { + code: `coreObj.setOutput("success", true);`, + errors: [ + { + messageId: "nonStringValue", + suggestions: [{ messageId: "wrapWithString", output: `coreObj.setOutput("success", String(true));` }], + }, + ], + }, + ], + }); + }); + + it("invalid: coreObj alias with null value is flagged", () => { + cjsRuleTester.run("no-core-setoutput-non-string", noCoreSetOutputNonStringRule, { + valid: [], + invalid: [ + { + code: `coreObj.setOutput("result", null);`, + errors: [ + { + messageId: "nonStringValue", + suggestions: [ + { messageId: "useEmptyString", output: `coreObj.setOutput("result", "");` }, + { messageId: "wrapWithString", output: `coreObj.setOutput("result", String(null));` }, + ], + }, + ], + }, + ], + }); + }); }); diff --git a/eslint-factory/src/rules/no-core-setoutput-non-string.ts b/eslint-factory/src/rules/no-core-setoutput-non-string.ts index 6ef603a1838..1bb0d03bf78 100644 --- a/eslint-factory/src/rules/no-core-setoutput-non-string.ts +++ b/eslint-factory/src/rules/no-core-setoutput-non-string.ts @@ -1,47 +1,9 @@ -import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; +import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils"; +import { CORE_ALIASES } from "./core-aliases"; +import { nonStringKind, NULL_KIND, UNDEFINED_KIND } from "./non-string-kind"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); -/** - * Returns a description of the non-string value kind if the node is one of the - * low-false-positive forms targeted by this rule, or null if the value may already - * be a string or cannot be determined without type information. - * - * Targeted forms (low false-positive risk): - * - Numeric literal: 0, 42, 3.14 - * - Boolean literal: true, false - * - Null literal - * - Identifier `undefined` - * - .length member access: commonly numeric in practice - */ -const NUMERIC_LITERAL_KIND = "numeric literal" as const; -const BOOLEAN_LITERAL_KIND = "boolean literal" as const; -const NULL_KIND = "null" as const; -const UNDEFINED_KIND = "undefined" as const; -const LENGTH_KIND = ".length (number)" as const; - -type NonStringKind = typeof NUMERIC_LITERAL_KIND | typeof BOOLEAN_LITERAL_KIND | typeof NULL_KIND | typeof UNDEFINED_KIND | typeof LENGTH_KIND; - -function nonStringKind(node: TSESTree.Node): NonStringKind | null { - if (node.type === AST_NODE_TYPES.Literal) { - if (typeof node.value === "number") return NUMERIC_LITERAL_KIND; - if (typeof node.value === "boolean") return BOOLEAN_LITERAL_KIND; - if (node.value === null) return NULL_KIND; - } - - if (node.type === AST_NODE_TYPES.Identifier && node.name === "undefined") { - return UNDEFINED_KIND; - } - - // expr.length — commonly numeric; computed access (expr["length"]) is intentionally - // excluded because it is far less common and raises the FP risk slightly. - if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "length") { - return LENGTH_KIND; - } - - return null; -} - export const noCoreSetOutputNonStringRule = createRule({ name: "no-core-setoutput-non-string", meta: { @@ -70,8 +32,8 @@ export const noCoreSetOutputNonStringRule = createRule({ // Must be a member expression: something.setOutput(...) if (callee.type !== AST_NODE_TYPES.MemberExpression) return; - // Object must be `core` (the @actions/core import convention in actions/setup/js) - if (callee.object.type !== AST_NODE_TYPES.Identifier || callee.object.name !== "core") return; + // Object must be a known @actions/core alias (`core` or `coreObj`) + if (callee.object.type !== AST_NODE_TYPES.Identifier || !CORE_ALIASES.has(callee.object.name)) return; // Property must be `setOutput` (direct or computed string-literal access) const prop = callee.property; diff --git a/eslint-factory/src/rules/non-string-kind.ts b/eslint-factory/src/rules/non-string-kind.ts new file mode 100644 index 00000000000..36571638ab7 --- /dev/null +++ b/eslint-factory/src/rules/non-string-kind.ts @@ -0,0 +1,41 @@ +import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils"; + +/** + * Returns a description of the non-string value kind if the node is one of the + * low-false-positive forms targeted by these rules, or null if the value may already + * be a string or cannot be determined without type information. + * + * Targeted forms (low false-positive risk): + * - Numeric literal: 0, 42, 3.14 + * - Boolean literal: true, false + * - Null literal + * - Identifier `undefined` + * - .length member access: commonly numeric in practice + */ +export const NUMERIC_LITERAL_KIND = "numeric literal" as const; +export const BOOLEAN_LITERAL_KIND = "boolean literal" as const; +export const NULL_KIND = "null" as const; +export const UNDEFINED_KIND = "undefined" as const; +export const LENGTH_KIND = ".length (number)" as const; + +export type NonStringKind = typeof NUMERIC_LITERAL_KIND | typeof BOOLEAN_LITERAL_KIND | typeof NULL_KIND | typeof UNDEFINED_KIND | typeof LENGTH_KIND; + +export function nonStringKind(node: TSESTree.Node): NonStringKind | null { + if (node.type === AST_NODE_TYPES.Literal) { + if (typeof node.value === "number") return NUMERIC_LITERAL_KIND; + if (typeof node.value === "boolean") return BOOLEAN_LITERAL_KIND; + if (node.value === null) return NULL_KIND; + } + + if (node.type === AST_NODE_TYPES.Identifier && node.name === "undefined") { + return UNDEFINED_KIND; + } + + // expr.length — commonly numeric; computed access (expr["length"]) is intentionally + // excluded because it is far less common and raises the FP risk slightly. + if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "length") { + return LENGTH_KIND; + } + + return null; +}