eslint-plugin-mobx 0.0.0 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,34 +1,52 @@
1
1
  {
2
- "name": "eslint-plugin-mobx",
3
- "version": "0.0.0",
4
- "description": "ESLint rules for MobX",
5
- "keywords": [
6
- "eslint",
7
- "eslintplugin",
8
- "eslint-plugin",
9
- "mobx"
10
- ],
11
- "author": "Andrey Osiyuk <osiyuk@protonmail.com> (https://twitter.com/osiyuk)",
12
- "main": "lib/index.js",
13
- "scripts": {
14
- "test": "mocha tests --recursive",
15
- "test:watch": "mocha --watch tests --recursive"
16
- },
17
- "dependencies": {},
18
- "devDependencies": {
19
- "eslint": "^4.0.0 | ^5.0.0 | ^6.0.0",
20
- "eslint-config-prettier": "^6.10.1",
21
- "eslint-plugin-prettier": "^3.1.3",
22
- "husky": "^4.2.5",
23
- "lint-staged": "^10.1.3",
24
- "mocha": "^3.1.2",
25
- "prettier": "^2.0.4"
26
- },
27
- "peerDependencies": {
28
- "eslint": "^4.0.0 | ^5.0.0 | ^6.0.0"
29
- },
30
- "engines": {
31
- "node": ">=10"
32
- },
33
- "license": "MIT"
2
+ "name": "eslint-plugin-mobx",
3
+ "version": "0.0.5",
4
+ "description": "ESLint rules for MobX",
5
+ "main": "dist/index.js",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mobxjs/mobx.git",
9
+ "directory": "packages/eslint-plugin-mobx"
10
+ },
11
+ "license": "MIT",
12
+ "funding": {
13
+ "type": "opencollective",
14
+ "url": "https://opencollective.com/mobx"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/mobxjs/mobx/issues"
18
+ },
19
+ "files": [
20
+ "src",
21
+ "dist",
22
+ "LICENSE",
23
+ "CHANGELOG.md",
24
+ "README.md"
25
+ ],
26
+ "homepage": "https://mobx.js.org/",
27
+ "peerDependencies": {
28
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@typescript-eslint/eslint-plugin": "^4.29.3",
32
+ "@typescript-eslint/parser": "^4.0.0",
33
+ "eslint": "^7.0.0",
34
+ "@babel/core": "^7.16.0",
35
+ "@babel/preset-env": "^7.16.4",
36
+ "rollup": "^2.60.2",
37
+ "@rollup/plugin-babel": "^5.3.0",
38
+ "@rollup/plugin-commonjs": "^21.0.1",
39
+ "@rollup/plugin-node-resolve": "13.0.6"
40
+ },
41
+ "keywords": [
42
+ "eslint",
43
+ "eslint-plugin",
44
+ "eslintplugin",
45
+ "mobx"
46
+ ],
47
+ "scripts": {
48
+ "test": "jest",
49
+ "build": "yarn rollup --config",
50
+ "prepublish": "yarn build"
51
+ }
34
52
  }
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const { findAncestor, isMobxDecorator } = require('./utils.js');
4
+
5
+ // TODO support this.foo = 5; in constructor
6
+ // TODO? report on field as well
7
+ function create(context) {
8
+ const sourceCode = context.getSourceCode();
9
+
10
+ function fieldToKey(field) {
11
+ // TODO cache on field?
12
+ const key = sourceCode.getText(field.key);
13
+ return field.computed ? `[${key}]` : key;
14
+ }
15
+
16
+ return {
17
+ 'CallExpression[callee.name="makeObservable"]': makeObservable => {
18
+ // Only interested about makeObservable(this, ...) in constructor or makeObservable({}, ...)
19
+ // ClassDeclaration
20
+ // ClassBody
21
+ // MethodDefinition[kind="constructor"]
22
+ // FunctionExpression
23
+ // BlockStatement
24
+ // ExpressionStatement
25
+ // CallExpression[callee.name="makeObservable"]
26
+ const [firstArg, secondArg] = makeObservable.arguments;
27
+ if (!firstArg) return;
28
+ let members;
29
+ if (firstArg.type === 'ThisExpression') {
30
+ const closestFunction = findAncestor(makeObservable, node => node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration')
31
+ if (closestFunction?.parent?.kind !== 'constructor') return;
32
+ members = closestFunction.parent.parent.parent.body.body;
33
+ } else if (firstArg.type === 'ObjectExpression') {
34
+ members = firstArg.properties;
35
+ } else {
36
+ return;
37
+ }
38
+
39
+ const annotationProps = secondArg?.properties || [];
40
+ const nonAnnotatedMembers = [];
41
+ let hasAnyDecorator = false;
42
+
43
+ members.forEach(member => {
44
+ if (member.static) return;
45
+ if (member.kind === "constructor") return;
46
+ //if (member.type !== 'MethodDefinition' && member.type !== 'ClassProperty') return;
47
+ hasAnyDecorator = hasAnyDecorator || member.decorators?.some(isMobxDecorator) || false;
48
+ if (!annotationProps.some(prop => fieldToKey(prop) === fieldToKey(member))) { // TODO optimize?
49
+ nonAnnotatedMembers.push(member);
50
+ }
51
+ })
52
+ /*
53
+ // With decorators, second arg must be null/undefined or not provided
54
+ if (hasAnyDecorator && secondArg && secondArg.name !== "undefined" && secondArg.value !== null) {
55
+ context.report({
56
+ node: makeObservable,
57
+ message: 'When using decorators, second arg must be `null`, `undefined` or not provided.',
58
+ })
59
+ }
60
+ // Without decorators, in constructor, second arg must be object literal
61
+ if (!hasAnyDecorator && firstArg.type === 'ThisExpression' && (!secondArg || secondArg.type !== 'ObjectExpression')) {
62
+ context.report({
63
+ node: makeObservable,
64
+ message: 'Second argument must be object in form of `{ key: annotation }`.',
65
+ })
66
+ }
67
+ */
68
+
69
+ if (!hasAnyDecorator && nonAnnotatedMembers.length) {
70
+ // Set avoids reporting twice for setter+getter pair or actual duplicates
71
+ const keys = [...new Set(nonAnnotatedMembers.map(fieldToKey))];
72
+ const keyList = keys.map(key => `\`${key}\``).join(', ');
73
+
74
+ const fix = fixer => {
75
+ const annotationList = keys.map(key => `${key}: true`).join(', ') + ',';
76
+ if (!secondArg) {
77
+ return fixer.insertTextAfter(firstArg, `, { ${annotationList} }`);
78
+ } else if (secondArg.type !== 'ObjectExpression') {
79
+ return fixer.replaceText(secondArg, `{ ${annotationList} }`);
80
+ } else {
81
+ const openingBracket = sourceCode.getFirstToken(secondArg)
82
+ return fixer.insertTextAfter(openingBracket, ` ${annotationList} `);
83
+ }
84
+ };
85
+
86
+ context.report({
87
+ node: makeObservable,
88
+ messageId: 'missingAnnotation',
89
+ data: { keyList },
90
+ fix,
91
+ })
92
+ }
93
+ },
94
+ };
95
+ }
96
+
97
+ module.exports = {
98
+ meta: {
99
+ type: 'suggestion',
100
+ fixable: 'code',
101
+ docs: {
102
+ description: 'enforce all fields being listen in `makeObservable`',
103
+ recommended: true,
104
+ suggestion: false,
105
+ },
106
+ messages: {
107
+ 'missingAnnotation': 'Missing annotation for {{ keyList }}. To exclude a field, use `false` as annotation.',
108
+ },
109
+ },
110
+ create,
111
+ };
package/src/index.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const exhaustiveMakeObservable = require('./exhaustive-make-observable.js');
4
+ const unconditionalMakeObservable = require('./unconditional-make-observable.js');
5
+ const missingMakeObservable = require('./missing-make-observable.js');
6
+ const missingObserver = require('./missing-observer');
7
+ const noAnonymousObserver = require('./no-anonymous-observer.js');
8
+
9
+ module.exports = {
10
+ configs: {
11
+ recommended: {
12
+ plugins: ['mobx'],
13
+ rules: {
14
+ 'mobx/exhaustive-make-observable': 'warn',
15
+ 'mobx/unconditional-make-observable': 'error',
16
+ 'mobx/missing-make-observable': 'error',
17
+ 'mobx/no-missing-observer': 'warn',
18
+ 'mobx/no-anonymous-observer': 'warn',
19
+ },
20
+ },
21
+ },
22
+ rules: {
23
+ 'exhaustive-make-observable': exhaustiveMakeObservable,
24
+ 'unconditional-make-observable': unconditionalMakeObservable,
25
+ 'missing-make-observable': missingMakeObservable,
26
+ 'missing-observer': missingObserver,
27
+ 'no-anonymous-observer': noAnonymousObserver,
28
+ }
29
+ }
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const { findAncestor, isMobxDecorator } = require('./utils.js');
4
+
5
+ function create(context) {
6
+ const sourceCode = context.getSourceCode();
7
+
8
+ return {
9
+ 'Decorator': decorator => {
10
+ if (!isMobxDecorator(decorator)) return;
11
+ const clazz = findAncestor(decorator, node => node.type === 'ClassDeclaration' || node.type === 'ClassExpression');
12
+ if (!clazz) return;
13
+ // ClassDeclaration > ClassBody > []
14
+ const constructor = clazz.body.body.find(node => node.kind === 'constructor');
15
+ // MethodDefinition > FunctionExpression > BlockStatement > []
16
+ const isMakeObservable = node => node.expression?.callee?.name === 'makeObservable' && node.expression?.arguments[0]?.type === 'ThisExpression';
17
+ const makeObservable = constructor?.value.body?.body.find(isMakeObservable)?.expression;
18
+
19
+ if (makeObservable) {
20
+ // make sure second arg is nullish
21
+ const secondArg = makeObservable.arguments[1];
22
+ if (secondArg && secondArg.value !== null && secondArg.name !== 'undefined') {
23
+ context.report({
24
+ node: makeObservable,
25
+ messageId: 'secondArgMustBeNullish',
26
+ })
27
+ }
28
+ } else {
29
+ const fix = fixer => {
30
+ if (constructor?.value.type === 'TSEmptyBodyFunctionExpression') {
31
+ // constructor() - yes this a thing
32
+ const closingBracket = sourceCode.getLastToken(constructor.value);
33
+ return fixer.insertTextAfter(closingBracket, ' { makeObservable(this); }')
34
+ } else if (constructor) {
35
+ // constructor() {}
36
+ const closingBracket = sourceCode.getLastToken(constructor.value.body);
37
+ return fixer.insertTextBefore(closingBracket, ';makeObservable(this);')
38
+ } else {
39
+ // class C {}
40
+ const openingBracket = sourceCode.getFirstToken(clazz.body);
41
+ return fixer.insertTextAfter(openingBracket, '\nconstructor() { makeObservable(this); }')
42
+ }
43
+ };
44
+
45
+ context.report({
46
+ node: clazz,
47
+ messageId: 'missingMakeObservable',
48
+ fix,
49
+ })
50
+ }
51
+ },
52
+ };
53
+ }
54
+
55
+ module.exports = {
56
+ meta: {
57
+ type: 'problem',
58
+ fixable: 'code',
59
+ docs: {
60
+ description: 'prevents missing `makeObservable(this)` when using decorators',
61
+ recommended: true,
62
+ suggestion: false,
63
+ },
64
+ messages: {
65
+ missingMakeObservable: "Constructor is missing `makeObservable(this)`.",
66
+ secondArgMustBeNullish: "`makeObservable`'s second argument must be nullish or not provided when using decorators."
67
+ },
68
+ },
69
+ create,
70
+ };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ function create(context) {
4
+ const sourceCode = context.getSourceCode();
5
+
6
+ return {
7
+ 'FunctionDeclaration,FunctionExpression,ArrowFunctionExpression,ClassDeclaration,ClassExpression': cmp => {
8
+ // Already has observer
9
+ if (cmp.parent && cmp.parent.type === 'CallExpression' && cmp.parent.callee.name === 'observer') return;
10
+ let name = cmp.id?.name;
11
+ // If anonymous try to infer name from variable declaration
12
+ if (!name && cmp.parent?.type === 'VariableDeclarator') {
13
+ name = cmp.parent.id.name;
14
+ }
15
+ if (cmp.type.startsWith('Class')) {
16
+ // Must extend Component or React.Component
17
+ const { superClass } = cmp;
18
+ if (!superClass) return;
19
+ const superClassText = sourceCode.getText(superClass);
20
+ if (superClassText !== 'Component' && superClassText !== 'React.Component') return;
21
+ } else {
22
+ // Name must start with uppercase letter
23
+ if (!name?.charAt(0).match(/^[A-Z]$/)) return;
24
+ }
25
+
26
+ const fix = fixer => {
27
+ return [
28
+ fixer.insertTextBefore(
29
+ sourceCode.getFirstToken(cmp),
30
+ (name && cmp.type.endsWith('Declaration') ? `const ${name} = ` : '') + 'observer(',
31
+ ),
32
+ fixer.insertTextAfter(
33
+ sourceCode.getLastToken(cmp),
34
+ ')',
35
+ ),
36
+ ]
37
+ }
38
+ context.report({
39
+ node: cmp,
40
+ messageId: 'missingObserver',
41
+ data: {
42
+ name: name || '<anonymous>',
43
+ },
44
+ fix,
45
+ })
46
+ },
47
+ };
48
+ }
49
+
50
+ module.exports = {
51
+ meta: {
52
+ type: 'problem',
53
+ fixable: 'code',
54
+ docs: {
55
+ description: 'prevents missing `observer` on react component',
56
+ recommended: true,
57
+ },
58
+ messages: {
59
+ missingObserver: "Component `{{ name }}` is missing `observer`.",
60
+ },
61
+ },
62
+ create,
63
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ function create(context) {
4
+ const sourceCode = context.getSourceCode();
5
+
6
+ return {
7
+ 'CallExpression[callee.name="observer"]': observer => {
8
+ const cmp = observer.arguments[0];
9
+ if (!cmp) return;
10
+ if (cmp?.id?.name) return;
11
+
12
+ const fix = fixer => {
13
+ // Use name from variable for autofix
14
+ const name = observer.parent?.type === 'VariableDeclarator'
15
+ ? observer.parent.id.name
16
+ : undefined;
17
+
18
+ if (!name) return;
19
+ if (cmp.type === 'ArrowFunctionExpression') {
20
+ const arrowToken = sourceCode.getTokenBefore(cmp.body);
21
+ return [
22
+ fixer.replaceText(arrowToken, ''),
23
+ fixer.insertTextBefore(cmp, `function ${name}`),
24
+ ]
25
+ }
26
+ if (cmp.type === 'FunctionExpression') {
27
+ const functionToken = sourceCode.getFirstToken(cmp);
28
+ return fixer.replaceText(functionToken, `function ${name}`);
29
+ }
30
+ if (cmp.type === 'ClassExpression') {
31
+ const classToken = sourceCode.getFirstToken(cmp);
32
+ return fixer.replaceText(classToken, `class ${name}`);
33
+ }
34
+ }
35
+ context.report({
36
+ node: cmp,
37
+ messageId: 'observerComponentMustHaveName',
38
+ fix,
39
+ })
40
+ },
41
+ };
42
+ }
43
+
44
+ module.exports = {
45
+ meta: {
46
+ type: 'problem',
47
+ fixable: 'code',
48
+ docs: {
49
+ description: 'forbids anonymous functions or classes as `observer` components',
50
+ recommended: true,
51
+ },
52
+ messages: {
53
+ observerComponentMustHaveName: "`observer` component must have a name.",
54
+ },
55
+ },
56
+ create,
57
+ };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const { findAncestor } = require('./utils.js');
4
+
5
+ function create(context) {
6
+ return {
7
+ 'CallExpression[callee.name=/(makeObservable|makeAutoObservable)/]': makeObservable => {
8
+ // Only iterested about makeObservable(this, ...) inside constructor and not inside nested bindable function
9
+ const [firstArg] = makeObservable.arguments;
10
+ if (!firstArg) return;
11
+ if (firstArg.type !== 'ThisExpression') return;
12
+ // MethodDefinition[key.name="constructor"][kind="constructor"]
13
+ // FunctionExpression
14
+ // BlockStatement
15
+ // ExpressionStatement
16
+ // CallExpression[callee.name="makeObservable"]
17
+ const closestFunction = findAncestor(makeObservable, node => node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration');
18
+ if (closestFunction?.parent?.kind !== 'constructor') return;
19
+ if (makeObservable.parent.parent.parent !== closestFunction) {
20
+ context.report({
21
+ node: makeObservable,
22
+ messageId: 'mustCallUnconditionally',
23
+ data: {
24
+ name: makeObservable.callee.name,
25
+ }
26
+ });
27
+ }
28
+ },
29
+ };
30
+ }
31
+
32
+ module.exports = {
33
+ meta: {
34
+ type: 'problem',
35
+ docs: {
36
+ description: 'disallows calling `makeObservable(this)` conditionally inside constructors',
37
+ recommended: true,
38
+ },
39
+ messages: {
40
+ mustCallUnconditionally: '`{{ name }}` must be called unconditionally inside constructor.',
41
+ }
42
+ },
43
+ create,
44
+ }
package/src/utils.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const mobxDecorators = new Set(['observable', 'computed', 'action', 'flow', 'override']);
4
+
5
+ function isMobxDecorator(decorator) {
6
+ return mobxDecorators.has(decorator.expression.name) // @foo
7
+ || mobxDecorators.has(decorator.expression.callee?.name) // @foo()
8
+ || mobxDecorators.has(decorator.expression.object?.name) // @foo.bar
9
+ }
10
+
11
+ function findAncestor(node, match) {
12
+ const { parent } = node;
13
+ if (!parent) return;
14
+ if (match(parent)) return parent;
15
+ return findAncestor(parent, match);
16
+ }
17
+
18
+ function assert(expr, error) {
19
+ if (!expr) {
20
+ error ??= 'Assertion failed';
21
+ error = error instanceof Error ? error : new Error(error)
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ module.exports = {
27
+ findAncestor,
28
+ isMobxDecorator,
29
+ }
package/.editorconfig DELETED
@@ -1,15 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- indent_style = space
5
- indent_size = 2
6
- charset = utf-8
7
- trim_trailing_whitespace = true
8
- insert_final_newline = true
9
- max_line_length = 80
10
-
11
- [*.md]
12
- trim_trailing_whitespace = false
13
-
14
- [{*.json,*.yml}]
15
- indent_size = 2
package/.eslintrc.js DELETED
@@ -1,35 +0,0 @@
1
- module.exports = {
2
- root: true,
3
- parserOptions: {
4
- ecmaVersion: '2020',
5
- sourceType: 'module',
6
- },
7
- extends: ['eslint:recommended', 'plugin:prettier/recommended'],
8
- env: {
9
- es6: true,
10
- node: true,
11
- },
12
- // 0 - off, 1 - warn, 2 - error
13
- rules: {
14
- 'prefer-template': 1,
15
- 'no-param-reassign': 2,
16
- 'no-new-wrappers': 2,
17
- 'no-shadow': [1, { hoist: 'functions' }],
18
- 'eol-last': [1, 'always'],
19
- 'lines-between-class-members': [
20
- 2,
21
- 'always',
22
- {
23
- exceptAfterSingleLine: true,
24
- },
25
- ],
26
- 'no-lonely-if': 2,
27
- 'no-console': [
28
- 1,
29
- {
30
- allow: ['error'],
31
- },
32
- ],
33
- 'object-shorthand': 2,
34
- },
35
- }
package/husky.config.js DELETED
@@ -1,5 +0,0 @@
1
- module.exports = {
2
- hooks: {
3
- 'pre-commit': 'lint-staged',
4
- },
5
- }
@@ -1,4 +0,0 @@
1
- module.exports = {
2
- '*.{md,yaml,json}': 'prettier --write',
3
- '**/*.{js,jsx}': 'eslint --fix',
4
- }