UNPKG

eslint-plugin-mobx

Version:

ESLint rules for MobX

132 lines (121 loc) 5.17 kB
"use strict" const { findAncestor, isMobxDecorator } = require("./utils.js") // TODO support this.foo = 5; in constructor // TODO? report on field as well function create(context) { const sourceCode = context.getSourceCode() const autofixAnnotation = context.options[0]?.autofixAnnotation ?? true function fieldToKey(field) { // TODO cache on field? const key = sourceCode.getText(field.key) return field.computed ? `[${key}]` : key } return { 'CallExpression[callee.name="makeObservable"]': makeObservable => { // Only interested about makeObservable(this, ...) in constructor or makeObservable({}, ...) // ClassDeclaration // ClassBody // MethodDefinition[kind="constructor"] // FunctionExpression // BlockStatement // ExpressionStatement // CallExpression[callee.name="makeObservable"] const [firstArg, secondArg] = makeObservable.arguments if (!firstArg) return let members if (firstArg.type === "ThisExpression") { const closestFunction = findAncestor( makeObservable, node => node.type === "FunctionExpression" || node.type === "FunctionDeclaration" ) if (closestFunction?.parent?.kind !== "constructor") return members = closestFunction.parent.parent.parent.body.body } else if (firstArg.type === "ObjectExpression") { members = firstArg.properties } else { return } const annotationProps = secondArg?.properties || [] const nonAnnotatedMembers = [] let hasAnyDecorator = false members.forEach(member => { if (member.static) return if (member.kind === "constructor") return //if (member.type !== 'MethodDefinition' && member.type !== 'ClassProperty') return; hasAnyDecorator = hasAnyDecorator || member.decorators?.some(isMobxDecorator) || false if (!annotationProps.some(prop => fieldToKey(prop) === fieldToKey(member))) { // TODO optimize? nonAnnotatedMembers.push(member) } }) /* // With decorators, second arg must be null/undefined or not provided if (hasAnyDecorator && secondArg && secondArg.name !== "undefined" && secondArg.value !== null) { context.report({ node: makeObservable, message: 'When using decorators, second arg must be `null`, `undefined` or not provided.', }) } // Without decorators, in constructor, second arg must be object literal if (!hasAnyDecorator && firstArg.type === 'ThisExpression' && (!secondArg || secondArg.type !== 'ObjectExpression')) { context.report({ node: makeObservable, message: 'Second argument must be object in form of `{ key: annotation }`.', }) } */ if (!hasAnyDecorator && nonAnnotatedMembers.length) { // Set avoids reporting twice for setter+getter pair or actual duplicates const keys = [...new Set(nonAnnotatedMembers.map(fieldToKey))] const keyList = keys.map(key => `\`${key}\``).join(", ") const fix = fixer => { const annotationList = keys.map(key => `${key}: ${autofixAnnotation}`).join(", ") + "," if (!secondArg) { return fixer.insertTextAfter(firstArg, `, { ${annotationList} }`) } else if (secondArg.type !== "ObjectExpression") { return fixer.replaceText(secondArg, `{ ${annotationList} }`) } else { const openingBracket = sourceCode.getFirstToken(secondArg) return fixer.insertTextAfter(openingBracket, ` ${annotationList} `) } } context.report({ node: makeObservable, messageId: "missingAnnotation", data: { keyList }, fix }) } } } } module.exports = { meta: { type: "suggestion", fixable: "code", schema: [ { type: "object", properties: { autofixAnnotation: { type: "boolean" } }, additionalProperties: false } ], docs: { description: "enforce all fields being listen in `makeObservable`", recommended: true, suggestion: false }, messages: { missingAnnotation: "Missing annotation for {{ keyList }}. To exclude a field, use `false` as annotation." } }, create }