Skip to content

Commit 8b877f7

Browse files
marcjfj-vmlyrFloEdelmann
andauthoredSep 12, 2024··
Vuex and Pinia support for no-undef-properties (#2513)
Co-authored-by: Flo Edelmann <[email protected]>
1 parent 3157fad commit 8b877f7

File tree

2 files changed

+608
-1
lines changed

2 files changed

+608
-1
lines changed
 

‎lib/rules/no-undef-properties.js

+52-1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ module.exports = {
111111
).map(toRegExp)
112112
const propertyReferenceExtractor = definePropertyReferenceExtractor(context)
113113
const programNode = context.getSourceCode().ast
114+
/**
115+
* Property names identified as defined via a Vuex or Pinia helpers
116+
* @type {Set<string>}
117+
*/
118+
const propertiesDefinedByStoreHelpers = new Set()
114119

115120
/**
116121
* @param {ASTNode} node
@@ -185,7 +190,8 @@ module.exports = {
185190
report(node, name, messageId = 'undef') {
186191
if (
187192
reserved.includes(name) ||
188-
ignores.some((ignore) => ignore.test(name))
193+
ignores.some((ignore) => ignore.test(name)) ||
194+
propertiesDefinedByStoreHelpers.has(name)
189195
) {
190196
return
191197
}
@@ -331,6 +337,51 @@ module.exports = {
331337
}
332338
}),
333339
utils.defineVueVisitor(context, {
340+
/**
341+
* @param {CallExpression} node
342+
*/
343+
CallExpression(node) {
344+
if (node.callee.type !== 'Identifier') return
345+
/** @type {'methods'|'computed'|null} */
346+
let groupName = null
347+
if (/^mapMutations|mapActions$/u.test(node.callee.name)) {
348+
groupName = GROUP_METHODS
349+
} else if (
350+
/^mapState|mapGetters|mapWritableState$/u.test(node.callee.name)
351+
) {
352+
groupName = GROUP_COMPUTED_PROPERTY
353+
}
354+
355+
if (!groupName || node.arguments.length === 0) return
356+
// On Pinia the store is always the first argument
357+
const arg =
358+
node.arguments.length === 2 ? node.arguments[1] : node.arguments[0]
359+
if (arg.type === 'ObjectExpression') {
360+
// e.g.
361+
// `mapMutations({ add: 'increment' })`
362+
// `mapState({ count: state => state.todosCount })`
363+
for (const prop of arg.properties) {
364+
const name =
365+
prop.type === 'SpreadElement'
366+
? null
367+
: utils.getStaticPropertyName(prop)
368+
if (name) {
369+
propertiesDefinedByStoreHelpers.add(name)
370+
}
371+
}
372+
} else if (arg.type === 'ArrayExpression') {
373+
// e.g. `mapMutations(['add'])`
374+
for (const element of arg.elements) {
375+
if (!element || !utils.isStringLiteral(element)) {
376+
continue
377+
}
378+
const name = utils.getStringLiteralValue(element)
379+
if (name) {
380+
propertiesDefinedByStoreHelpers.add(name)
381+
}
382+
}
383+
}
384+
},
334385
onVueObjectEnter(node) {
335386
const ctx = getVueComponentContext(node)
336387

‎tests/lib/rules/no-undef-properties.js

+556
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,248 @@ tester.run('no-undef-properties', rule, {
561561
}
562562
}
563563
},
564+
{
565+
// Vuex
566+
filename: 'test.vue',
567+
code: `
568+
<script>
569+
import { mapState } from 'vuex';
570+
571+
export default {
572+
computed: {
573+
...mapState({
574+
a: (vm) => vm.a,
575+
b: (vm) => vm.b,
576+
})
577+
},
578+
methods: {
579+
c() {return this.a * this.b}
580+
}
581+
}
582+
</script>
583+
<template>
584+
{{ a }} {{ b }}
585+
</template>
586+
`
587+
},
588+
{
589+
filename: 'test.vue',
590+
code: `
591+
<script>
592+
import { mapActions } from 'vuex';
593+
594+
export default {
595+
methods: {
596+
...mapActions({
597+
a: 'a',
598+
b: 'b',
599+
}),
600+
c() {return this.a()},
601+
d() {return this.b()},
602+
}
603+
}
604+
</script>
605+
<template>
606+
{{ a }} {{ b }}
607+
</template>
608+
`
609+
},
610+
{
611+
filename: 'test.vue',
612+
code: `
613+
<script>
614+
import { mapMutations } from 'vuex';
615+
616+
export default {
617+
methods: {
618+
...mapMutations({
619+
a: 'a',
620+
b: 'b',
621+
}),
622+
c() {return this.a()},
623+
d() {return this.b()},
624+
}
625+
}
626+
</script>
627+
<template>
628+
{{ a }} {{ b }}
629+
</template>
630+
`
631+
},
632+
{
633+
filename: 'test.vue',
634+
code: `
635+
<script>
636+
import { mapActions } from 'vuex';
637+
638+
export default {
639+
methods: {
640+
...mapActions(['a', 'b']),
641+
c() {return this.a()},
642+
d() {return this.b()},
643+
}
644+
}
645+
</script>
646+
<template>
647+
{{ a }} {{ b }}
648+
</template>
649+
`
650+
},
651+
{
652+
filename: 'test.vue',
653+
code: `
654+
<script>
655+
import { mapMutations } from 'vuex';
656+
657+
export default {
658+
methods: {
659+
...mapMutations(['a', 'b']),
660+
c() {return this.a()},
661+
d() {return this.b()},
662+
}
663+
}
664+
</script>
665+
<template>
666+
{{ a }} {{ b }}
667+
</template>
668+
`
669+
},
670+
{
671+
filename: 'test.vue',
672+
code: `
673+
<script>
674+
import { mapGetters } from 'vuex';
675+
676+
export default {
677+
computed: {
678+
...mapGetters(['a', 'b'])
679+
},
680+
methods: {
681+
c() {return this.a},
682+
d() {return this.b},
683+
}
684+
}
685+
</script>
686+
<template>
687+
{{ a }} {{ b }}
688+
</template>
689+
`
690+
},
691+
{
692+
// Pinia
693+
filename: 'test.vue',
694+
code: `
695+
<script>
696+
import { mapGetters } from 'pinia'
697+
import { useStore } from '../store'
698+
699+
export default {
700+
computed: {
701+
...mapGetters(useStore, ['a', 'b'])
702+
},
703+
methods: {
704+
c() {return this.a},
705+
d() {return this.b},
706+
}
707+
}
708+
</script>
709+
<template>
710+
{{ a }} {{ b }}
711+
</template>
712+
`
713+
},
714+
{
715+
filename: 'test.vue',
716+
code: `
717+
<script>
718+
import { mapState } from 'pinia'
719+
import { useStore } from '../store'
720+
721+
export default {
722+
computed: {
723+
...mapState(useStore, {
724+
a: 'a',
725+
b: store => store.b,
726+
})
727+
},
728+
methods: {
729+
c() {return this.a},
730+
d() {return this.b},
731+
}
732+
}
733+
</script>
734+
<template>
735+
{{ a }} {{ b }}
736+
</template>
737+
`
738+
},
739+
{
740+
filename: 'test.vue',
741+
code: `
742+
<script>
743+
import { mapWritableState } from 'pinia'
744+
import { useStore } from '../store'
745+
746+
export default {
747+
computed: {
748+
...mapWritableState(useStore, {
749+
a: 'a',
750+
b: 'b',
751+
})
752+
},
753+
methods: {
754+
c() {return this.a},
755+
d() {return this.b},
756+
}
757+
}
758+
</script>
759+
<template>
760+
{{ a }} {{ b }}
761+
</template>
762+
`
763+
},
764+
{
765+
filename: 'test.vue',
766+
code: `
767+
<script>
768+
import { mapWritableState } from 'pinia'
769+
import { useStore } from '../store'
770+
771+
export default {
772+
computed: {
773+
...mapWritableState(useStore, ['a', 'b'])
774+
},
775+
methods: {
776+
c() {return this.a},
777+
d() {return this.b},
778+
}
779+
}
780+
</script>
781+
<template>
782+
{{ a }} {{ b }}
783+
</template>
784+
`
785+
},
786+
{
787+
filename: 'test.vue',
788+
code: `
789+
<script>
790+
import { mapActions } from 'pinia'
791+
import { useStore } from '../store'
564792
793+
export default {
794+
methods: {
795+
...mapActions(useStore, ['a', 'b']),
796+
c() {return this.a()},
797+
d() {return this.b()},
798+
}
799+
}
800+
</script>
801+
<template>
802+
{{ a() }} {{ b() }}
803+
</template>
804+
`
805+
},
565806
`
566807
<script setup>
567808
const model = defineModel();
@@ -1214,6 +1455,321 @@ tester.run('no-undef-properties', rule, {
12141455
line: 14
12151456
}
12161457
]
1458+
},
1459+
{
1460+
// Vuex
1461+
filename: 'test.vue',
1462+
code: `
1463+
<script>
1464+
import { mapState } from 'vuex';
1465+
1466+
export default {
1467+
computed: {
1468+
...mapState({
1469+
a: (vm) => vm.a,
1470+
b: (vm) => vm.b,
1471+
})
1472+
},
1473+
methods: {
1474+
c() {return this.a * this.g}
1475+
}
1476+
}
1477+
</script>
1478+
<template>
1479+
{{ a }} {{ b }} {{ c }}
1480+
</template>
1481+
`,
1482+
errors: [
1483+
{
1484+
message: "'g' is not defined.",
1485+
line: 13
1486+
}
1487+
]
1488+
},
1489+
{
1490+
filename: 'test.vue',
1491+
code: `
1492+
<script>
1493+
import { mapActions } from 'vuex';
1494+
1495+
export default {
1496+
methods: {
1497+
...mapActions({
1498+
a: 'a',
1499+
b: 'b',
1500+
}),
1501+
c() {return this.a()},
1502+
d() {return this.f()},
1503+
}
1504+
}
1505+
</script>
1506+
<template>
1507+
{{ a }} {{ b }}
1508+
</template>
1509+
`,
1510+
errors: [
1511+
{
1512+
message: "'f' is not defined.",
1513+
line: 12
1514+
}
1515+
]
1516+
},
1517+
{
1518+
filename: 'test.vue',
1519+
code: `
1520+
<script>
1521+
import { mapMutations } from 'vuex';
1522+
1523+
export default {
1524+
methods: {
1525+
...mapMutations({
1526+
a: 'a',
1527+
b: 'b',
1528+
}),
1529+
c() {return this.a()},
1530+
d() {return this.b()},
1531+
d() {return this.x()},
1532+
}
1533+
}
1534+
</script>
1535+
<template>
1536+
{{ a }} {{ b }}
1537+
</template>
1538+
`,
1539+
errors: [
1540+
{
1541+
message: "'x' is not defined.",
1542+
line: 13
1543+
}
1544+
]
1545+
},
1546+
{
1547+
filename: 'test.vue',
1548+
code: `
1549+
<script>
1550+
import { mapActions } from 'vuex';
1551+
1552+
export default {
1553+
methods: {
1554+
...mapActions(['a', 'b']),
1555+
c() {return this.a()},
1556+
d() {return this.b()},
1557+
d() {return this.f()},
1558+
}
1559+
}
1560+
</script>
1561+
<template>
1562+
{{ a }} {{ b }}
1563+
</template>
1564+
`,
1565+
errors: [
1566+
{
1567+
message: "'f' is not defined.",
1568+
line: 10
1569+
}
1570+
]
1571+
},
1572+
{
1573+
filename: 'test.vue',
1574+
code: `
1575+
<script>
1576+
import { mapMutations } from 'vuex';
1577+
1578+
export default {
1579+
methods: {
1580+
...mapMutations(['a', 'b']),
1581+
c() {return this.a()},
1582+
d() {return this.b()},
1583+
}
1584+
}
1585+
</script>
1586+
<template>
1587+
{{ a }} {{ b }} {{ q }}
1588+
</template>
1589+
`,
1590+
errors: [
1591+
{
1592+
message: "'q' is not defined.",
1593+
line: 14
1594+
}
1595+
]
1596+
},
1597+
{
1598+
filename: 'test.vue',
1599+
code: `
1600+
<script>
1601+
import { mapGetters } from 'vuex';
1602+
1603+
export default {
1604+
computed: {
1605+
...mapGetters(['a', 'b'])
1606+
},
1607+
methods: {
1608+
c() {return this.a},
1609+
d() {return this.b},
1610+
d() {return this.z},
1611+
}
1612+
}
1613+
</script>
1614+
<template>
1615+
{{ a }} {{ b }}
1616+
</template>
1617+
`,
1618+
errors: [
1619+
{
1620+
message: "'z' is not defined.",
1621+
line: 12
1622+
}
1623+
]
1624+
},
1625+
{
1626+
// Pinia
1627+
filename: 'test.vue',
1628+
code: `
1629+
<script>
1630+
import { mapGetters } from 'pinia'
1631+
import { useStore } from '../store'
1632+
1633+
export default {
1634+
computed: {
1635+
...mapGetters(useStore, ['a', 'b'])
1636+
},
1637+
methods: {
1638+
c() {return this.a},
1639+
d() {return this.b},
1640+
d() {return this.z},
1641+
}
1642+
}
1643+
</script>
1644+
<template>
1645+
{{ a }} {{ b }}
1646+
</template>
1647+
`,
1648+
errors: [
1649+
{
1650+
message: "'z' is not defined.",
1651+
line: 13
1652+
}
1653+
]
1654+
},
1655+
{
1656+
filename: 'test.vue',
1657+
code: `
1658+
<script>
1659+
import { mapState } from 'pinia'
1660+
import { useStore } from '../store'
1661+
1662+
export default {
1663+
computed: {
1664+
...mapState(useStore, {
1665+
a: 'a',
1666+
b: store => store.b,
1667+
})
1668+
},
1669+
methods: {
1670+
c() {return this.a},
1671+
d() {return this.b},
1672+
}
1673+
}
1674+
</script>
1675+
<template>
1676+
{{ a }} {{ b }} {{ q }}
1677+
</template>
1678+
`,
1679+
errors: [
1680+
{
1681+
message: "'q' is not defined.",
1682+
line: 20
1683+
}
1684+
]
1685+
},
1686+
{
1687+
filename: 'test.vue',
1688+
code: `
1689+
<script>
1690+
import { mapWritableState } from 'pinia'
1691+
import { useStore } from '../store'
1692+
1693+
export default {
1694+
computed: {
1695+
...mapWritableState(useStore, {
1696+
a: 'a',
1697+
b: 'b',
1698+
})
1699+
},
1700+
methods: {
1701+
c() {return this.a},
1702+
d() {return this.b},
1703+
d() {return this.z},
1704+
}
1705+
}
1706+
</script>
1707+
<template>
1708+
{{ a }} {{ b }}
1709+
</template>
1710+
`,
1711+
errors: [
1712+
{
1713+
message: "'z' is not defined.",
1714+
line: 16
1715+
}
1716+
]
1717+
},
1718+
{
1719+
filename: 'test.vue',
1720+
code: `
1721+
<script>
1722+
import { mapWritableState } from 'pinia'
1723+
import { useStore } from '../store'
1724+
1725+
export default {
1726+
computed: {
1727+
...mapWritableState(useStore, ['a', 'b'])
1728+
},
1729+
methods: {
1730+
c() {return this.a},
1731+
d() {return this.b},
1732+
d() {return this.z},
1733+
}
1734+
}
1735+
</script>
1736+
<template>
1737+
{{ a }} {{ b }}
1738+
</template>
1739+
`,
1740+
errors: [
1741+
{
1742+
message: "'z' is not defined.",
1743+
line: 13
1744+
}
1745+
]
1746+
},
1747+
{
1748+
filename: 'test.vue',
1749+
code: `
1750+
<script>
1751+
import { mapActions } from 'pinia'
1752+
import { useStore } from '../store'
1753+
1754+
export default {
1755+
methods: {
1756+
...mapActions(useStore, ['a', 'b']),
1757+
c() {return this.a()},
1758+
d() {return this.b()},
1759+
d() {return this.x()},
1760+
}
1761+
}
1762+
</script>
1763+
<template>
1764+
{{ a() }} {{ b() }}
1765+
</template>
1766+
`,
1767+
errors: [
1768+
{
1769+
message: "'x' is not defined.",
1770+
line: 11
1771+
}
1772+
]
12171773
}
12181774
]
12191775
})

0 commit comments

Comments
 (0)
Please sign in to comment.