diff --git a/eslint-factory/src/rules/prefer-core-logging.test.ts b/eslint-factory/src/rules/prefer-core-logging.test.ts index e5e2b0e7f61..97390f99a43 100644 --- a/eslint-factory/src/rules/prefer-core-logging.test.ts +++ b/eslint-factory/src/rules/prefer-core-logging.test.ts @@ -38,6 +38,16 @@ describe("prefer-core-logging", () => { }, ], }, + { + code: "console.log(`hello`);", + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: "`hello`" }, output: "core.info(`hello`);" }], + }, + ], + }, ], }); }); @@ -162,7 +172,45 @@ describe("prefer-core-logging", () => { { messageId: "preferCoreLogging", data: { method: "log", replacement: "core.info" }, - suggestions: [{ messageId: "replaceWithCoreMethod", data: { replacement: "core.info", args: `"value:", someVar` }, output: `const core = require("@actions/core"); const someVar = 1; core.info("value:", someVar);` }], + suggestions: [], + }, + ], + }, + ], + }); + }); + + it("invalid: console.log with format specifier is report-only", () => { + ruleTester.run("prefer-core-logging", preferCoreLoggingRule, { + valid: [], + invalid: [ + { + code: `const core = require("@actions/core"); console.log("%s processed");`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [], + }, + ], + }, + { + code: `const core = require("@actions/core"); const someVar = 1; console.log("value: %s", someVar);`, + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [], + }, + ], + }, + { + code: 'const core = require("@actions/core"); const someVar = 1; console.log(`value: ${someVar}`);', + errors: [ + { + messageId: "preferCoreLogging", + data: { method: "log", replacement: "core.info" }, + suggestions: [], }, ], }, diff --git a/eslint-factory/src/rules/prefer-core-logging.ts b/eslint-factory/src/rules/prefer-core-logging.ts index a1b36612aec..ac372a83e9d 100644 --- a/eslint-factory/src/rules/prefer-core-logging.ts +++ b/eslint-factory/src/rules/prefer-core-logging.ts @@ -1,4 +1,4 @@ -import { AST_NODE_TYPES, ESLintUtils, TSESTree } from "@typescript-eslint/utils"; +import { AST_NODE_TYPES, ESLintUtils, TSESLint, TSESTree } from "@typescript-eslint/utils"; const createRule = ESLintUtils.RuleCreator(name => `https://github.com/github/gh-aw/tree/main/eslint-factory#${name}`); @@ -30,6 +30,58 @@ function getConsoleMethod(node: TSESTree.CallExpression): string | null { return prop.name in CONSOLE_TO_CORE ? prop.name : null; } +function getStaticStringValue(node: TSESTree.CallExpressionArgument): string | null { + if (node.type === AST_NODE_TYPES.Literal) { + return typeof node.value === "string" ? node.value : null; + } + + if (node.type !== AST_NODE_TYPES.TemplateLiteral) { + return null; + } + + if (node.expressions.length > 0) { + return null; + } + + const parts: string[] = []; + for (const quasi of node.quasis) { + if (quasi.value.cooked === null) { + return null; + } + parts.push(quasi.value.cooked); + } + + return parts.join(""); +} + +function hasConsoleFormatSpecifier(node: TSESTree.CallExpressionArgument | undefined): boolean { + const value = node ? getStaticStringValue(node) : null; + if (value === null) { + return false; + } + + for (let i = 0; i < value.length - 1; i++) { + if (value[i] === "%" && value[i - 1] !== "%" && "sdifojO".includes(value[i + 1] ?? "")) { + return true; + } + } + + return false; +} + +function isInterpolatedTemplateLiteral(node: TSESTree.CallExpressionArgument): boolean { + return node.type === AST_NODE_TYPES.TemplateLiteral && node.expressions.length > 0; +} + +function canSuggestCoreReplacement(node: TSESTree.CallExpression): boolean { + const arg = node.arguments[0]; + if (node.arguments.length !== 1 || !arg) { + return false; + } + + return !isInterpolatedTemplateLiteral(arg) && !hasConsoleFormatSpecifier(arg); +} + export const preferCoreLoggingRule = createRule({ name: "prefer-core-logging", meta: { @@ -57,23 +109,23 @@ export const preferCoreLoggingRule = createRule({ if (!method) return; const replacement = CONSOLE_TO_CORE[method]!; - - // Build replacement argument text from original call - const argsText = node.arguments.map(arg => sourceCode.getText(arg)).join(", "); + const suggest = canSuggestCoreReplacement(node) + ? [ + { + messageId: "replaceWithCoreMethod" as const, + data: { replacement, args: sourceCode.getText(node.arguments[0]) }, + fix(fixer: TSESLint.RuleFixer) { + return fixer.replaceText(node, `${replacement}(${sourceCode.getText(node.arguments[0])})`); + }, + }, + ] + : undefined; context.report({ node, messageId: "preferCoreLogging", data: { method, replacement }, - suggest: [ - { - messageId: "replaceWithCoreMethod", - data: { replacement, args: argsText }, - fix(fixer) { - return fixer.replaceText(node, `${replacement}(${argsText})`); - }, - }, - ], + suggest, }); }, };