You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
697 lines
19 KiB
697 lines
19 KiB
/** |
|
* @author Yosuke Ota |
|
* @copyright 2021 Yosuke Ota. All rights reserved. |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
'use strict' |
|
|
|
const utils = require('./index') |
|
const eslintUtils = require('eslint-utils') |
|
|
|
/** |
|
* @typedef {import('./style-variables').StyleVariablesContext} StyleVariablesContext |
|
*/ |
|
/** |
|
* @typedef {object} IHasPropertyOption |
|
* @property {boolean} [unknownCallAsAny] |
|
*/ |
|
/** |
|
* @typedef {object} IPropertyReferences |
|
* @property { (name: string, option?: IHasPropertyOption) => boolean } hasProperty |
|
* @property { () => Map<string, {nodes:ASTNode[]}> } allProperties |
|
* @property { (name: string) => IPropertyReferences } getNest |
|
*/ |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Helpers |
|
// ------------------------------------------------------------------------------ |
|
|
|
/** @type {IPropertyReferences} */ |
|
const ANY = { |
|
hasProperty: () => true, |
|
allProperties: () => new Map(), |
|
getNest: () => ANY |
|
} |
|
|
|
/** @type {IPropertyReferences} */ |
|
const NEVER = { |
|
hasProperty: () => false, |
|
allProperties: () => new Map(), |
|
getNest: () => NEVER |
|
} |
|
|
|
/** |
|
* @param {RuleContext} context |
|
* @param {Identifier} id |
|
* @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null} |
|
*/ |
|
function findFunction(context, id) { |
|
const calleeVariable = utils.findVariableByIdentifier(context, id) |
|
if (!calleeVariable) { |
|
return null |
|
} |
|
if (calleeVariable.defs.length === 1) { |
|
const def = calleeVariable.defs[0] |
|
if (def.node.type === 'FunctionDeclaration') { |
|
return def.node |
|
} |
|
if ( |
|
def.type === 'Variable' && |
|
def.parent.kind === 'const' && |
|
def.node.init |
|
) { |
|
if ( |
|
def.node.init.type === 'FunctionExpression' || |
|
def.node.init.type === 'ArrowFunctionExpression' |
|
) { |
|
return def.node.init |
|
} |
|
if (def.node.init.type === 'Identifier') { |
|
return findFunction(context, def.node.init) |
|
} |
|
} |
|
} |
|
return null |
|
} |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Public |
|
// ------------------------------------------------------------------------------ |
|
|
|
module.exports = { |
|
definePropertyReferenceExtractor, |
|
mergePropertyReferences |
|
} |
|
|
|
/** |
|
* @param {RuleContext} context The rule context. |
|
*/ |
|
function definePropertyReferenceExtractor(context) { |
|
/** @type {Map<Expression, IPropertyReferences>} */ |
|
const cacheForExpression = new Map() |
|
/** @type {Map<Pattern, IPropertyReferences>} */ |
|
const cacheForPattern = new Map() |
|
/** @type {Map<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, Map<number, IPropertyReferences>>} */ |
|
const cacheForFunction = new Map() |
|
/** @type {{ toRefNodes: Set<ESNode>, toRefsNodes: Set<ESNode>} | null} */ |
|
let toRefSet = null |
|
|
|
let isFunctionalTemplate = false |
|
const templateBody = context.getSourceCode().ast.templateBody |
|
if (templateBody) { |
|
isFunctionalTemplate = utils.hasAttribute(templateBody, 'functional') |
|
} |
|
|
|
function getToRefSet() { |
|
if (toRefSet) { |
|
return toRefSet |
|
} |
|
const tracker = new eslintUtils.ReferenceTracker( |
|
context.getSourceCode().scopeManager.scopes[0] |
|
) |
|
const toRefNodes = new Set() |
|
for (const { node } of tracker.iterateEsmReferences( |
|
utils.createCompositionApiTraceMap({ |
|
[eslintUtils.ReferenceTracker.ESM]: true, |
|
toRef: { |
|
[eslintUtils.ReferenceTracker.CALL]: true |
|
} |
|
}) |
|
)) { |
|
toRefNodes.add(node) |
|
} |
|
const toRefsNodes = new Set() |
|
for (const { node } of tracker.iterateEsmReferences( |
|
utils.createCompositionApiTraceMap({ |
|
[eslintUtils.ReferenceTracker.ESM]: true, |
|
toRefs: { |
|
[eslintUtils.ReferenceTracker.CALL]: true |
|
} |
|
}) |
|
)) { |
|
toRefsNodes.add(node) |
|
} |
|
|
|
return (toRefSet = { toRefNodes, toRefsNodes }) |
|
} |
|
|
|
/** |
|
* Collects the property references for member expr. |
|
* @implements IPropertyReferences |
|
*/ |
|
class PropertyReferencesForMember { |
|
/** |
|
* |
|
* @param {MemberExpression} node |
|
* @param {string} name |
|
* @param {boolean} withInTemplate |
|
*/ |
|
constructor(node, name, withInTemplate) { |
|
this.node = node |
|
this.name = name |
|
this.withInTemplate = withInTemplate |
|
} |
|
|
|
/** |
|
* @param {string} name |
|
*/ |
|
hasProperty(name) { |
|
return name === this.name |
|
} |
|
|
|
allProperties() { |
|
return new Map([[this.name, { nodes: [this.node.property] }]]) |
|
} |
|
|
|
/** |
|
* @param {string} name |
|
* @returns {IPropertyReferences} |
|
*/ |
|
getNest(name) { |
|
return name === this.name |
|
? extractFromExpression(this.node, this.withInTemplate) |
|
: NEVER |
|
} |
|
} |
|
/** |
|
* Collects the property references for object. |
|
* @implements IPropertyReferences |
|
*/ |
|
class PropertyReferencesForObject { |
|
constructor() { |
|
/** @type {Record<string, AssignmentProperty[]>} */ |
|
this.properties = Object.create(null) |
|
} |
|
|
|
/** |
|
* @param {string} name |
|
*/ |
|
hasProperty(name) { |
|
return Boolean(this.properties[name]) |
|
} |
|
|
|
allProperties() { |
|
const result = new Map() |
|
for (const [name, nodes] of Object.entries(this.properties)) { |
|
result.set(name, { nodes: nodes.map((node) => node.key) }) |
|
} |
|
return result |
|
} |
|
|
|
/** |
|
* @param {string} name |
|
* @returns {IPropertyReferences} |
|
*/ |
|
getNest(name) { |
|
const properties = this.properties[name] |
|
return properties |
|
? mergePropertyReferences( |
|
properties.map((property) => getNestFromPattern(property.value)) |
|
) |
|
: NEVER |
|
|
|
/** |
|
* @param {Pattern} pattern |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function getNestFromPattern(pattern) { |
|
if (pattern.type === 'ObjectPattern') { |
|
return extractFromObjectPattern(pattern) |
|
} |
|
if (pattern.type === 'Identifier') { |
|
return extractFromIdentifier(pattern) |
|
} else if (pattern.type === 'AssignmentPattern') { |
|
return getNestFromPattern(pattern.left) |
|
} |
|
return ANY |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from Expression. |
|
* @param {Identifier | MemberExpression | ChainExpression | ThisExpression | CallExpression} node |
|
* @param {boolean} withInTemplate |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromExpression(node, withInTemplate) { |
|
const ref = cacheForExpression.get(node) |
|
if (ref) { |
|
return ref |
|
} |
|
cacheForExpression.set(node, ANY) |
|
const result = extractWithoutCache() |
|
cacheForExpression.set(node, result) |
|
return result |
|
|
|
function extractWithoutCache() { |
|
const parent = node.parent |
|
if (parent.type === 'AssignmentExpression') { |
|
if (withInTemplate) { |
|
return NEVER |
|
} |
|
if (parent.right === node) { |
|
// `({foo} = arg)` |
|
return extractFromPattern(parent.left) |
|
} |
|
return NEVER |
|
} else if (parent.type === 'VariableDeclarator') { |
|
if (withInTemplate) { |
|
return NEVER |
|
} |
|
if (parent.init === node) { |
|
// `const {foo} = arg` |
|
// `const foo = arg` |
|
return extractFromPattern(parent.id) |
|
} |
|
return NEVER |
|
} else if (parent.type === 'MemberExpression') { |
|
if (parent.object === node) { |
|
// `arg.foo` |
|
const name = utils.getStaticPropertyName(parent) |
|
if (name) { |
|
return new PropertyReferencesForMember(parent, name, withInTemplate) |
|
} else { |
|
return ANY |
|
} |
|
} |
|
return NEVER |
|
} else if (parent.type === 'CallExpression') { |
|
if (withInTemplate) { |
|
return NEVER |
|
} |
|
const argIndex = parent.arguments.indexOf(node) |
|
if (argIndex > -1) { |
|
// `foo(arg)` |
|
return extractFromCall(parent, argIndex) |
|
} |
|
} else if (parent.type === 'ChainExpression') { |
|
return extractFromExpression(parent, withInTemplate) |
|
} else if ( |
|
parent.type === 'ArrowFunctionExpression' || |
|
parent.type === 'ReturnStatement' || |
|
parent.type === 'VExpressionContainer' || |
|
parent.type === 'Property' || |
|
parent.type === 'ArrayExpression' |
|
) { |
|
// Maybe used externally. |
|
if (maybeExternalUsed(parent)) { |
|
return ANY |
|
} |
|
} |
|
return NEVER |
|
} |
|
|
|
/** |
|
* @param {ASTNode} parentTarget |
|
* @returns {boolean} |
|
*/ |
|
function maybeExternalUsed(parentTarget) { |
|
if ( |
|
parentTarget.type === 'ReturnStatement' || |
|
parentTarget.type === 'VExpressionContainer' |
|
) { |
|
return true |
|
} |
|
if (parentTarget.type === 'ArrayExpression') { |
|
return maybeExternalUsed(parentTarget.parent) |
|
} |
|
if (parentTarget.type === 'Property') { |
|
return maybeExternalUsed(parentTarget.parent.parent) |
|
} |
|
if (parentTarget.type === 'ArrowFunctionExpression') { |
|
return parentTarget.body === node |
|
} |
|
return false |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from one parameter of the function. |
|
* @param {Pattern} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromPattern(node) { |
|
const ref = cacheForPattern.get(node) |
|
if (ref) { |
|
return ref |
|
} |
|
cacheForPattern.set(node, ANY) |
|
const result = extractWithoutCache() |
|
cacheForPattern.set(node, result) |
|
return result |
|
|
|
function extractWithoutCache() { |
|
while (node.type === 'AssignmentPattern') { |
|
node = node.left |
|
} |
|
if (node.type === 'RestElement' || node.type === 'ArrayPattern') { |
|
// cannot check |
|
return NEVER |
|
} |
|
if (node.type === 'ObjectPattern') { |
|
return extractFromObjectPattern(node) |
|
} |
|
if (node.type === 'Identifier') { |
|
return extractFromIdentifier(node) |
|
} |
|
return NEVER |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from ObjectPattern. |
|
* @param {ObjectPattern} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromObjectPattern(node) { |
|
const refs = new PropertyReferencesForObject() |
|
for (const prop of node.properties) { |
|
if (prop.type === 'Property') { |
|
const name = utils.getStaticPropertyName(prop) |
|
if (name) { |
|
const list = refs.properties[name] || (refs.properties[name] = []) |
|
list.push(prop) |
|
} else { |
|
// If cannot trace name, everything is used! |
|
return ANY |
|
} |
|
} else { |
|
// If use RestElement, everything is used! |
|
return ANY |
|
} |
|
} |
|
return refs |
|
} |
|
|
|
/** |
|
* Extract the property references from id. |
|
* @param {Identifier} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromIdentifier(node) { |
|
const variable = utils.findVariableByIdentifier(context, node) |
|
if (!variable) { |
|
return NEVER |
|
} |
|
return mergePropertyReferences( |
|
variable.references.map((reference) => { |
|
const id = reference.identifier |
|
return extractFromExpression(id, false) |
|
}) |
|
) |
|
} |
|
|
|
/** |
|
* Extract the property references from call. |
|
* @param {CallExpression} node |
|
* @param {number} argIndex |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromCall(node, argIndex) { |
|
if (node.callee.type !== 'Identifier') { |
|
return { |
|
hasProperty(_name, options) { |
|
return Boolean(options && options.unknownCallAsAny) |
|
}, |
|
allProperties: () => new Map(), |
|
getNest: () => ANY |
|
} |
|
} |
|
|
|
const fnNode = findFunction(context, node.callee) |
|
if (!fnNode) { |
|
if (argIndex === 0) { |
|
if (getToRefSet().toRefNodes.has(node)) { |
|
return extractFromToRef(node) |
|
} else if (getToRefSet().toRefsNodes.has(node)) { |
|
return extractFromToRefs(node) |
|
} |
|
} |
|
return { |
|
hasProperty(_name, options) { |
|
return Boolean(options && options.unknownCallAsAny) |
|
}, |
|
allProperties: () => new Map(), |
|
getNest: () => ANY |
|
} |
|
} |
|
|
|
return extractFromFunctionParam(fnNode, argIndex) |
|
} |
|
|
|
/** |
|
* Extract the property references from function param. |
|
* @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node |
|
* @param {number} argIndex |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromFunctionParam(node, argIndex) { |
|
let cacheForIndexes = cacheForFunction.get(node) |
|
if (!cacheForIndexes) { |
|
cacheForIndexes = new Map() |
|
cacheForFunction.set(node, cacheForIndexes) |
|
} |
|
const ref = cacheForIndexes.get(argIndex) |
|
if (ref) { |
|
return ref |
|
} |
|
cacheForIndexes.set(argIndex, NEVER) |
|
const arg = node.params[argIndex] |
|
if (!arg) { |
|
return NEVER |
|
} |
|
const result = extractFromPattern(arg) |
|
cacheForIndexes.set(argIndex, result) |
|
return result |
|
} |
|
|
|
/** |
|
* Extract the property references from path. |
|
* @param {string} pathString |
|
* @param {Identifier | Literal | TemplateLiteral} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromPath(pathString, node) { |
|
return extractFromSegments(pathString.split('.')) |
|
|
|
/** |
|
* @param {string[]} segments |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromSegments(segments) { |
|
if (!segments.length) { |
|
return ANY |
|
} |
|
const segmentName = segments[0] |
|
return { |
|
hasProperty: (name) => name === segmentName, |
|
allProperties: () => new Map([[segmentName, { nodes: [node] }]]), |
|
getNest: (name) => |
|
name === segmentName ? extractFromSegments(segments.slice(1)) : NEVER |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from name literal. |
|
* @param {Expression} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromNameLiteral(node) { |
|
const referenceName = |
|
node.type === 'Literal' || node.type === 'TemplateLiteral' |
|
? utils.getStringLiteralValue(node) |
|
: null |
|
if (referenceName) { |
|
return { |
|
hasProperty: (name) => name === referenceName, |
|
allProperties: () => new Map([[referenceName, { nodes: [node] }]]), |
|
getNest: (name) => (name === referenceName ? ANY : NEVER) |
|
} |
|
} else { |
|
return NEVER |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from name. |
|
* @param {string} referenceName |
|
* @param {Expression|SpreadElement} nameNode |
|
* @param { () => IPropertyReferences } [getNest] |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromName(referenceName, nameNode, getNest) { |
|
return { |
|
hasProperty: (name) => name === referenceName, |
|
allProperties: () => new Map([[referenceName, { nodes: [nameNode] }]]), |
|
getNest: (name) => |
|
name === referenceName ? (getNest ? getNest() : ANY) : NEVER |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from toRef call. |
|
* @param {CallExpression} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromToRef(node) { |
|
const nameNode = node.arguments[1] |
|
const refName = |
|
nameNode && |
|
(nameNode.type === 'Literal' || nameNode.type === 'TemplateLiteral') |
|
? utils.getStringLiteralValue(nameNode) |
|
: null |
|
if (!refName) { |
|
// unknown name |
|
return ANY |
|
} |
|
return extractFromName(refName, nameNode, () => { |
|
return extractFromExpression(node, false).getNest('value') |
|
}) |
|
} |
|
|
|
/** |
|
* Extract the property references from toRefs call. |
|
* @param {CallExpression} node |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromToRefs(node) { |
|
const base = extractFromExpression(node, false) |
|
return { |
|
hasProperty: (name, option) => base.hasProperty(name, option), |
|
allProperties: () => base.allProperties(), |
|
getNest: (name) => base.getNest(name).getNest('value') |
|
} |
|
} |
|
|
|
/** |
|
* Extract the property references from VExpressionContainer. |
|
* @param {VExpressionContainer} node |
|
* @param {object} [options] |
|
* @param {boolean} [options.ignoreGlobals] |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromVExpressionContainer(node, options) { |
|
const ignoreGlobals = options && options.ignoreGlobals |
|
|
|
/** @type { (name:string)=>boolean } */ |
|
let ignoreRef = () => false |
|
if (ignoreGlobals) { |
|
const globalScope = |
|
context.getSourceCode().scopeManager.globalScope || |
|
context.getSourceCode().scopeManager.scopes[0] |
|
|
|
ignoreRef = (name) => globalScope.set.has(name) |
|
} |
|
/** @type {IPropertyReferences[]} */ |
|
const references = [] |
|
for (const id of node.references |
|
.filter((ref) => ref.variable == null) |
|
.map((ref) => ref.id)) { |
|
if (ignoreRef(id.name)) { |
|
continue |
|
} |
|
if (!isFunctionalTemplate) { |
|
references.push( |
|
extractFromName(id.name, id, () => extractFromExpression(id, true)) |
|
) |
|
} else { |
|
if (id.name === 'props') { |
|
references.push(extractFromExpression(id, true)) |
|
} |
|
} |
|
} |
|
return mergePropertyReferences(references) |
|
} |
|
/** |
|
* Extract the property references from StyleVariablesContext. |
|
* @param {StyleVariablesContext} ctx |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function extractFromStyleVariablesContext(ctx) { |
|
const references = [] |
|
for (const { id } of ctx.references) { |
|
references.push( |
|
extractFromName(id.name, id, () => extractFromExpression(id, true)) |
|
) |
|
} |
|
return mergePropertyReferences(references) |
|
} |
|
|
|
return { |
|
extractFromExpression, |
|
extractFromPattern, |
|
extractFromFunctionParam, |
|
extractFromPath, |
|
extractFromName, |
|
extractFromNameLiteral, |
|
extractFromVExpressionContainer, |
|
extractFromStyleVariablesContext |
|
} |
|
} |
|
|
|
/** |
|
* @param {IPropertyReferences[]} references |
|
* @returns {IPropertyReferences} |
|
*/ |
|
function mergePropertyReferences(references) { |
|
if (references.length === 0) { |
|
return NEVER |
|
} |
|
if (references.length === 1) { |
|
return references[0] |
|
} |
|
return new PropertyReferencesForMerge(references) |
|
} |
|
|
|
/** |
|
* Collects the property references for merge. |
|
* @implements IPropertyReferences |
|
*/ |
|
class PropertyReferencesForMerge { |
|
/** |
|
* @param {IPropertyReferences[]} references |
|
*/ |
|
constructor(references) { |
|
this.references = references |
|
} |
|
|
|
/** |
|
* @param {string} name |
|
* @param {IHasPropertyOption} [option] |
|
*/ |
|
hasProperty(name, option) { |
|
return this.references.some((ref) => ref.hasProperty(name, option)) |
|
} |
|
|
|
allProperties() { |
|
const result = new Map() |
|
for (const reference of this.references) { |
|
for (const [name, { nodes }] of reference.allProperties()) { |
|
const r = result.get(name) |
|
if (r) { |
|
r.nodes = [...new Set([...r.nodes, ...nodes])] |
|
} else { |
|
result.set(name, { nodes: [...nodes] }) |
|
} |
|
} |
|
} |
|
return result |
|
} |
|
|
|
/** |
|
* @param {string} name |
|
* @returns {IPropertyReferences} |
|
*/ |
|
getNest(name) { |
|
/** @type {IPropertyReferences[]} */ |
|
const nest = [] |
|
for (const ref of this.references) { |
|
if (ref.hasProperty(name)) { |
|
nest.push(ref.getNest(name)) |
|
} |
|
} |
|
return mergePropertyReferences(nest) |
|
} |
|
}
|
|
|