123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- /**
- * @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
- }
|