mirror of
https://github.com/github/codeql-action.git
synced 2025-12-28 02:00:12 +08:00
347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
// @ts-self-types="./index.d.ts"
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
/**
|
|
* @filedescription Functions to fix up rules to provide missing methods on the `context` object.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Types
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/** @typedef {import("eslint").ESLint.Plugin} FixupPluginDefinition */
|
|
/** @typedef {import("eslint").Rule.RuleModule} FixupRuleDefinition */
|
|
/** @typedef {import("eslint").Rule.OldStyleRule} FixupLegacyRuleDefinition */
|
|
/** @typedef {import("eslint").Linter.FlatConfig} FixupConfig */
|
|
/** @typedef {Array<FixupConfig>} FixupConfigArray */
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Data
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* The removed methods from the `context` object that need to be added back.
|
|
* The keys are the name of the method on the `context` object and the values
|
|
* are the name of the method on the `sourceCode` object.
|
|
* @type {Map<string, string>}
|
|
*/
|
|
const removedMethodNames = new Map([
|
|
["getSource", "getText"],
|
|
["getSourceLines", "getLines"],
|
|
["getAllComments", "getAllComments"],
|
|
["getDeclaredVariables", "getDeclaredVariables"],
|
|
["getNodeByRangeIndex", "getNodeByRangeIndex"],
|
|
["getCommentsBefore", "getCommentsBefore"],
|
|
["getCommentsAfter", "getCommentsAfter"],
|
|
["getCommentsInside", "getCommentsInside"],
|
|
["getJSDocComment", "getJSDocComment"],
|
|
["getFirstToken", "getFirstToken"],
|
|
["getFirstTokens", "getFirstTokens"],
|
|
["getLastToken", "getLastToken"],
|
|
["getLastTokens", "getLastTokens"],
|
|
["getTokenAfter", "getTokenAfter"],
|
|
["getTokenBefore", "getTokenBefore"],
|
|
["getTokenByRangeStart", "getTokenByRangeStart"],
|
|
["getTokens", "getTokens"],
|
|
["getTokensAfter", "getTokensAfter"],
|
|
["getTokensBefore", "getTokensBefore"],
|
|
["getTokensBetween", "getTokensBetween"],
|
|
]);
|
|
|
|
/**
|
|
* Tracks the original rule definition and the fixed-up rule definition.
|
|
* @type {WeakMap<FixupRuleDefinition|FixupLegacyRuleDefinition,FixupRuleDefinition>}
|
|
*/
|
|
const fixedUpRuleReplacements = new WeakMap();
|
|
|
|
/**
|
|
* Tracks all of the fixed up rule definitions so we don't duplicate effort.
|
|
* @type {WeakSet<FixupRuleDefinition>}
|
|
*/
|
|
const fixedUpRules = new WeakSet();
|
|
|
|
/**
|
|
* Tracks the original plugin definition and the fixed-up plugin definition.
|
|
* @type {WeakMap<FixupPluginDefinition,FixupPluginDefinition>}
|
|
*/
|
|
const fixedUpPluginReplacements = new WeakMap();
|
|
|
|
/**
|
|
* Tracks all of the fixed up plugin definitions so we don't duplicate effort.
|
|
* @type {WeakSet<FixupPluginDefinition>}
|
|
*/
|
|
const fixedUpPlugins = new WeakSet();
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Takes the given rule and creates a new rule with the `create()` method wrapped
|
|
* to provide the missing methods on the `context` object.
|
|
* @param {FixupRuleDefinition|FixupLegacyRuleDefinition} ruleDefinition The rule to fix up.
|
|
* @returns {FixupRuleDefinition} The fixed-up rule.
|
|
*/
|
|
function fixupRule(ruleDefinition) {
|
|
// first check if we've already fixed up this rule
|
|
if (fixedUpRuleReplacements.has(ruleDefinition)) {
|
|
return fixedUpRuleReplacements.get(ruleDefinition);
|
|
}
|
|
|
|
const isLegacyRule = typeof ruleDefinition === "function";
|
|
|
|
// check to see if this rule definition has already been fixed up
|
|
if (!isLegacyRule && fixedUpRules.has(ruleDefinition)) {
|
|
return ruleDefinition;
|
|
}
|
|
|
|
const originalCreate = isLegacyRule
|
|
? ruleDefinition
|
|
: ruleDefinition.create.bind(ruleDefinition);
|
|
|
|
function ruleCreate(context) {
|
|
// if getScope is already there then no need to create old methods
|
|
if ("getScope" in context) {
|
|
return originalCreate(context);
|
|
}
|
|
|
|
const sourceCode = context.sourceCode;
|
|
let currentNode = sourceCode.ast;
|
|
|
|
const newContext = Object.assign(Object.create(context), {
|
|
parserServices: sourceCode.parserServices,
|
|
|
|
/*
|
|
* The following methods rely on the current node in the traversal,
|
|
* so we need to add them manually.
|
|
*/
|
|
getScope() {
|
|
return sourceCode.getScope(currentNode);
|
|
},
|
|
|
|
getAncestors() {
|
|
return sourceCode.getAncestors(currentNode);
|
|
},
|
|
|
|
markVariableAsUsed(variable) {
|
|
sourceCode.markVariableAsUsed(variable, currentNode);
|
|
},
|
|
});
|
|
|
|
// add passthrough methods
|
|
for (const [
|
|
contextMethodName,
|
|
sourceCodeMethodName,
|
|
] of removedMethodNames) {
|
|
newContext[contextMethodName] =
|
|
sourceCode[sourceCodeMethodName].bind(sourceCode);
|
|
}
|
|
|
|
// freeze just like the original context
|
|
Object.freeze(newContext);
|
|
|
|
/*
|
|
* Create the visitor object using the original create() method.
|
|
* This is necessary to ensure that the visitor object is created
|
|
* with the correct context.
|
|
*/
|
|
const visitor = originalCreate(newContext);
|
|
|
|
/*
|
|
* Wrap each method in the visitor object to update the currentNode
|
|
* before calling the original method. This is necessary because the
|
|
* methods like `getScope()` need to know the current node.
|
|
*/
|
|
for (const [methodName, method] of Object.entries(visitor)) {
|
|
/*
|
|
* Node is the second argument to most code path methods,
|
|
* and the third argument for onCodePathSegmentLoop.
|
|
*/
|
|
if (methodName.startsWith("on")) {
|
|
// eslint-disable-next-line no-loop-func -- intentionally updating shared `currentNode` variable
|
|
visitor[methodName] = (...args) => {
|
|
currentNode =
|
|
args[methodName === "onCodePathSegmentLoop" ? 2 : 1];
|
|
|
|
return method.call(visitor, ...args);
|
|
};
|
|
|
|
continue;
|
|
}
|
|
|
|
// eslint-disable-next-line no-loop-func -- intentionally updating shared `currentNode` variable
|
|
visitor[methodName] = (...args) => {
|
|
currentNode = args[0];
|
|
|
|
return method.call(visitor, ...args);
|
|
};
|
|
}
|
|
|
|
return visitor;
|
|
}
|
|
|
|
const newRuleDefinition = {
|
|
...(isLegacyRule ? undefined : ruleDefinition),
|
|
create: ruleCreate,
|
|
};
|
|
|
|
// cache the fixed up rule
|
|
fixedUpRuleReplacements.set(ruleDefinition, newRuleDefinition);
|
|
fixedUpRules.add(newRuleDefinition);
|
|
|
|
return newRuleDefinition;
|
|
}
|
|
|
|
/**
|
|
* Takes the given plugin and creates a new plugin with all of the rules wrapped
|
|
* to provide the missing methods on the `context` object.
|
|
* @param {FixupPluginDefinition} plugin The plugin to fix up.
|
|
* @returns {FixupPluginDefinition} The fixed-up plugin.
|
|
*/
|
|
function fixupPluginRules(plugin) {
|
|
// first check if we've already fixed up this plugin
|
|
if (fixedUpPluginReplacements.has(plugin)) {
|
|
return fixedUpPluginReplacements.get(plugin);
|
|
}
|
|
|
|
/*
|
|
* If the plugin has already been fixed up, or if the plugin
|
|
* doesn't have any rules, we can just return it.
|
|
*/
|
|
if (fixedUpPlugins.has(plugin) || !plugin.rules) {
|
|
return plugin;
|
|
}
|
|
|
|
const newPlugin = {
|
|
...plugin,
|
|
rules: Object.fromEntries(
|
|
Object.entries(plugin.rules).map(([ruleId, ruleDefinition]) => [
|
|
ruleId,
|
|
fixupRule(ruleDefinition),
|
|
]),
|
|
),
|
|
};
|
|
|
|
// cache the fixed up plugin
|
|
fixedUpPluginReplacements.set(plugin, newPlugin);
|
|
fixedUpPlugins.add(newPlugin);
|
|
|
|
return newPlugin;
|
|
}
|
|
|
|
/**
|
|
* Takes the given configuration and creates a new configuration with all of the
|
|
* rules wrapped to provide the missing methods on the `context` object.
|
|
* @param {FixupConfigArray|FixupConfig} config The configuration to fix up.
|
|
* @returns {FixupConfigArray} The fixed-up configuration.
|
|
*/
|
|
function fixupConfigRules(config) {
|
|
const configs = Array.isArray(config) ? config : [config];
|
|
|
|
return configs.map(configItem => {
|
|
if (!configItem.plugins) {
|
|
return configItem;
|
|
}
|
|
|
|
const newPlugins = Object.fromEntries(
|
|
Object.entries(configItem.plugins).map(([pluginName, plugin]) => [
|
|
pluginName,
|
|
fixupPluginRules(plugin),
|
|
]),
|
|
);
|
|
|
|
return {
|
|
...configItem,
|
|
plugins: newPlugins,
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @fileoverview Ignore file utilities for the compat package.
|
|
* @author Nicholas C. Zakas
|
|
*/
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Types
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/** @typedef {import("eslint").Linter.FlatConfig} FlatConfig */
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Exports
|
|
//-----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Converts an ESLint ignore pattern to a minimatch pattern.
|
|
* @param {string} pattern The .eslintignore or .gitignore pattern to convert.
|
|
* @returns {string} The converted pattern.
|
|
*/
|
|
function convertIgnorePatternToMinimatch(pattern) {
|
|
const isNegated = pattern.startsWith("!");
|
|
const negatedPrefix = isNegated ? "!" : "";
|
|
const patternToTest = (isNegated ? pattern.slice(1) : pattern).trimEnd();
|
|
|
|
// special cases
|
|
if (["", "**", "/**", "**/"].includes(patternToTest)) {
|
|
return `${negatedPrefix}${patternToTest}`;
|
|
}
|
|
|
|
const firstIndexOfSlash = patternToTest.indexOf("/");
|
|
|
|
const matchEverywherePrefix =
|
|
firstIndexOfSlash < 0 || firstIndexOfSlash === patternToTest.length - 1
|
|
? "**/"
|
|
: "";
|
|
|
|
const patternWithoutLeadingSlash =
|
|
firstIndexOfSlash === 0 ? patternToTest.slice(1) : patternToTest;
|
|
|
|
/*
|
|
* Escape `{` and `(` because in gitignore patterns they are just
|
|
* literal characters without any specific syntactic meaning,
|
|
* while in minimatch patterns they can form brace expansion or extglob syntax.
|
|
*
|
|
* For example, gitignore pattern `src/{a,b}.js` ignores file `src/{a,b}.js`.
|
|
* But, the same minimatch pattern `src/{a,b}.js` ignores files `src/a.js` and `src/b.js`.
|
|
* Minimatch pattern `src/\{a,b}.js` is equivalent to gitignore pattern `src/{a,b}.js`.
|
|
*/
|
|
const escapedPatternWithoutLeadingSlash =
|
|
patternWithoutLeadingSlash.replaceAll(
|
|
/(?=((?:\\.|[^{(])*))\1([{(])/guy,
|
|
"$1\\$2",
|
|
);
|
|
|
|
const matchInsideSuffix = patternToTest.endsWith("/**") ? "/*" : "";
|
|
|
|
return `${negatedPrefix}${matchEverywherePrefix}${escapedPatternWithoutLeadingSlash}${matchInsideSuffix}`;
|
|
}
|
|
|
|
/**
|
|
* Reads an ignore file and returns an object with the ignore patterns.
|
|
* @param {string} ignoreFilePath The absolute path to the ignore file.
|
|
* @returns {FlatConfig} An object with an `ignores` property that is an array of ignore patterns.
|
|
* @throws {Error} If the ignore file path is not an absolute path.
|
|
*/
|
|
function includeIgnoreFile(ignoreFilePath) {
|
|
if (!path.isAbsolute(ignoreFilePath)) {
|
|
throw new Error("The ignore file location must be an absolute path.");
|
|
}
|
|
|
|
const ignoreFile = fs.readFileSync(ignoreFilePath, "utf8");
|
|
const lines = ignoreFile.split(/\r?\n/u);
|
|
|
|
return {
|
|
name: "Imported .gitignore patterns",
|
|
ignores: lines
|
|
.map(line => line.trim())
|
|
.filter(line => line && !line.startsWith("#"))
|
|
.map(convertIgnorePatternToMinimatch),
|
|
};
|
|
}
|
|
|
|
export { convertIgnorePatternToMinimatch, fixupConfigRules, fixupPluginRules, fixupRule, includeIgnoreFile };
|