@locker/eslint-rule-maker
Version:
Lightning Web Security ESLint rule maker utilities
255 lines (253 loc) • 7.68 kB
JavaScript
/*!
* Copyright (C) 2020 salesforce.com, inc.
*/
;
Object.defineProperty(exports, '__esModule', {
value: true
});
var astLibMaker = require('@locker/ast-lib-maker');
var shared = require('@locker/shared');
const ASTERISK_CHARACTER = '*';
const NO_FIX_OVERRIDE = {
fix: null
};
const astLib = astLibMaker.createLib();
const defaultConfig = {
create: undefined,
meta: {
fixable: undefined,
type: 'suggestion'
},
rule: {
fix: undefined,
message: undefined,
onMatch: undefined,
search: []
}
};
function cloneConfig(config) {
const configClone = JSON.parse(JSON.stringify(config));
if (typeof config.create === 'function') {
configClone.create = config.create;
}
if (shared.isObject(config.rule)) {
const rule = config.rule;
if (typeof rule.fix === 'function') {
configClone.rule.fix = rule.fix;
}
if (typeof rule.message === 'function') {
configClone.rule.message = rule.message;
}
if (typeof rule.onMatch === 'function') {
configClone.rule.onMatch = rule.onMatch;
}
}
return configClone;
}
function defaults(target, source) {
if (typeof target === 'object' && target !== null || typeof target === 'function') {
const props = shared.ReflectOwnKeys(source);
for (let i = 0, {
length
} = props; i < length; i += 1) {
const name = props[i];
if (target[name] === undefined || !shared.ObjectHasOwn(target, name)) {
target[name] = source[name];
}
}
}
return target;
}
function getGlobalScopeByContext(context) {
return context.getSourceCode().scopeManager.globalScope;
}
function getGlobalIdentifiersByContext(context) {
const {
through,
variables
} = getGlobalScopeByContext(context);
const identifiers = [];
for (let i = 0, {
length
} = variables; i < length; i += 1) {
const variable = variables[i];
// ESLint global identifiers have the 'writeable' key.
if (typeof variable.writeable === 'boolean') {
const {
references
} = variable;
// eslint-disable-next-line @typescript-eslint/naming-convention
for (let j = 0, {
length: length_j
} = references; j < length_j; j += 1) {
identifiers.push(references[j].identifier);
}
}
}
for (let i = 0, {
length
} = through; i < length; i += 1) {
identifiers.push(through[i].identifier);
}
return identifiers;
}
function matchAsNonReadableNonWritable({
node
}) {
const type = astLib.getType(node);
return (type === 'Identifier' || type === 'MemberExpression') && NO_FIX_OVERRIDE;
}
function matchAsNonWritable({
node
}) {
const parent = astLib.getParent(node);
if (parent && astLib.getType(parent) === 'AssignmentExpression' && parent.left === node) {
return NO_FIX_OVERRIDE;
}
return false;
}
function matchAsNullishAndNonWritable(data) {
const {
node
} = data;
const parent = astLib.getParent(node);
if (parent) {
// If `parent` is a MemberExpression then its AST represents a child
// property access. For example, with a `pattern` of 'window.top' the
// matched `node` represents `window.top` and `parent` represents
// `window.top.pageXOffset` which is a lint error since `window.top` is
// treated as nullish.
return astLib.getType(parent) === 'MemberExpression' || matchAsNonWritable(data);
}
return false;
}
const matchers = {
matchAsNonReadableNonWritable,
matchAsNonWritable,
matchAsNullishAndNonWritable
};
function createRule(config) {
const configClone = cloneConfig(config);
const defaultConfigClone = cloneConfig(defaultConfig);
let patterns;
let patternsWithAsterisks;
defaultConfigClone.create = function create(context) {
let globals;
let checkedNodes;
const detectedNodes = new Set();
if (patterns === undefined) {
patterns = astLib.expandPatterns(configClone.rule.search);
}
if (patternsWithAsterisks === undefined) {
patternsWithAsterisks = patterns.filter(({
0: segment
}) => segment === ASTERISK_CHARACTER);
}
function detect(detectableIdentifiers, detectablePatterns, callback) {
const matches = astLib.matchAll(detectableIdentifiers, detectablePatterns);
for (let i = 0, {
length
} = matches; i < length; i += 1) {
const matchData = matches[i];
const {
node
} = matchData;
if (!detectedNodes.has(node)) {
detectedNodes.add(node);
callback(matchData);
}
}
}
function report(data) {
const matchedData = {
context,
identifier: data.identifier,
node: data.node,
pattern: data.pattern
};
const {
onMatch
} = configClone.rule;
const matcherData = typeof onMatch !== 'function' || onMatch.call(config.rule, matchedData);
if (!matcherData) {
return;
}
let {
fix,
message
} = configClone.rule;
if (shared.isObject(matcherData)) {
if (shared.ObjectHasOwn(matcherData, 'fix')) {
fix = matcherData.fix;
}
if (shared.ObjectHasOwn(matcherData, 'message')) {
message = matcherData.message;
}
}
const matchedNode = data.node;
if (typeof fix === 'string') {
const replacementCode = fix;
fix = fixer => [fixer.insertTextAfter(matchedNode, replacementCode), fixer.remove(matchedNode)];
} else if (typeof fix === 'function') {
const oldFix = fix;
fix = fixer => oldFix.call(config.rule, fixer, matchedData);
} else {
fix = undefined;
}
if (typeof message === 'function') {
message = message.call(config.rule, matchedData);
}
context.report({
fix,
node: matchedNode,
message: message
});
}
const visitor = {
'Program:exit'() {
if (!globals) {
globals = getGlobalIdentifiersByContext(context);
}
detect(globals, patterns, report);
}
};
if (patternsWithAsterisks.length) {
// @ts-ignore This is flagging a type error due to conflicting versions of @types/estree & eslint types
visitor.MemberExpression = node => {
let currentNode = node;
do {
currentNode = currentNode.object;
} while (currentNode.type === 'MemberExpression');
if (!checkedNodes) {
globals = getGlobalIdentifiersByContext(context);
checkedNodes = new Set(globals);
}
if (!checkedNodes.has(currentNode)) {
checkedNodes.add(currentNode);
detect([currentNode], patternsWithAsterisks, report);
}
};
}
return visitor;
};
// Populate first level default properties.
defaults(configClone, defaultConfigClone);
// Populate second level default properties.
configClone.meta = defaults(configClone.meta, defaultConfigClone.meta);
configClone.rule = defaults(configClone.rule, defaultConfigClone.rule);
if (configClone.meta.fixable === undefined && configClone.rule.fix !== undefined) {
configClone.meta.fixable = 'code';
}
if (shared.isObject(config.rule) && typeof configClone.rule.onMatch !== 'function') {
configClone.rule.onMatch = matchers.matchAsNonReadableNonWritable;
}
// Remove 'rule' from `exportedConfig` so it aligns with the expected
// ESLint.Rule.RuleModule interface.
const exportedConfig = cloneConfig(configClone);
delete exportedConfig.rule;
return exportedConfig;
}
exports.astLib = astLib;
exports.createRule = createRule;
exports.matchers = matchers;
/*! version: 0.25.7 */