From fec4e419b591c03606c15b081a12c35018f074ea Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 14 Jul 2020 11:43:17 +0900 Subject: [PATCH] Add `vue/no-dupe-v-else-if` rule (#1239) --- docs/rules/README.md | 2 + docs/rules/no-dupe-v-else-if.md | 98 +++ lib/configs/essential.js | 1 + lib/configs/vue3-essential.js | 1 + lib/index.js | 1 + lib/rules/no-dupe-v-else-if.js | 193 +++++ lib/utils/index.js | 26 + tests/lib/rules/no-dupe-v-else-if.js | 662 ++++++++++++++++++ .../eslint-plugin-vue/util-types/ast/ast.ts | 104 ++- 9 files changed, 1072 insertions(+), 16 deletions(-) create mode 100644 docs/rules/no-dupe-v-else-if.md create mode 100644 lib/rules/no-dupe-v-else-if.js create mode 100644 tests/lib/rules/no-dupe-v-else-if.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 6b74ad281..3dc1c2550 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -58,6 +58,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/no-deprecated-v-on-number-modifiers](./no-deprecated-v-on-number-modifiers.md) | disallow using deprecated number (keycode) modifiers (in Vue.js 3.0.0+) | :wrench: | | [vue/no-deprecated-vue-config-keycodes](./no-deprecated-vue-config-keycodes.md) | disallow using deprecated `Vue.config.keyCodes` (in Vue.js 3.0.0+) | | | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | | +| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains | | | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | | | [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks | | | [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | | @@ -171,6 +172,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component | | | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names | | +| [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains | | | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes | | | [vue/no-multiple-template-root](./no-multiple-template-root.md) | disallow adding multiple root nodes to the template | | | [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props | | diff --git a/docs/rules/no-dupe-v-else-if.md b/docs/rules/no-dupe-v-else-if.md new file mode 100644 index 000000000..2960bdbb1 --- /dev/null +++ b/docs/rules/no-dupe-v-else-if.md @@ -0,0 +1,98 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-dupe-v-else-if +description: disallow duplicate conditions in `v-if` / `v-else-if` chains +--- +# vue/no-dupe-v-else-if +> disallow duplicate conditions in `v-if` / `v-else-if` chains + +- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`. + +## :book: Rule Details + +This rule disallows duplicate conditions in the same `v-if` / `v-else-if` chain. + + + +```vue + +``` + + + +This rule can also detect some cases where the conditions are not identical, but the branch can never execute due to the logic of `||` and `&&` operators. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :couple: Related rules + +- [no-dupe-else-if] + +[no-dupe-else-if]: https://eslint.org/docs/rules/no-dupe-else-if + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-dupe-v-else-if.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-dupe-v-else-if.js) diff --git a/lib/configs/essential.js b/lib/configs/essential.js index 9005982b7..722cf1fa1 100644 --- a/lib/configs/essential.js +++ b/lib/configs/essential.js @@ -11,6 +11,7 @@ module.exports = { 'vue/no-async-in-computed-properties': 'error', 'vue/no-custom-modifiers-on-v-model': 'error', 'vue/no-dupe-keys': 'error', + 'vue/no-dupe-v-else-if': 'error', 'vue/no-duplicate-attributes': 'error', 'vue/no-multiple-template-root': 'error', 'vue/no-mutating-props': 'error', diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index 432df355b..5adbbcd23 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -26,6 +26,7 @@ module.exports = { 'vue/no-deprecated-v-on-number-modifiers': 'error', 'vue/no-deprecated-vue-config-keycodes': 'error', 'vue/no-dupe-keys': 'error', + 'vue/no-dupe-v-else-if': 'error', 'vue/no-duplicate-attributes': 'error', 'vue/no-lifecycle-after-await': 'error', 'vue/no-mutating-props': 'error', diff --git a/lib/index.js b/lib/index.js index 8e54379af..282ddd399 100644 --- a/lib/index.js +++ b/lib/index.js @@ -67,6 +67,7 @@ module.exports = { 'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'), 'no-deprecated-vue-config-keycodes': require('./rules/no-deprecated-vue-config-keycodes'), 'no-dupe-keys': require('./rules/no-dupe-keys'), + 'no-dupe-v-else-if': require('./rules/no-dupe-v-else-if'), 'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'), 'no-duplicate-attributes': require('./rules/no-duplicate-attributes'), 'no-empty-component-block': require('./rules/no-empty-component-block'), diff --git a/lib/rules/no-dupe-v-else-if.js b/lib/rules/no-dupe-v-else-if.js new file mode 100644 index 000000000..8e3a3dec1 --- /dev/null +++ b/lib/rules/no-dupe-v-else-if.js @@ -0,0 +1,193 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * @typedef {NonNullable} VExpression + */ +/** + * @typedef {object} OrOperands + * @property {VExpression} OrOperands.node + * @property {AndOperands[]} OrOperands.operands + * + * @typedef {object} AndOperands + * @property {VExpression} AndOperands.node + * @property {VExpression[]} AndOperands.operands + */ +/** + * Splits the given node by the given logical operator. + * @param {string} operator Logical operator `||` or `&&`. + * @param {VExpression} node The node to split. + * @returns {VExpression[]} Array of conditions that makes the node when joined by the operator. + */ +function splitByLogicalOperator(operator, node) { + if (node.type === 'LogicalExpression' && node.operator === operator) { + return [ + ...splitByLogicalOperator(operator, node.left), + ...splitByLogicalOperator(operator, node.right) + ] + } + return [node] +} + +/** + * @param {VExpression} node + */ +function splitByOr(node) { + return splitByLogicalOperator('||', node) +} +/** + * @param {VExpression} node + */ +function splitByAnd(node) { + return splitByLogicalOperator('&&', node) +} + +/** + * @param {VExpression} node + * @returns {OrOperands} + */ +function buildOrOperands(node) { + const orOperands = splitByOr(node) + return { + node, + operands: orOperands.map((orOperand) => { + const andOperands = splitByAnd(orOperand) + return { + node: orOperand, + operands: andOperands + } + }) + } +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: + 'disallow duplicate conditions in `v-if` / `v-else-if` chains', + categories: ['vue3-essential', 'essential'], + url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html' + }, + fixable: null, + schema: [], + messages: { + unexpected: + 'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const tokenStore = + context.parserServices.getTemplateBodyTokenStore && + context.parserServices.getTemplateBodyTokenStore() + /** + * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes + * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators. + * @param {VExpression} a First node. + * @param {VExpression} b Second node. + * @returns {boolean} `true` if the nodes are considered to be equal. + */ + function equal(a, b) { + if (a.type !== b.type) { + return false + } + + if ( + a.type === 'LogicalExpression' && + b.type === 'LogicalExpression' && + (a.operator === '||' || a.operator === '&&') && + a.operator === b.operator + ) { + return ( + (equal(a.left, b.left) && equal(a.right, b.right)) || + (equal(a.left, b.right) && equal(a.right, b.left)) + ) + } + + return utils.equalTokens(a, b, tokenStore) + } + + /** + * Determines whether the first given AndOperands is a subset of the second given AndOperands. + * + * e.g. A: (a && b), B: (a && b && c): B is a subset of A. + * + * @param {AndOperands} operandsA The AndOperands to compare from. + * @param {AndOperands} operandsB The AndOperands to compare against. + * @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`. + */ + function isSubset(operandsA, operandsB) { + return operandsA.operands.every((operandA) => + operandsB.operands.some((operandB) => equal(operandA, operandB)) + ) + } + + return utils.defineTemplateBodyVisitor(context, { + "VAttribute[directive=true][key.name.name='else-if']"(node) { + if (!node.value || !node.value.expression) { + return + } + const test = node.value.expression + const conditionsToCheck = + test.type === 'LogicalExpression' && test.operator === '&&' + ? [...splitByAnd(test), test] + : [test] + const listToCheck = conditionsToCheck.map(buildOrOperands) + + /** @type {VElement | null} */ + let current = node.parent.parent + while (current && (current = utils.prevSibling(current))) { + const vIf = utils.getDirective(current, 'if') + const currentTestDir = vIf || utils.getDirective(current, 'else-if') + if (!currentTestDir) { + return + } + if (currentTestDir.value && currentTestDir.value.expression) { + const currentOrOperands = buildOrOperands( + currentTestDir.value.expression + ) + + for (const condition of listToCheck) { + const operands = (condition.operands = condition.operands.filter( + (orOperand) => { + return !currentOrOperands.operands.some((currentOrOperand) => + isSubset(currentOrOperand, orOperand) + ) + } + )) + if (!operands.length) { + context.report({ + node: condition.node, + messageId: 'unexpected' + }) + return + } + } + } + + if (vIf) { + return + } + } + } + }) + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index a0710a8d9..7fae65530 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1530,6 +1530,32 @@ module.exports = { return null } + }, + + /** + * Checks whether or not the tokens of two given nodes are same. + * @param {ASTNode} left A node 1 to compare. + * @param {ASTNode} right A node 2 to compare. + * @param {ParserServices.TokenStore | SourceCode} sourceCode The ESLint source code object. + * @returns {boolean} the source code for the given node. + */ + equalTokens(left, right, sourceCode) { + const tokensL = sourceCode.getTokens(left) + const tokensR = sourceCode.getTokens(right) + + if (tokensL.length !== tokensR.length) { + return false + } + for (let i = 0; i < tokensL.length; ++i) { + if ( + tokensL[i].type !== tokensR[i].type || + tokensL[i].value !== tokensR[i].value + ) { + return false + } + } + + return true } } diff --git a/tests/lib/rules/no-dupe-v-else-if.js b/tests/lib/rules/no-dupe-v-else-if.js new file mode 100644 index 000000000..0b5698fe8 --- /dev/null +++ b/tests/lib/rules/no-dupe-v-else-if.js @@ -0,0 +1,662 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-dupe-v-else-if') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2019, + sourceType: 'module' + } +}) + +tester.run('no-dupe-v-else-if', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + // parse error + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + + // Referred to the ESLint core rule. + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '' + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: + 'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.', + line: 4, + column: 25, + endLine: 4, + endColumn: 28 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'unexpected', + line: 4, + column: 25, + endLine: 4, + endColumn: 28 + }, + { + messageId: 'unexpected', + line: 5, + column: 32, + endLine: 5, + endColumn: 35 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { messageId: 'unexpected', line: 4 }, + { messageId: 'unexpected', line: 5 } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { messageId: 'unexpected', line: 4 }, + { messageId: 'unexpected', line: 5 } + ] + }, + { + filename: 'foo.vue', + code: ` + + `, + errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }] + }, + + // Referred to the ESLint core rule. + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [ + { messageId: 'unexpected' }, + { messageId: 'unexpected' }, + { messageId: 'unexpected' } + ] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }, { messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: + '', + errors: [{ messageId: 'unexpected' }] + }, + { + filename: 'test.vue', + code: '', + errors: [{ messageId: 'unexpected' }] + } + ] +}) diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts index bcdeb93c6..357b56400 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts @@ -12,32 +12,104 @@ export type VNodeListenerMap = { 'VAttribute:exit': V.VAttribute | V.VDirective 'VAttribute[directive=false]': V.VAttribute 'VAttribute[directive=false]:exit': V.VAttribute - "VAttribute[directive=true][key.name.name='bind']": V.VDirective - "VAttribute[directive=true][key.name.name='bind']:exit": V.VDirective + "VAttribute[directive=true][key.name.name='bind']": V.VDirective & { + value: + | (V.VExpressionContainer & { + expression: ES.Expression | V.VFilterSequenceExpression | null + }) + | null + } + "VAttribute[directive=true][key.name.name='bind']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { + expression: ES.Expression | V.VFilterSequenceExpression | null + }) + | null + } "VAttribute[directive=true][key.name.name='cloak']": V.VDirective "VAttribute[directive=true][key.name.name='cloak']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='else-if']": V.VDirective - "VAttribute[directive=true][key.name.name='else-if']:exit": V.VDirective + "VAttribute[directive=true][key.name.name='else-if']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='else-if']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } "VAttribute[directive=true][key.name.name='else']": V.VDirective "VAttribute[directive=true][key.name.name='else']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='for']": V.VDirective - "VAttribute[directive=true][key.name.name='for']:exit": V.VDirective + "VAttribute[directive=true][key.name.name='for']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: V.VForExpression | null }) + | null + } + "VAttribute[directive=true][key.name.name='for']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: V.VForExpression | null }) + | null + } "VAttribute[directive=true][key.name.name='html']": V.VDirective "VAttribute[directive=true][key.name.name='html']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='if']": V.VDirective - "VAttribute[directive=true][key.name.name='if']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='model']": V.VDirective - "VAttribute[directive=true][key.name.name='model']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='on']": V.VDirective - "VAttribute[directive=true][key.name.name='on']:exit": V.VDirective + "VAttribute[directive=true][key.name.name='if']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='if']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='model']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='model']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='on']": V.VDirective & { + value: + | (V.VExpressionContainer & { + expression: ES.Expression | V.VOnExpression | null + }) + | null + } + "VAttribute[directive=true][key.name.name='on']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { + expression: ES.Expression | V.VOnExpression | null + }) + | null + } "VAttribute[directive=true][key.name.name='once']": V.VDirective "VAttribute[directive=true][key.name.name='once']:exit": V.VDirective "VAttribute[directive=true][key.name.name='pre']": V.VDirective "VAttribute[directive=true][key.name.name='pre']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='show']": V.VDirective - "VAttribute[directive=true][key.name.name='show']:exit": V.VDirective - "VAttribute[directive=true][key.name.name='slot']": V.VDirective - "VAttribute[directive=true][key.name.name='slot']:exit": V.VDirective + "VAttribute[directive=true][key.name.name='show']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='show']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='slot']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: V.VSlotScopeExpression | null }) + | null + } + "VAttribute[directive=true][key.name.name='slot']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: V.VSlotScopeExpression | null }) + | null + } "VAttribute[directive=true][key.name.name='text']": V.VDirective "VAttribute[directive=true][key.name.name='text']:exit": V.VDirective 'VAttribute[value!=null]':