UNPKG

@locker/eslint-rule-maker

Version:
255 lines (253 loc) 7.68 kB
/*! * Copyright (C) 2020 salesforce.com, inc. */ 'use strict'; 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 */