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.
131 lines
4.0 KiB
131 lines
4.0 KiB
/** |
|
* @fileoverview disallow usage of `this` in template. |
|
* @author Armano |
|
*/ |
|
'use strict' |
|
|
|
const utils = require('../utils') |
|
const RESERVED_NAMES = new Set(require('../utils/js-reserved.json')) |
|
|
|
module.exports = { |
|
meta: { |
|
type: 'suggestion', |
|
docs: { |
|
description: 'disallow usage of `this` in template', |
|
categories: ['vue3-recommended', 'vue2-recommended'], |
|
url: 'https://eslint.vuejs.org/rules/this-in-template.html' |
|
}, |
|
fixable: 'code', |
|
schema: [ |
|
{ |
|
enum: ['always', 'never'] |
|
} |
|
], |
|
messages: { |
|
unexpected: "Unexpected usage of 'this'.", |
|
expected: "Expected 'this'." |
|
} |
|
}, |
|
|
|
/** |
|
* Creates AST event handlers for this-in-template. |
|
* |
|
* @param {RuleContext} context - The rule context. |
|
* @returns {Object} AST event handlers. |
|
*/ |
|
create(context) { |
|
const options = context.options[0] === 'always' ? 'always' : 'never' |
|
/** |
|
* @typedef {object} ScopeStack |
|
* @property {ScopeStack | null} parent |
|
* @property {Identifier[]} nodes |
|
*/ |
|
|
|
/** @type {ScopeStack | null} */ |
|
let scopeStack = null |
|
|
|
return utils.defineTemplateBodyVisitor(context, { |
|
/** @param {VElement} node */ |
|
VElement(node) { |
|
scopeStack = { |
|
parent: scopeStack, |
|
nodes: scopeStack |
|
? [...scopeStack.nodes] // make copy |
|
: [] |
|
} |
|
if (node.variables) { |
|
for (const variable of node.variables) { |
|
const varNode = variable.id |
|
const name = varNode.name |
|
if (!scopeStack.nodes.some((node) => node.name === name)) { |
|
// Prevent adding duplicates |
|
scopeStack.nodes.push(varNode) |
|
} |
|
} |
|
} |
|
}, |
|
'VElement:exit'() { |
|
scopeStack = scopeStack && scopeStack.parent |
|
}, |
|
...(options === 'never' |
|
? { |
|
/** @param { ThisExpression & { parent: MemberExpression } } node */ |
|
'VExpressionContainer MemberExpression > ThisExpression'(node) { |
|
if (!scopeStack) { |
|
return |
|
} |
|
const propertyName = utils.getStaticPropertyName(node.parent) |
|
if ( |
|
!propertyName || |
|
scopeStack.nodes.some((el) => el.name === propertyName) || |
|
RESERVED_NAMES.has(propertyName) || // this.class | this['class'] |
|
/^\d.*$|[^\w$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas'] |
|
) { |
|
return |
|
} |
|
|
|
context.report({ |
|
node, |
|
loc: node.loc, |
|
fix(fixer) { |
|
// node.parent should be some code like `this.test`, `this?.['result']` |
|
return fixer.replaceText(node.parent, propertyName) |
|
}, |
|
messageId: 'unexpected' |
|
}) |
|
} |
|
} |
|
: { |
|
/** @param {VExpressionContainer} node */ |
|
VExpressionContainer(node) { |
|
if (!scopeStack) { |
|
return |
|
} |
|
if (node.parent.type === 'VDirectiveKey') { |
|
// We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier. |
|
// For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier. |
|
return |
|
} |
|
if (node.references) { |
|
for (const reference of node.references) { |
|
if ( |
|
!scopeStack.nodes.some( |
|
(el) => el.name === reference.id.name |
|
) |
|
) { |
|
context.report({ |
|
node: reference.id, |
|
loc: reference.id.loc, |
|
messageId: 'expected', |
|
fix(fixer) { |
|
return fixer.insertTextBefore(reference.id, 'this.') |
|
} |
|
}) |
|
} |
|
} |
|
} |
|
} |
|
}) |
|
}) |
|
} |
|
}
|
|
|