UNPKG

eslint-plugin-pinia

Version:
456 lines (443 loc) 14.6 kB
import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils'; import { isIdentifier } from '@typescript-eslint/utils/ast-utils'; import { resolve } from 'path'; const createEslintRule = ESLintUtils.RuleCreator( (name) => `https://github.com/lisilinhart/eslint-plugin-pinia/blob/main/docs/rules/${name}.md` ); function isRefOrReactiveCall(node) { return !!node && node.type === AST_NODE_TYPES.CallExpression && node.callee.type === "Identifier" && (node.callee.name === "ref" || node.callee.name === "reactive"); } const RULE_NAME$6 = "require-setup-store-properties-export"; const requireSetupStorePropertiesExport = createEslintRule({ name: RULE_NAME$6, meta: { type: "problem", docs: { description: "In setup stores all state properties must be exported." }, schema: [], messages: { missingVariables: "Missing state variable exports in return statement: {{variableNames}}" } }, defaultOptions: [], create: (context) => { return { CallExpression(node) { if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "defineStore" && node.arguments.length === 2 && node.arguments[1].type !== AST_NODE_TYPES.ObjectExpression) { const arrowFunc = node.arguments[1]; if (arrowFunc.body.type !== AST_NODE_TYPES.BlockStatement) return; const declaredStateVariables = arrowFunc.body.body.filter(({ type }) => type === AST_NODE_TYPES.VariableDeclaration).flatMap((declaration) => { return declaration.declarations.filter(({ init, id }) => isRefOrReactiveCall(init) && isIdentifier(id)).map(({ id }) => id.name); }); if (declaredStateVariables.length <= 0) return; const returnStatement = arrowFunc.body.body.find(({ type }) => type === AST_NODE_TYPES.ReturnStatement); if (!returnStatement) { return context.report({ node, messageId: "missingVariables", data: { variableNames: declaredStateVariables.join(", ") } }); } const returnedVariables = returnStatement?.argument?.type === AST_NODE_TYPES.ObjectExpression ? returnStatement.argument.properties.flatMap( (property) => property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.Identifier ? [property.value.name] : [] ) : []; const missingVariables = declaredStateVariables.filter( (variable) => !returnedVariables.includes(variable) ); if (missingVariables.length > 0) { context.report({ node: returnStatement, messageId: "missingVariables", data: { variableNames: missingVariables.join(", ") } }); } } } }; } }); const RULE_NAME$5 = "never-export-initialized-store"; const storeIds = /* @__PURE__ */ new Set(); const neverExportInitializedStore = createEslintRule({ name: RULE_NAME$5, meta: { type: "problem", docs: { description: "Never export an initialized named or default store." }, schema: [], messages: { namedInitialization: "Never export an initialized store: {{storeName}}. Use inject/import instead where it is used.", defaultInitialization: "Never export default initialized store. Use inject/import instead where it is used." } }, defaultOptions: [], create: (context) => { return { CallExpression(node) { if (node.callee.type === "Identifier" && node.callee.name === "defineStore" && node.arguments.length >= 2 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string" && node.parent.id.type === "Identifier") { const callee = node.callee; if (callee.type !== "Identifier" || callee.name !== "defineStore") return; const storeId = node.arguments && node.arguments[0]; if (!storeId || storeId.type !== AST_NODE_TYPES.Literal) return; const value = node.parent.id.name; storeIds.add(value); } }, ExportDefaultDeclaration(node) { if (storeIds.has(node.declaration?.parent?.declaration?.callee?.name)) { context.report({ node, messageId: "defaultInitialization" }); } }, ExportNamedDeclaration(node) { if (node?.declaration?.type === "VariableDeclaration") { node?.declaration?.declarations.forEach((declaration) => { if (storeIds.has(declaration?.init?.callee?.name)) { context.report({ node, messageId: "namedInitialization", data: { storeName: declaration?.init?.callee?.name } }); } }); } } }; } }); const RULE_NAME$4 = "prefer-use-store-naming-convention"; const preferUseStoreNamingConvention = createEslintRule({ name: RULE_NAME$4, meta: { type: "problem", docs: { description: "Enforces the convention of naming stores with the prefix `use` followed by the store name." }, schema: [ { type: "object", properties: { checkStoreNameMismatch: { type: "boolean", default: false }, storeSuffix: { type: "string", default: "" } }, additionalProperties: false } ], messages: { incorrectPrefix: 'Store names should start with "use" followed by the store name.', incorrectSuffix: 'Store names should end with "{{ suffixName }}".', storeNameMismatch: 'The "{{name}}" variable naming does not match the unique identifier "{{id}}" naming for the store.' } }, defaultOptions: [ { checkStoreNameMismatch: false, storeSuffix: "" } ], create: (context, options) => { return { CallExpression(node) { if (node.callee.type === "Identifier" && node.callee.name === "defineStore" && node.arguments.length >= 2 && node.arguments[0].type === "Literal" && typeof node.arguments[0].value === "string" && node.parent.id.type === "Identifier") { const { checkStoreNameMismatch, storeSuffix } = options[0]; const uniqueId = node.arguments[0].value; const hasSuffixConfigured = storeSuffix.length > 0; const expectedName = `use${uniqueId.charAt(0).toUpperCase()}${uniqueId.slice(1)}${storeSuffix}`; const variableName = node.parent.id.name; if (!variableName.startsWith("use")) { context.report({ node: node.parent, messageId: "incorrectPrefix" }); } if (hasSuffixConfigured && !variableName.endsWith(storeSuffix)) { context.report({ node: node.parent, messageId: "incorrectSuffix", data: { suffixName: storeSuffix } }); } if (checkStoreNameMismatch && variableName !== expectedName) { context.report({ node: node.arguments[0], messageId: "storeNameMismatch", data: { name: variableName, id: uniqueId } }); } } } }; } }); const RULE_NAME$3 = "prefer-single-store-per-file"; const preferSingleStorePerFile = createEslintRule({ name: RULE_NAME$3, meta: { type: "problem", docs: { description: "Encourages defining each store in a separate file." }, schema: [], messages: { multipleStores: "Only one store definition per file is allowed." } }, defaultOptions: [], create: (context) => { let storeDeclaration = null; return { Program() { storeDeclaration = null; }, CallExpression(node) { const callee = node.callee; if (callee.type === "Identifier" && callee.name === "defineStore") { if (!storeDeclaration) { storeDeclaration = node; } else { context.report({ messageId: "multipleStores", node: storeDeclaration }); } } } }; } }); const RULE_NAME$2 = "no-return-global-properties"; const noReturnGlobalProperties = createEslintRule({ name: RULE_NAME$2, meta: { type: "problem", docs: { description: "Disallows returning globally provided properties from Pinia stores." }, schema: [], messages: { returnGlobalProperties: "Do not return properties like {{property}} as they are globally available and should not be returned from stores." } }, defaultOptions: [], create: (context) => { const variablesUsingGlobalCallee = /* @__PURE__ */ new Set(); return { VariableDeclaration(node) { node.declarations.forEach((declaration) => { if (declaration.init && declaration.init.type === "CallExpression") { const calleeName = declaration.init.callee.name; if (calleeName === "useRoute" || calleeName === "inject") variablesUsingGlobalCallee.add(declaration.id.name); } }); }, ReturnStatement(node) { const { argument } = node; if (argument && argument.type === "ObjectExpression") { const { properties } = argument; if (!properties) return; properties.forEach((property) => { if (variablesUsingGlobalCallee.has(property?.value?.name)) { context.report({ messageId: "returnGlobalProperties", node: property, data: { property: property?.value?.name } }); } }); } } }; } }); const RULE_NAME$1 = "no-duplicate-store-ids"; const storeIdsCache = /* @__PURE__ */ new Map(); const noDuplicateStoreIds = createEslintRule({ name: RULE_NAME$1, meta: { type: "problem", docs: { description: "Disallow duplicate store ids." }, schema: [], messages: { duplicatedStoreIds: "No duplicated store ids allowed: {{storeId}}" } }, defaultOptions: [], create: (context) => { const filepath = resolve(context.physicalFilename ?? context.filename); let crtStoreIds = storeIdsCache.get(filepath); if (!crtStoreIds) { crtStoreIds = /* @__PURE__ */ new Set(); storeIdsCache.set(filepath, crtStoreIds); } else { crtStoreIds.clear(); } return { CallExpression(node) { const callee = node.callee; const storeId = node.arguments?.[0]; if (callee.type !== AST_NODE_TYPES.Identifier || callee.name !== "defineStore") return; if (!storeId || storeId.type !== AST_NODE_TYPES.Literal) return; const value = storeId.value; if (crtStoreIds.has(value)) { reportError(); return; } else { crtStoreIds.add(value); } for (const [key, ids] of storeIdsCache) { if (key !== filepath && ids.has(value)) { reportError(); return; } } function reportError() { context.report({ node: storeId, messageId: "duplicatedStoreIds", data: { storeId: value } }); } } }; } }); const RULE_NAME = "no-store-to-refs-in-store"; const noStoreToRefsInStore = createEslintRule({ name: RULE_NAME, meta: { type: "problem", docs: { description: "Disallow use of storeToRefs inside defineStore" }, schema: [], messages: { storeToRefs: "Do not use storeToRefs in other stores. Use the store as a whole directly." } }, defaultOptions: [], create: (context) => { return { CallExpression(node) { if (node.callee.type === "Identifier" && node.callee.name === "defineStore" && node.arguments.length >= 2) { const functionBody = node.arguments[1]; if (functionBody.type === "ArrowFunctionExpression" || functionBody.type === "FunctionExpression") { const body = functionBody.body; if (body.type === "BlockStatement") { body.body.forEach((statement) => { if (statement.type === "VariableDeclaration" && statement.declarations.length > 0) { statement.declarations.forEach((declaration) => { if (declaration.init && declaration.init.type === "CallExpression" && declaration.init.callee.type === "Identifier" && declaration.init.callee.name === "storeToRefs") { context.report({ node: declaration.init.callee, messageId: "storeToRefs" }); } }); } }); } } } } }; } }); const rules = { [RULE_NAME$5]: neverExportInitializedStore, [RULE_NAME$1]: noDuplicateStoreIds, [RULE_NAME$2]: noReturnGlobalProperties, [RULE_NAME]: noStoreToRefsInStore, [RULE_NAME$3]: preferSingleStorePerFile, [RULE_NAME$4]: preferUseStoreNamingConvention, [RULE_NAME$6]: requireSetupStorePropertiesExport }; const plugin = { rules }; const allRules = { [RULE_NAME$5]: "warn", [RULE_NAME$1]: "warn", [RULE_NAME$2]: "warn", [RULE_NAME]: "warn", [RULE_NAME$4]: "warn", [RULE_NAME$3]: "off", [RULE_NAME$6]: "warn" }; const recommended = { [RULE_NAME$5]: "error", [RULE_NAME$1]: "error", [RULE_NAME$2]: "error", [RULE_NAME]: "error", [RULE_NAME$4]: "warn", [RULE_NAME$6]: "error" }; function createConfig(_rules, flat = false) { const name = "pinia"; const constructedRules = Object.keys( _rules ).reduce((acc, ruleName) => { return { ...acc, [`${name}/${ruleName}`]: _rules[ruleName] }; }, {}); if (flat) { return { plugins: { [name]: plugin }, rules: constructedRules }; } else { return { plugins: [name], rules: constructedRules }; } } const configs = { all: createConfig(allRules), recommended: createConfig(recommended), "all-flat": createConfig(allRules, true), "recommended-flat": createConfig(recommended, true) }; const index = { ...plugin, configs }; export { index as default };