Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ Then configure the rules you want to use under the rules section.
// .eslintrc [Legacy Config]
{
"rules": {
"pinia/require-export-define-store": [
"warn"
]
"pinia/require-export-define-store": ["warn"]
}
}
```
Expand All @@ -66,7 +64,7 @@ export default [
pinia
},
rules: {
"pinia/require-export-define-store": ["warn"]
'pinia/require-export-define-store': ['warn']
}
}
]
Expand All @@ -87,9 +85,7 @@ To use the recommended configuration, extend it in your `.eslintrc` or `eslint.c
// eslint.config.js
import pinia from 'eslint-plugin-pinia'

export default [
pinia.configs["recommended-flat"],
]
export default [pinia.configs['recommended-flat']]
```

All recommend rules will be set to error by default. You can however disable some rules by setting turning them `off` in your configuration file or by setting them to `warn` in your configuration file.
Expand All @@ -109,9 +105,7 @@ To use the all configuration, extend it in your `.eslintrc` or `eslint.config.js
// eslint.config.js
import pinia from 'eslint-plugin-pinia'

export default [
pinia.configs["all-flat"],
]
export default [pinia.configs['all-flat']]
```

## Rules
Expand All @@ -132,6 +126,7 @@ export default [
| [no-duplicate-store-ids](docs/rules/no-duplicate-store-ids.md) | Disallow duplicate store ids. | ✅ ✅ | 🌐 🌐 | |
| [no-return-global-properties](docs/rules/no-return-global-properties.md) | Disallows returning globally provided properties from Pinia stores. | ✅ ✅ | 🌐 🌐 | |
| [no-store-to-refs-in-store](docs/rules/no-store-to-refs-in-store.md) | Disallow use of storeToRefs inside defineStore | ✅ ✅ | 🌐 🌐 | |
| [no-unwrapped-store-refs](docs/rules/no-unwrapped-store-refs.md) | Disallow using refs from a store without unwrapping them. | | | |
| [prefer-single-store-per-file](docs/rules/prefer-single-store-per-file.md) | Encourages defining each store in a separate file. | | | 🌐 🌐 |
| [prefer-use-store-naming-convention](docs/rules/prefer-use-store-naming-convention.md) | Enforces the convention of naming stores with the prefix `use` followed by the store name. | | 🌐 🌐 ✅ ✅ | |
| [require-setup-store-properties-export](docs/rules/require-setup-store-properties-export.md) | In setup stores all state properties must be exported. | ✅ ✅ | 🌐 🌐 | |
Expand Down
35 changes: 35 additions & 0 deletions docs/rules/no-unwrapped-store-refs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Disallow using refs from a store without unwrapping them (`pinia/no-unwrapped-store-refs`)

💼⚠️ This rule is enabled in the following configs: ✅ `recommended`, ✅ `recommended-flat`. This rule _warns_ in the following configs: 🌐 `all`, 🌐 `all-flat`.

<!-- end auto-generated rule header -->

Refs are objects that wrap a value. To access the value, you need to use the `.value` property. When using `storeToRefs` from Pinia, you get refs for the state properties of a store. This rule ensures that you always access the value of the ref and not the ref object itself.

## Rule Details

This rule aims to prevent accidental usage of ref objects instead of their values.

Examples of **incorrect** code for this rule:

```js
import { storeToRefs } from 'pinia'
import { useMyStore } from './my-store'

const myStore = useMyStore()
const { myRef } = storeToRefs(myStore)

console.log(myRef)
```

Examples of **correct** code for this rule:

```js
import { storeToRefs } from 'pinia'
import { useMyStore } from './my-store'

const myStore = useMyStore()
const { myRef } = storeToRefs(myStore)

console.log(myRef.value)
```
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { RULE_NAME as preferSingleStoreName } from './rules/prefer-single-store-
import { RULE_NAME as noReturnGlobalPropertiesName } from './rules/no-return-global-properties'
import { RULE_NAME as noDuplicateStoreIdsName } from './rules/no-duplicate-store-ids'
import { RULE_NAME as noStoreToRefs } from './rules/no-store-to-refs-in-store'
import { RULE_NAME as noUnwrappedStoreRefsName } from './rules/no-unwrapped-store-refs'
import rules from './rules/index'

const plugin = {
Expand All @@ -16,6 +17,7 @@ const allRules = {
[noDuplicateStoreIdsName]: 'warn',
[noReturnGlobalPropertiesName]: 'warn',
[noStoreToRefs]: 'warn',
[noUnwrappedStoreRefsName]: 'warn',
[preferNamingConventionName]: 'warn',
[preferSingleStoreName]: 'off',
[requireSetupStorePropsName]: 'warn'
Expand All @@ -26,6 +28,7 @@ const recommended = {
[noDuplicateStoreIdsName]: 'error',
[noReturnGlobalPropertiesName]: 'error',
[noStoreToRefs]: 'error',
[noUnwrappedStoreRefsName]: 'error',
[preferNamingConventionName]: 'warn',
[requireSetupStorePropsName]: 'error'
}
Expand Down
2 changes: 2 additions & 0 deletions src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import neverExportInitializedStore, { RULE_NAME as neverExportInitializedStoreNa
import noDuplicateStoreIds, { RULE_NAME as noDuplicateStoreIdsName } from './no-duplicate-store-ids'
import noReturnGlobalProperties, { RULE_NAME as noReturnGlobalPropertiesName } from './no-return-global-properties'
import noStoreToRefsInStore, { RULE_NAME as noStoreToRefsInStoreName } from './no-store-to-refs-in-store'
import noUnwrappedStoreRefs, { RULE_NAME as noUnwrappedStoreRefsName } from './no-unwrapped-store-refs'
import preferSingleStorePerFile, { RULE_NAME as preferSingleStorePerFileName } from './prefer-single-store-per-file'
import preferUseStoreNamingConvention, { RULE_NAME as preferUseStoreNamingConventionName } from './prefer-use-store-naming-convention'
import requireSetupStorePropertiesExport, { RULE_NAME as requireSetupStorePropertiesExportName } from './require-setup-store-properties-export'
Expand All @@ -12,6 +13,7 @@ export default {
[noDuplicateStoreIdsName]: noDuplicateStoreIds,
[noReturnGlobalPropertiesName]: noReturnGlobalProperties,
[noStoreToRefsInStoreName]: noStoreToRefsInStore,
[noUnwrappedStoreRefsName]: noUnwrappedStoreRefs,
[preferSingleStorePerFileName]: preferSingleStorePerFile,
[preferUseStoreNamingConventionName]: preferUseStoreNamingConvention,
[requireSetupStorePropertiesExportName]: requireSetupStorePropertiesExport,
Expand Down
73 changes: 73 additions & 0 deletions src/rules/no-unwrapped-store-refs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { TSESTree, TSESLint } from '@typescript-eslint/utils'
import { createEslintRule } from '../utils/rule-creator'

export const RULE_NAME = 'no-unwrapped-store-refs'
export type MESSAGE_IDS = 'noUnwrappedStoreRefs'
type Options = []

export default createEslintRule<Options, MESSAGE_IDS>({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description: 'Disallow using refs from a store without unwrapping them.'
},
messages: {
noUnwrappedStoreRefs: 'Refs from a store must be unwrapped to be used.'
},
schema: []
},
defaultOptions: [],

create(context) {
const storeToRefsVariables: TSESLint.Scope.Variable[] = []

return {
VariableDeclarator(node: TSESTree.VariableDeclarator) {
if (
node.init?.type === 'CallExpression' &&
node.init.callee.type === 'Identifier' &&
node.init.callee.name === 'storeToRefs' &&
node.id.type === 'ObjectPattern'
) {
const scope = context.sourceCode.getScope(node)
for (const prop of node.id.properties) {
if (prop.type === 'Property') {
const propValue = prop.value
if (propValue.type === 'Identifier') {
const variable = scope.variables.find(
(v) => v.name === propValue.name
)
if (variable) {
storeToRefsVariables.push(variable)
}
}
}
}
}
},

'Program:exit'() {
for (const variable of storeToRefsVariables) {
for (const reference of variable.references) {
if (reference.init) {
continue
}

const idNode = reference.identifier

if (
idNode.parent.type !== 'MemberExpression' ||
idNode.parent.object !== idNode
) {
context.report({
node: idNode,
messageId: 'noUnwrappedStoreRefs'
})
}
}
}
}
}
}
})
37 changes: 37 additions & 0 deletions tests/rules/no-unwrapped-store-refs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ruleTester } from '../rule-tester'
import rule, { RULE_NAME } from '../../src/rules/no-unwrapped-store-refs'

ruleTester.run(RULE_NAME, rule, {
valid: [
{
code: `
import { storeToRefs } from 'pinia';
import { useMyStore } from './my-store';
const myStore = useMyStore();
const { myRef } = storeToRefs(myStore);
console.log(myRef.value);
`
},
{
code: `
import { storeToRefs } from 'pinia';
import { useMyStore } from './my-store';
const myStore = useMyStore();
const { myRef } = storeToRefs(myStore);
const computedRef = computed(() => myRef.value);
`
}
],
invalid: [
{
code: `
import { storeToRefs } from 'pinia';
import { useMyStore } from './my-store';
const myStore = useMyStore();
const { myRef } = storeToRefs(myStore);
const computedRef = computed(() => myRef);
`,
errors: [{ messageId: 'noUnwrappedStoreRefs' }]
}
]
})