@codeque/eslint-plugin
Version:
Create custom ESLint rules based on code samples. Utilizing CodeQue - structural code search engine.
238 lines (237 loc) • 11 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLintCode = void 0;
const core_1 = require("@codeque/core");
const utils_1 = require("./utils");
const telemetry_1 = require("./telemetry");
const queriesCache = {};
let preparationTime = 0;
let shallowSearchTime = 0;
let preparingVisitorsTime = 0;
let preparingQueriesTime = 0;
let filteringFilePathsTime = 0;
const searchTimeForQueries = {};
process.on('beforeExit', () => {
const print = console.log;
const shouldPrintMetric = process.env.CODEQUE_DEBUG === 'true';
if (shouldPrintMetric) {
print('\nCodeQue debug metrics:\n');
print('preparationTime', preparationTime);
print('shallowSearchTime', shallowSearchTime);
print('preparingVisitorsTime', preparingVisitorsTime);
print('preparingQueriesTime', preparingQueriesTime);
print('filteringFilePathsTime', filteringFilePathsTime);
print('searchTimeForQueries', searchTimeForQueries);
print('');
print('Telemetry is', telemetry_1.telemetryDisabled ? 'disabled' : 'enabled');
}
});
const telemetryReported = {
problem: false,
suggestion: false,
layout: false,
};
const createLintCode = (type) => ({
meta: {
type: type,
docs: {
description: 'Lint anything based on code sample(s).',
},
fixable: 'code',
schema: [
{
type: 'array',
items: {
type: 'object',
properties: {
mode: {
type: 'string',
},
query: {
type: 'string',
},
message: {
type: 'string',
},
caseInsensitive: {
type: 'boolean',
},
includeFiles: {
type: 'array',
items: {
type: 'string',
},
},
excludeFiles: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
],
},
create: function (context) {
const telemetry = (0, telemetry_1.createTelemetryInstance)();
const prepStart = performance.now();
const parser = (0, utils_1.assertCompatibleParser)(context.parserPath);
const settings = context.options[0];
if (!settings || settings.length === 0) {
return {};
}
const code = context.getSourceCode().text;
const absoluteFilePath = context.getPhysicalFilename();
const root = context.getCwd();
const defaultCaseInsensitive = true;
const queryCodes = settings.map(({ query }) => query);
const searchModes = settings.map(({ mode }) => mode).filter(Boolean);
if (queryCodes.includes(undefined) || queryCodes.includes(null)) {
throw new Error('Each setting has to have at least query defined.');
}
if (searchModes.includes('text')) {
throw new Error('"Text" search mode is not supported.');
}
const parserSettings = core_1.__internal.parserSettingsMap[utils_1.parserNamesMappingsToCodeQueInternal[parser]]();
const startPreparingQueries = performance.now();
const queriesParseResult = settings.map(({ query, caseInsensitive: caseInsensitive_ }) => {
const caseInsensitive = caseInsensitive_ !== null && caseInsensitive_ !== void 0 ? caseInsensitive_ : defaultCaseInsensitive;
const cacheKey = `(${query});${caseInsensitive}`;
const queryFromCache = queriesCache[cacheKey];
if (queryFromCache) {
return [[queryFromCache], true];
}
const parsedQuery = (0, core_1.parseQueries)([query], caseInsensitive, parserSettings);
queriesCache[cacheKey] = parsedQuery[0][0];
return parsedQuery;
});
const queriesNotParsedCorrectly = queriesParseResult.filter(([_, parseOk]) => !parseOk);
if (queriesNotParsedCorrectly.length > 0) {
throw new Error((0, utils_1.formatQueryParseErrors)(queriesNotParsedCorrectly));
}
const parsedQueries = queriesParseResult.map(([[parsedQuery]]) => parsedQuery);
const parsedQueriesWithSettings = parsedQueries.map((parsedQuery, idx) => {
var _a, _b, _c, _d;
return ({
parsedQuery: parsedQuery,
mode: (_a = settings[idx].mode) !== null && _a !== void 0 ? _a : 'include',
caseInsensitive: (_b = settings[idx].caseInsensitive) !== null && _b !== void 0 ? _b : defaultCaseInsensitive,
message: (_c = settings[idx].message) !== null && _c !== void 0 ? _c : 'Restricted code pattern',
includeFiles: settings[idx].includeFiles,
excludeFiles: (_d = settings[idx].excludeFiles) !== null && _d !== void 0 ? _d : [],
});
});
preparingQueriesTime += performance.now() - startPreparingQueries;
const startFilteringFilePaths = performance.now();
const queriesWithSettingsMatchedFilePath = parsedQueriesWithSettings.filter(({ includeFiles, excludeFiles }) => {
const matchedFilePath = (0, core_1.filterIncludeExclude)({
filesList: [absoluteFilePath],
searchRoot: root,
exclude: excludeFiles,
include: includeFiles,
});
return matchedFilePath.length === 1;
});
filteringFilePathsTime += performance.now() - startFilteringFilePaths;
const shallowStart = performance.now();
const queriesWithSettingsMatchedShallow = queriesWithSettingsMatchedFilePath.filter((parsedQuery) => {
return core_1.__internal.shallowSearch({
queries: [parsedQuery.parsedQuery],
fileContent: code,
logger: {},
caseInsensitive: parsedQuery.caseInsensitive,
});
});
shallowSearchTime += performance.now() - shallowStart;
if (queriesWithSettingsMatchedShallow.length === 0) {
return {};
}
const preparingVisitorsStart = performance.now();
const queryCodesGroupedByStartingNode = queriesWithSettingsMatchedShallow.reduce((map, query) => {
const nodeType = query.parsedQuery.queryNode.type;
if (!map[nodeType]) {
map[nodeType] = [];
}
map[nodeType].push(query);
return map;
}, {});
const createSearchForNode = (parsedQueries) => (node) => {
for (const queryWithSettings of parsedQueries) {
const searchOptions = {
mode: queryWithSettings.mode,
parserSettings,
debug: false,
caseInsensitive: queryWithSettings.caseInsensitive,
};
const startSearch = performance.now();
const { isMultistatement, queryCode, queryNode } = queryWithSettings.parsedQuery;
if (searchTimeForQueries[queryCode] === undefined) {
searchTimeForQueries[queryCode] = 0;
}
const matchContext = core_1.__internal.createMatchContext();
const { match } = core_1.__internal.validateMatch(node, queryWithSettings.parsedQuery.queryNode, searchOptions, matchContext);
if (match) {
let matchData = core_1.__internal.getMatchFromNode(node, parserSettings, matchContext.getAllAliases());
if (isMultistatement) {
/**
* For multi-statement queries we search where exactly statements are located within parent node
*/
matchData = core_1.__internal.getLocationOfMultilineMatch(matchData, queryNode, searchOptions, core_1.__internal.traverseAndMatch);
}
searchTimeForQueries[queryCode] += performance.now() - startSearch;
context.report({
loc: matchData.loc,
message: queryWithSettings.message,
});
}
}
};
const visitorsSearchArrayMap = Object.entries(queryCodesGroupedByStartingNode)
.map(([nodeType, queries]) => {
const visitorKeys = core_1.__internal.getVisitorKeysForQueryNodeType(nodeType, parserSettings);
const searchForNode = createSearchForNode(queries);
const objectEntries = visitorKeys.map((visitorKey) => [
visitorKey,
searchForNode,
]);
return Object.fromEntries(objectEntries);
})
.reduce((visitorsMap, visitorObj) => {
const newVisitorsMap = Object.assign({}, visitorsMap);
for (const visitorKey in visitorObj) {
const searchFn = visitorObj[visitorKey];
if (Array.isArray(newVisitorsMap[visitorKey])) {
newVisitorsMap[visitorKey].push(searchFn);
}
else {
newVisitorsMap[visitorKey] = [searchFn];
}
}
return newVisitorsMap;
}, {});
const visitors = Object.fromEntries(Object.entries(visitorsSearchArrayMap).map(([visitorKey, searchFnsArray]) => {
return [
visitorKey,
(0, utils_1.createMultipleSearchFunctionsExecutor)(searchFnsArray),
];
}));
preparingVisitorsTime += performance.now() - preparingVisitorsStart;
preparationTime += performance.now() - prepStart;
if (!telemetryReported[type]) {
telemetry.reportConfig({
ruleType: type === 'problem' ? 'error' : 'warning',
queriesCount: queryCodes.length,
mode_include: searchModes.includes('include') ||
settings.some(({ mode }) => mode === undefined),
mode_exact: searchModes.includes('exact'),
mode_include_w_order: searchModes.includes('include-with-order'),
fileFilters_include: settings.some(({ includeFiles }) => includeFiles !== undefined),
fileFilters_exclude: settings.some(({ excludeFiles }) => excludeFiles !== undefined),
});
telemetryReported[type] = true;
}
return visitors;
},
});
exports.createLintCode = createLintCode;
;