diff --git a/lib/rules/no-undef-properties.js b/lib/rules/no-undef-properties.js index 29fbb714b..711c2ed22 100644 --- a/lib/rules/no-undef-properties.js +++ b/lib/rules/no-undef-properties.js @@ -111,6 +111,11 @@ module.exports = { ).map(toRegExp) const propertyReferenceExtractor = definePropertyReferenceExtractor(context) const programNode = context.getSourceCode().ast + /** + * Property names identified as defined via a Vuex or Pinia helpers + * @type {Set<string>} + */ + const propertiesDefinedByStoreHelpers = new Set() /** * @param {ASTNode} node @@ -185,7 +190,8 @@ module.exports = { report(node, name, messageId = 'undef') { if ( reserved.includes(name) || - ignores.some((ignore) => ignore.test(name)) + ignores.some((ignore) => ignore.test(name)) || + propertiesDefinedByStoreHelpers.has(name) ) { return } @@ -331,6 +337,51 @@ module.exports = { } }), utils.defineVueVisitor(context, { + /** + * @param {CallExpression} node + */ + CallExpression(node) { + if (node.callee.type !== 'Identifier') return + /** @type {'methods'|'computed'|null} */ + let groupName = null + if (/^mapMutations|mapActions$/u.test(node.callee.name)) { + groupName = GROUP_METHODS + } else if ( + /^mapState|mapGetters|mapWritableState$/u.test(node.callee.name) + ) { + groupName = GROUP_COMPUTED_PROPERTY + } + + if (!groupName || node.arguments.length === 0) return + // On Pinia the store is always the first argument + const arg = + node.arguments.length === 2 ? node.arguments[1] : node.arguments[0] + if (arg.type === 'ObjectExpression') { + // e.g. + // `mapMutations({ add: 'increment' })` + // `mapState({ count: state => state.todosCount })` + for (const prop of arg.properties) { + const name = + prop.type === 'SpreadElement' + ? null + : utils.getStaticPropertyName(prop) + if (name) { + propertiesDefinedByStoreHelpers.add(name) + } + } + } else if (arg.type === 'ArrayExpression') { + // e.g. `mapMutations(['add'])` + for (const element of arg.elements) { + if (!element || !utils.isStringLiteral(element)) { + continue + } + const name = utils.getStringLiteralValue(element) + if (name) { + propertiesDefinedByStoreHelpers.add(name) + } + } + } + }, onVueObjectEnter(node) { const ctx = getVueComponentContext(node) diff --git a/tests/lib/rules/no-undef-properties.js b/tests/lib/rules/no-undef-properties.js index 83812a248..63313b4b0 100644 --- a/tests/lib/rules/no-undef-properties.js +++ b/tests/lib/rules/no-undef-properties.js @@ -561,7 +561,248 @@ tester.run('no-undef-properties', rule, { } } }, + { + // Vuex + filename: 'test.vue', + code: ` + <script> + import { mapState } from 'vuex'; + + export default { + computed: { + ...mapState({ + a: (vm) => vm.a, + b: (vm) => vm.b, + }) + }, + methods: { + c() {return this.a * this.b} + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapActions } from 'vuex'; + + export default { + methods: { + ...mapActions({ + a: 'a', + b: 'b', + }), + c() {return this.a()}, + d() {return this.b()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapMutations } from 'vuex'; + + export default { + methods: { + ...mapMutations({ + a: 'a', + b: 'b', + }), + c() {return this.a()}, + d() {return this.b()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapActions } from 'vuex'; + + export default { + methods: { + ...mapActions(['a', 'b']), + c() {return this.a()}, + d() {return this.b()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapMutations } from 'vuex'; + + export default { + methods: { + ...mapMutations(['a', 'b']), + c() {return this.a()}, + d() {return this.b()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapGetters } from 'vuex'; + + export default { + computed: { + ...mapGetters(['a', 'b']) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + // Pinia + filename: 'test.vue', + code: ` + <script> + import { mapGetters } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapGetters(useStore, ['a', 'b']) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapState } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapState(useStore, { + a: 'a', + b: store => store.b, + }) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapWritableState } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapWritableState(useStore, { + a: 'a', + b: 'b', + }) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapWritableState } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapWritableState(useStore, ['a', 'b']) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + ` + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapActions } from 'pinia' + import { useStore } from '../store' + export default { + methods: { + ...mapActions(useStore, ['a', 'b']), + c() {return this.a()}, + d() {return this.b()}, + } + } + </script> + <template> + {{ a() }} {{ b() }} + </template> + ` + }, ` <script setup> const model = defineModel(); @@ -1214,6 +1455,321 @@ tester.run('no-undef-properties', rule, { line: 14 } ] + }, + { + // Vuex + filename: 'test.vue', + code: ` + <script> + import { mapState } from 'vuex'; + + export default { + computed: { + ...mapState({ + a: (vm) => vm.a, + b: (vm) => vm.b, + }) + }, + methods: { + c() {return this.a * this.g} + } + } + </script> + <template> + {{ a }} {{ b }} {{ c }} + </template> + `, + errors: [ + { + message: "'g' is not defined.", + line: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapActions } from 'vuex'; + + export default { + methods: { + ...mapActions({ + a: 'a', + b: 'b', + }), + c() {return this.a()}, + d() {return this.f()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'f' is not defined.", + line: 12 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapMutations } from 'vuex'; + + export default { + methods: { + ...mapMutations({ + a: 'a', + b: 'b', + }), + c() {return this.a()}, + d() {return this.b()}, + d() {return this.x()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'x' is not defined.", + line: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapActions } from 'vuex'; + + export default { + methods: { + ...mapActions(['a', 'b']), + c() {return this.a()}, + d() {return this.b()}, + d() {return this.f()}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'f' is not defined.", + line: 10 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapMutations } from 'vuex'; + + export default { + methods: { + ...mapMutations(['a', 'b']), + c() {return this.a()}, + d() {return this.b()}, + } + } + </script> + <template> + {{ a }} {{ b }} {{ q }} + </template> + `, + errors: [ + { + message: "'q' is not defined.", + line: 14 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapGetters } from 'vuex'; + + export default { + computed: { + ...mapGetters(['a', 'b']) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + d() {return this.z}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'z' is not defined.", + line: 12 + } + ] + }, + { + // Pinia + filename: 'test.vue', + code: ` + <script> + import { mapGetters } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapGetters(useStore, ['a', 'b']) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + d() {return this.z}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'z' is not defined.", + line: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapState } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapState(useStore, { + a: 'a', + b: store => store.b, + }) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + } + } + </script> + <template> + {{ a }} {{ b }} {{ q }} + </template> + `, + errors: [ + { + message: "'q' is not defined.", + line: 20 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapWritableState } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapWritableState(useStore, { + a: 'a', + b: 'b', + }) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + d() {return this.z}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'z' is not defined.", + line: 16 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapWritableState } from 'pinia' + import { useStore } from '../store' + + export default { + computed: { + ...mapWritableState(useStore, ['a', 'b']) + }, + methods: { + c() {return this.a}, + d() {return this.b}, + d() {return this.z}, + } + } + </script> + <template> + {{ a }} {{ b }} + </template> + `, + errors: [ + { + message: "'z' is not defined.", + line: 13 + } + ] + }, + { + filename: 'test.vue', + code: ` + <script> + import { mapActions } from 'pinia' + import { useStore } from '../store' + + export default { + methods: { + ...mapActions(useStore, ['a', 'b']), + c() {return this.a()}, + d() {return this.b()}, + d() {return this.x()}, + } + } + </script> + <template> + {{ a() }} {{ b() }} + </template> + `, + errors: [ + { + message: "'x' is not defined.", + line: 11 + } + ] } ] })