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.
702 lines
18 KiB
702 lines
18 KiB
/** |
|
* @author Yosuke Ota |
|
* @copyright 2022 Yosuke Ota. All rights reserved. |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
'use strict' |
|
|
|
const utils = require('./index') |
|
const eslintUtils = require('@eslint-community/eslint-utils') |
|
const { definePropertyReferenceExtractor } = require('./property-references') |
|
const { ReferenceTracker } = eslintUtils |
|
|
|
/** |
|
* @typedef {object} RefObjectReferenceForExpression |
|
* @property {'expression'} type |
|
* @property {MemberExpression | CallExpression} node |
|
* @property {string} method |
|
* @property {CallExpression} define |
|
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects. |
|
* |
|
* @typedef {object} RefObjectReferenceForPattern |
|
* @property {'pattern'} type |
|
* @property {ObjectPattern} node |
|
* @property {string} method |
|
* @property {CallExpression} define |
|
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects. |
|
* |
|
* @typedef {object} RefObjectReferenceForIdentifier |
|
* @property {'expression' | 'pattern'} type |
|
* @property {Identifier} node |
|
* @property {VariableDeclarator | null} variableDeclarator |
|
* @property {VariableDeclaration | null} variableDeclaration |
|
* @property {string} method |
|
* @property {CallExpression} define |
|
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects. |
|
* |
|
* @typedef {RefObjectReferenceForIdentifier | RefObjectReferenceForExpression | RefObjectReferenceForPattern} RefObjectReference |
|
*/ |
|
/** |
|
* @typedef {object} ReactiveVariableReference |
|
* @property {Identifier} node |
|
* @property {boolean} escape Within escape hint (`$$()`) |
|
* @property {VariableDeclaration} variableDeclaration |
|
* @property {string} method |
|
* @property {CallExpression} define |
|
*/ |
|
|
|
/** |
|
* @typedef {object} RefObjectReferences |
|
* @property {<T extends Identifier | Expression | Pattern | Super> (node: T) => |
|
* T extends Identifier ? |
|
* RefObjectReferenceForIdentifier | null : |
|
* T extends Expression ? |
|
* RefObjectReferenceForExpression | null : |
|
* T extends Pattern ? |
|
* RefObjectReferenceForPattern | null : |
|
* null} get |
|
*/ |
|
/** |
|
* @typedef {object} ReactiveVariableReferences |
|
* @property {(node: Identifier) => ReactiveVariableReference | null} get |
|
*/ |
|
|
|
const REF_MACROS = [ |
|
'$ref', |
|
'$computed', |
|
'$shallowRef', |
|
'$customRef', |
|
'$toRef', |
|
'$' |
|
] |
|
|
|
/** @type {WeakMap<Program, RefObjectReferences>} */ |
|
const cacheForRefObjectReferences = new WeakMap() |
|
/** @type {WeakMap<Program, ReactiveVariableReferences>} */ |
|
const cacheForReactiveVariableReferences = new WeakMap() |
|
|
|
/** |
|
* Iterate the call expressions that define the ref object. |
|
* @param {import('eslint').Scope.Scope} globalScope |
|
* @returns {Iterable<{ node: CallExpression, name: string }>} |
|
*/ |
|
function* iterateDefineRefs(globalScope) { |
|
const tracker = new ReferenceTracker(globalScope) |
|
for (const { node, path } of utils.iterateReferencesTraceMap(tracker, { |
|
ref: { |
|
[ReferenceTracker.CALL]: true |
|
}, |
|
computed: { |
|
[ReferenceTracker.CALL]: true |
|
}, |
|
toRef: { |
|
[ReferenceTracker.CALL]: true |
|
}, |
|
customRef: { |
|
[ReferenceTracker.CALL]: true |
|
}, |
|
shallowRef: { |
|
[ReferenceTracker.CALL]: true |
|
}, |
|
toRefs: { |
|
[ReferenceTracker.CALL]: true |
|
} |
|
})) { |
|
const expr = /** @type {CallExpression} */ (node) |
|
yield { |
|
node: expr, |
|
name: path[path.length - 1] |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Iterate the call expressions that defineModel() macro. |
|
* @param {import('eslint').Scope.Scope} globalScope |
|
* @returns {Iterable<{ node: CallExpression }>} |
|
*/ |
|
function* iterateDefineModels(globalScope) { |
|
for (const { identifier } of iterateMacroReferences()) { |
|
if ( |
|
identifier.parent.type === 'CallExpression' && |
|
identifier.parent.callee === identifier |
|
) { |
|
yield { |
|
node: identifier.parent |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Iterate macro reference. |
|
* @returns {Iterable<Reference>} |
|
*/ |
|
function* iterateMacroReferences() { |
|
const variable = globalScope.set.get('defineModel') |
|
if ( |
|
variable && |
|
variable.defs.length === 0 /* It was automatically defined. */ |
|
) { |
|
yield* variable.references |
|
} |
|
for (const ref of globalScope.through) { |
|
if (ref.identifier.name === 'defineModel') { |
|
yield ref |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Iterate the call expressions that define the reactive variables. |
|
* @param {import('eslint').Scope.Scope} globalScope |
|
* @returns {Iterable<{ node: CallExpression, name: string }>} |
|
*/ |
|
function* iterateDefineReactiveVariables(globalScope) { |
|
for (const { identifier } of iterateRefMacroReferences()) { |
|
if ( |
|
identifier.parent.type === 'CallExpression' && |
|
identifier.parent.callee === identifier |
|
) { |
|
yield { |
|
node: identifier.parent, |
|
name: identifier.name |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Iterate ref macro reference. |
|
* @returns {Iterable<Reference>} |
|
*/ |
|
function* iterateRefMacroReferences() { |
|
yield* REF_MACROS.map((m) => globalScope.set.get(m)) |
|
.filter(utils.isDef) |
|
.flatMap((v) => v.references) |
|
for (const ref of globalScope.through) { |
|
if (REF_MACROS.includes(ref.identifier.name)) { |
|
yield ref |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Iterate the call expressions that the escape hint values. |
|
* @param {import('eslint').Scope.Scope} globalScope |
|
* @returns {Iterable<CallExpression>} |
|
*/ |
|
function* iterateEscapeHintValueRefs(globalScope) { |
|
for (const { identifier } of iterateEscapeHintReferences()) { |
|
if ( |
|
identifier.parent.type === 'CallExpression' && |
|
identifier.parent.callee === identifier |
|
) { |
|
yield identifier.parent |
|
} |
|
} |
|
|
|
/** |
|
* Iterate escape hint reference. |
|
* @returns {Iterable<Reference>} |
|
*/ |
|
function* iterateEscapeHintReferences() { |
|
const escapeHint = globalScope.set.get('$$') |
|
if (escapeHint) { |
|
yield* escapeHint.references |
|
} |
|
for (const ref of globalScope.through) { |
|
if (ref.identifier.name === '$$') { |
|
yield ref |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Extract identifier from given pattern node. |
|
* @param {Pattern} node |
|
* @returns {Iterable<Identifier>} |
|
*/ |
|
function* extractIdentifier(node) { |
|
switch (node.type) { |
|
case 'Identifier': { |
|
yield node |
|
break |
|
} |
|
case 'ObjectPattern': { |
|
for (const property of node.properties) { |
|
if (property.type === 'Property') { |
|
yield* extractIdentifier(property.value) |
|
} else if (property.type === 'RestElement') { |
|
yield* extractIdentifier(property) |
|
} |
|
} |
|
break |
|
} |
|
case 'ArrayPattern': { |
|
for (const element of node.elements) { |
|
if (element) { |
|
yield* extractIdentifier(element) |
|
} |
|
} |
|
break |
|
} |
|
case 'AssignmentPattern': { |
|
yield* extractIdentifier(node.left) |
|
break |
|
} |
|
case 'RestElement': { |
|
yield* extractIdentifier(node.argument) |
|
break |
|
} |
|
case 'MemberExpression': { |
|
// can't extract |
|
break |
|
} |
|
// No default |
|
} |
|
} |
|
|
|
/** |
|
* Iterate references of the given identifier. |
|
* @param {Identifier} id |
|
* @param {import('eslint').Scope.Scope} globalScope |
|
* @returns {Iterable<import('eslint').Scope.Reference>} |
|
*/ |
|
function* iterateIdentifierReferences(id, globalScope) { |
|
const variable = eslintUtils.findVariable(globalScope, id) |
|
if (!variable) { |
|
return |
|
} |
|
|
|
for (const reference of variable.references) { |
|
yield reference |
|
} |
|
} |
|
|
|
/** |
|
* @param {RuleContext} context The rule context. |
|
*/ |
|
function getGlobalScope(context) { |
|
const sourceCode = context.getSourceCode() |
|
return ( |
|
sourceCode.scopeManager.globalScope || sourceCode.scopeManager.scopes[0] |
|
) |
|
} |
|
|
|
module.exports = { |
|
iterateDefineRefs, |
|
extractRefObjectReferences, |
|
extractReactiveVariableReferences |
|
} |
|
|
|
/** |
|
* @typedef {object} RefObjectReferenceContext |
|
* @property {string} method |
|
* @property {CallExpression} define |
|
* @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects. |
|
*/ |
|
|
|
/** |
|
* @implements {RefObjectReferences} |
|
*/ |
|
class RefObjectReferenceExtractor { |
|
/** |
|
* @param {RuleContext} context The rule context. |
|
*/ |
|
constructor(context) { |
|
this.context = context |
|
/** @type {Map<Identifier | MemberExpression | CallExpression | ObjectPattern, RefObjectReference>} */ |
|
this.references = new Map() |
|
|
|
/** @type {Set<Identifier>} */ |
|
this._processedIds = new Set() |
|
} |
|
|
|
/** |
|
* @template {Identifier | Expression | Pattern | Super} T |
|
* @param {T} node |
|
* @returns {T extends Identifier ? |
|
* RefObjectReferenceForIdentifier | null : |
|
* T extends Expression ? |
|
* RefObjectReferenceForExpression | null : |
|
* T extends Pattern ? |
|
* RefObjectReferenceForPattern | null : |
|
* null} |
|
*/ |
|
get(node) { |
|
return /** @type {never} */ ( |
|
this.references.get(/** @type {never} */ (node)) || null |
|
) |
|
} |
|
|
|
/** |
|
* @param {CallExpression} node |
|
* @param {string} method |
|
*/ |
|
processDefineRef(node, method) { |
|
const parent = node.parent |
|
/** @type {Pattern | null} */ |
|
let pattern = null |
|
if (parent.type === 'VariableDeclarator') { |
|
pattern = parent.id |
|
} else if ( |
|
parent.type === 'AssignmentExpression' && |
|
parent.operator === '=' |
|
) { |
|
pattern = parent.left |
|
} else { |
|
if (method !== 'toRefs') { |
|
this.references.set(node, { |
|
type: 'expression', |
|
node, |
|
method, |
|
define: node, |
|
defineChain: [node] |
|
}) |
|
} |
|
return |
|
} |
|
|
|
const ctx = { |
|
method, |
|
define: node, |
|
defineChain: [node] |
|
} |
|
|
|
if (method === 'toRefs') { |
|
const propertyReferenceExtractor = definePropertyReferenceExtractor( |
|
this.context |
|
) |
|
const propertyReferences = |
|
propertyReferenceExtractor.extractFromPattern(pattern) |
|
for (const name of propertyReferences.allProperties().keys()) { |
|
for (const nest of propertyReferences.getNestNodes(name)) { |
|
if (nest.type === 'expression') { |
|
this.processMemberExpression(nest.node, ctx) |
|
} else if (nest.type === 'pattern') { |
|
this.processPattern(nest.node, ctx) |
|
} |
|
} |
|
} |
|
} else { |
|
this.processPattern(pattern, ctx) |
|
} |
|
} |
|
|
|
/** |
|
* @param {CallExpression} node |
|
*/ |
|
processDefineModel(node) { |
|
const parent = node.parent |
|
/** @type {Pattern | null} */ |
|
let pattern = null |
|
if (parent.type === 'VariableDeclarator') { |
|
pattern = parent.id |
|
} else if ( |
|
parent.type === 'AssignmentExpression' && |
|
parent.operator === '=' |
|
) { |
|
pattern = parent.left |
|
} else { |
|
return |
|
} |
|
|
|
const ctx = { |
|
method: 'defineModel', |
|
define: node, |
|
defineChain: [node] |
|
} |
|
|
|
if (pattern.type === 'ArrayPattern' && pattern.elements[0]) { |
|
pattern = pattern.elements[0] |
|
} |
|
this.processPattern(pattern, ctx) |
|
} |
|
|
|
/** |
|
* @param {MemberExpression | Identifier} node |
|
* @param {RefObjectReferenceContext} ctx |
|
*/ |
|
processExpression(node, ctx) { |
|
const parent = node.parent |
|
if (parent.type === 'AssignmentExpression') { |
|
if (parent.operator === '=' && parent.right === node) { |
|
// `(foo = obj.mem)` |
|
this.processPattern(parent.left, { |
|
...ctx, |
|
defineChain: [node, ...ctx.defineChain] |
|
}) |
|
return true |
|
} |
|
} else if (parent.type === 'VariableDeclarator' && parent.init === node) { |
|
// `const foo = obj.mem` |
|
this.processPattern(parent.id, { |
|
...ctx, |
|
defineChain: [node, ...ctx.defineChain] |
|
}) |
|
return true |
|
} |
|
return false |
|
} |
|
/** |
|
* @param {MemberExpression} node |
|
* @param {RefObjectReferenceContext} ctx |
|
*/ |
|
processMemberExpression(node, ctx) { |
|
if (this.processExpression(node, ctx)) { |
|
return |
|
} |
|
this.references.set(node, { |
|
type: 'expression', |
|
node, |
|
...ctx |
|
}) |
|
} |
|
|
|
/** |
|
* @param {Pattern} node |
|
* @param {RefObjectReferenceContext} ctx |
|
*/ |
|
processPattern(node, ctx) { |
|
switch (node.type) { |
|
case 'Identifier': { |
|
this.processIdentifierPattern(node, ctx) |
|
break |
|
} |
|
case 'ArrayPattern': |
|
case 'RestElement': |
|
case 'MemberExpression': { |
|
return |
|
} |
|
case 'ObjectPattern': { |
|
this.references.set(node, { |
|
type: 'pattern', |
|
node, |
|
...ctx |
|
}) |
|
return |
|
} |
|
case 'AssignmentPattern': { |
|
this.processPattern(node.left, ctx) |
|
return |
|
} |
|
// No default |
|
} |
|
} |
|
|
|
/** |
|
* @param {Identifier} node |
|
* @param {RefObjectReferenceContext} ctx |
|
*/ |
|
processIdentifierPattern(node, ctx) { |
|
if (this._processedIds.has(node)) { |
|
return |
|
} |
|
this._processedIds.add(node) |
|
|
|
for (const reference of iterateIdentifierReferences( |
|
node, |
|
getGlobalScope(this.context) |
|
)) { |
|
const def = |
|
reference.resolved && |
|
reference.resolved.defs.length === 1 && |
|
reference.resolved.defs[0].type === 'Variable' |
|
? reference.resolved.defs[0] |
|
: null |
|
if (def && def.name === reference.identifier) { |
|
continue |
|
} |
|
if ( |
|
reference.isRead() && |
|
this.processExpression(reference.identifier, ctx) |
|
) { |
|
continue |
|
} |
|
this.references.set(reference.identifier, { |
|
type: reference.isWrite() ? 'pattern' : 'expression', |
|
node: reference.identifier, |
|
variableDeclarator: def ? def.node : null, |
|
variableDeclaration: def ? def.parent : null, |
|
...ctx |
|
}) |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Extracts references of all ref objects. |
|
* @param {RuleContext} context The rule context. |
|
* @returns {RefObjectReferences} |
|
*/ |
|
function extractRefObjectReferences(context) { |
|
const sourceCode = context.getSourceCode() |
|
const cachedReferences = cacheForRefObjectReferences.get(sourceCode.ast) |
|
if (cachedReferences) { |
|
return cachedReferences |
|
} |
|
const references = new RefObjectReferenceExtractor(context) |
|
|
|
const globalScope = getGlobalScope(context) |
|
for (const { node, name } of iterateDefineRefs(globalScope)) { |
|
references.processDefineRef(node, name) |
|
} |
|
for (const { node } of iterateDefineModels(globalScope)) { |
|
references.processDefineModel(node) |
|
} |
|
|
|
cacheForRefObjectReferences.set(sourceCode.ast, references) |
|
|
|
return references |
|
} |
|
|
|
/** |
|
* @implements {ReactiveVariableReferences} |
|
*/ |
|
class ReactiveVariableReferenceExtractor { |
|
/** |
|
* @param {RuleContext} context The rule context. |
|
*/ |
|
constructor(context) { |
|
this.context = context |
|
/** @type {Map<Identifier, ReactiveVariableReference>} */ |
|
this.references = new Map() |
|
|
|
/** @type {Set<Identifier>} */ |
|
this._processedIds = new Set() |
|
|
|
/** @type {Set<CallExpression>} */ |
|
this._escapeHintValueRefs = new Set( |
|
iterateEscapeHintValueRefs(getGlobalScope(context)) |
|
) |
|
} |
|
|
|
/** |
|
* @param {Identifier} node |
|
* @returns {ReactiveVariableReference | null} |
|
*/ |
|
get(node) { |
|
return this.references.get(node) || null |
|
} |
|
|
|
/** |
|
* @param {CallExpression} node |
|
* @param {string} method |
|
*/ |
|
processDefineReactiveVariable(node, method) { |
|
const parent = node.parent |
|
if (parent.type !== 'VariableDeclarator') { |
|
return |
|
} |
|
/** @type {Pattern | null} */ |
|
const pattern = parent.id |
|
|
|
if (method === '$') { |
|
for (const id of extractIdentifier(pattern)) { |
|
this.processIdentifierPattern(id, method, node) |
|
} |
|
} else { |
|
if (pattern.type === 'Identifier') { |
|
this.processIdentifierPattern(pattern, method, node) |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* @param {Identifier} node |
|
* @param {string} method |
|
* @param {CallExpression} define |
|
*/ |
|
processIdentifierPattern(node, method, define) { |
|
if (this._processedIds.has(node)) { |
|
return |
|
} |
|
this._processedIds.add(node) |
|
|
|
for (const reference of iterateIdentifierReferences( |
|
node, |
|
getGlobalScope(this.context) |
|
)) { |
|
const def = |
|
reference.resolved && |
|
reference.resolved.defs.length === 1 && |
|
reference.resolved.defs[0].type === 'Variable' |
|
? reference.resolved.defs[0] |
|
: null |
|
if (!def || def.name === reference.identifier) { |
|
continue |
|
} |
|
this.references.set(reference.identifier, { |
|
node: reference.identifier, |
|
escape: this.withinEscapeHint(reference.identifier), |
|
method, |
|
define, |
|
variableDeclaration: def.parent |
|
}) |
|
} |
|
} |
|
|
|
/** |
|
* Checks whether the given identifier node within the escape hints (`$$()`) or not. |
|
* @param {Identifier} node |
|
*/ |
|
withinEscapeHint(node) { |
|
/** @type {Identifier | ObjectExpression | ArrayExpression | SpreadElement | Property | AssignmentProperty} */ |
|
let target = node |
|
/** @type {ASTNode | null} */ |
|
let parent = target.parent |
|
while (parent) { |
|
if (parent.type === 'CallExpression') { |
|
if ( |
|
parent.arguments.includes(/** @type {any} */ (target)) && |
|
this._escapeHintValueRefs.has(parent) |
|
) { |
|
return true |
|
} |
|
return false |
|
} |
|
if ( |
|
(parent.type === 'Property' && parent.value === target) || |
|
(parent.type === 'ObjectExpression' && |
|
parent.properties.includes(/** @type {any} */ (target))) || |
|
parent.type === 'ArrayExpression' || |
|
parent.type === 'SpreadElement' |
|
) { |
|
target = parent |
|
parent = target.parent |
|
} else { |
|
return false |
|
} |
|
} |
|
return false |
|
} |
|
} |
|
|
|
/** |
|
* Extracts references of all reactive variables. |
|
* @param {RuleContext} context The rule context. |
|
* @returns {ReactiveVariableReferences} |
|
*/ |
|
function extractReactiveVariableReferences(context) { |
|
const sourceCode = context.getSourceCode() |
|
const cachedReferences = cacheForReactiveVariableReferences.get( |
|
sourceCode.ast |
|
) |
|
if (cachedReferences) { |
|
return cachedReferences |
|
} |
|
|
|
const references = new ReactiveVariableReferenceExtractor(context) |
|
|
|
for (const { node, name } of iterateDefineReactiveVariables( |
|
getGlobalScope(context) |
|
)) { |
|
references.processDefineReactiveVariable(node, name) |
|
} |
|
|
|
cacheForReactiveVariableReferences.set(sourceCode.ast, references) |
|
|
|
return references |
|
}
|
|
|