eslint-plugin-mobx 0.0.4 → 0.0.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # eslint-plugin-mobx
2
2
 
3
+ ## 0.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [`aafda613`](https://github.com/mobxjs/mobx/commit/aafda6136afd107c6cbdd73d9bab57e45a2eb9f5) [#3256](https://github.com/mobxjs/mobx/pull/3256) Thanks [@urugator](https://github.com/urugator)! - fix `no-anonymous-observer` autofix for arrow functions without BlockStatement body
8
+
9
+ ## 0.0.7
10
+
11
+ ### Patch Changes
12
+
13
+ - [`0aaf1831`](https://github.com/mobxjs/mobx/commit/0aaf183131f3eadd40c05ccc94139282bd8d7d56) [#3231](https://github.com/mobxjs/mobx/pull/3231) Thanks [@ahoisl](https://github.com/ahoisl)! - fix(lint): fix 'missing-make-observable' rule with constructor overloads
14
+
15
+ ## 0.0.6
16
+
17
+ ### Patch Changes
18
+
19
+ - [`4b1337ec`](https://github.com/mobxjs/mobx/commit/4b1337ecd64c7bfc904a04063bd1b07e62e392f1) [#3228](https://github.com/mobxjs/mobx/pull/3228) Thanks [@ahoisl](https://github.com/ahoisl)! - fix name for missing-observer rule in recommended
20
+
21
+ ## 0.0.5
22
+
23
+ ### Patch Changes
24
+
25
+ - [`021f34ec`](https://github.com/mobxjs/mobx/commit/021f34ec81daed9e5b5ed8425b2f3e0fa85dfe5b) [#3219](https://github.com/mobxjs/mobx/pull/3219) Thanks [@urugator](https://github.com/urugator)! - Add [`mobx/missing-observer`](https://github.com/mobxjs/mobx/tree/main/packages/eslint-plugin-mobx#mobxmissing-observer),
26
+ [`mobx/no-anonymous-observer`](https://github.com/mobxjs/mobx/tree/main/packages/eslint-plugin-mobx#mobxno-anonymous-observer) rules,
27
+
3
28
  ## 0.0.4
4
29
 
5
30
  ### Patch Changes
package/README.md CHANGED
@@ -5,7 +5,7 @@ Mobx specific linting rules for `eslint`.
5
5
  ## Installation
6
6
 
7
7
  ```
8
- npm install --save-dev eslint @typescript-eslint/parser eslint-plugin-mobx
8
+ npm install --save-dev eslint @typescript-eslint/parser eslint-plugin-mobx
9
9
  ```
10
10
 
11
11
  ## Configuration
@@ -15,17 +15,19 @@ npm install --save-dev eslint @typescript-eslint/parser eslint-plugin-mobx
15
15
  module.exports = {
16
16
  parser: "@typescript-eslint/parser",
17
17
  // Include "mobx" in plugins array:
18
- plugins: ["mobx"],
18
+ plugins: ["mobx"],
19
19
  // Either extend our recommended configuration:
20
20
  extends: "plugin:mobx/recommended",
21
21
  // ...or specify and customize individual rules:
22
22
  rules: {
23
- // these values are the same as recommended
24
- 'mobx/exhaustive-make-observable': 'warn',
25
- 'mobx/missing-make-observable': 'error',
26
- 'mobx/unconditional-make-observable': 'error',
27
- },
28
- };
23
+ // these values are the same as recommended
24
+ "mobx/exhaustive-make-observable": "warn",
25
+ "mobx/unconditional-make-observable": "error",
26
+ "mobx/missing-make-observable": "error",
27
+ "mobx/missing-observer": "warn",
28
+ "mobx/no-anonymous-observer": "warn"
29
+ }
30
+ }
29
31
  ```
30
32
 
31
33
  ## Rules
@@ -33,16 +35,56 @@ module.exports = {
33
35
  ### mobx/exhaustive-make-observable
34
36
 
35
37
  Makes sure that `makeObservable` annotates all fields defined on class or object literal.<br>
36
- Autofix adds `field: true` for each missing field.<br>
38
+ **Autofix** adds `field: true` for each missing field.<br>
37
39
  To exclude a field, annotate it using `field: false`.<br>
38
40
  Does not support fields introduced by constructor (`this.foo = 5`).<br>
39
41
  Does not warn about annotated non-existing fields (there is a runtime check, but the autofix removing the field could be handy...).
40
42
 
41
43
  ### mobx/missing-make-observable
42
44
 
43
- *When using decorators (eg `@observable foo = 5`)*, makes sure that `makeObservable(this)` is called in a constructor.<br>
44
- Autofix creates a constructor if necessary and adds `makeObservable(this)` at it's end.
45
+ _When using decorators (eg `@observable foo = 5`)_, makes sure that `makeObservable(this)` is called in a constructor.<br>
46
+ **Autofix** creates a constructor if necessary and adds `makeObservable(this)` at it's end.
45
47
 
46
48
  ### mobx/unconditional-make-observable
47
49
 
48
- Makes sure the `make(Auto)Observable(this)` is called unconditionally inside a constructor.
50
+ Makes sure the `make(Auto)Observable(this)` is called unconditionally inside a constructor.
51
+
52
+ ### mobx/missing-observer
53
+
54
+ Makes sure every React component is wrapped with `observer`. A React component is considered to be any _class_ extending from `Component` or `React.Component` and any _function_ which name has the first letter capitalized (for anonymous functions the name is inferred from variable). These are all considered components:
55
+
56
+ ```javascript
57
+ class Cmp extends React.Component { }
58
+ class Cmp extends Component { }
59
+ const Cmp = class extends React.Component { }
60
+ const Cmp = class extends Component { }
61
+ class extends Component { }
62
+ class extends React.Component { }
63
+
64
+ function Named() { }
65
+ const foo = function Named() { }
66
+ const Anonym = function () { };
67
+ const Arrow = () => { };
68
+ ```
69
+
70
+ **Autofix** wraps the component with `observer` and if necessary declares a constant of the same name: `const Name = observer(function Name() {})`.
71
+ It's a bit opinionated and can lead to a lot of false positives depending on your conventions. You will probably want to combine this rule with `overrides` option, eg:
72
+
73
+ ```javascript
74
+ // .eslintrc.js
75
+ "overrides": [
76
+ {
77
+ "files": ["*.jsx"],
78
+ "rules": {
79
+ "mobx/missing-observer": "error"
80
+ }
81
+ }
82
+ ]
83
+ ```
84
+
85
+ ### mobx/no-anonymous-observer
86
+
87
+ Forbids anonymous functions or classes as `observer` components.
88
+ Improves debugging experience and [avoids problem with inability to customize `displayName`](https://github.com/mobxjs/mobx/issues/2721).
89
+ Plays nice with `eslint-plugin-react-hooks` and `mobx/missing-observer` as both of these don't not recognize anonymous function as component.
90
+ **Autofix** infers the name from variable if possible.
package/dist/index.js CHANGED
@@ -101,7 +101,7 @@ var findAncestor$2 = utils.findAncestor,
101
101
  isMobxDecorator$1 = utils.isMobxDecorator; // TODO support this.foo = 5; in constructor
102
102
  // TODO? report on field as well
103
103
 
104
- function create$2(context) {
104
+ function create$4(context) {
105
105
  var sourceCode = context.getSourceCode();
106
106
 
107
107
  function fieldToKey(field) {
@@ -225,12 +225,12 @@ var exhaustiveMakeObservable$1 = {
225
225
  'missingAnnotation': 'Missing annotation for {{ keyList }}. To exclude a field, use `false` as annotation.'
226
226
  }
227
227
  },
228
- create: create$2
228
+ create: create$4
229
229
  };
230
230
 
231
231
  var findAncestor$1 = utils.findAncestor;
232
232
 
233
- function create$1(context) {
233
+ function create$3(context) {
234
234
  return {
235
235
  'CallExpression[callee.name=/(makeObservable|makeAutoObservable)/]': function CallExpressionCalleeNameMakeObservableMakeAutoObservable(makeObservable) {
236
236
  var _closestFunction$pare;
@@ -275,17 +275,17 @@ var unconditionalMakeObservable$1 = {
275
275
  mustCallUnconditionally: '`{{ name }}` must be called unconditionally inside constructor.'
276
276
  }
277
277
  },
278
- create: create$1
278
+ create: create$3
279
279
  };
280
280
 
281
281
  var findAncestor = utils.findAncestor,
282
282
  isMobxDecorator = utils.isMobxDecorator;
283
283
 
284
- function create(context) {
284
+ function create$2(context) {
285
285
  var sourceCode = context.getSourceCode();
286
286
  return {
287
287
  'Decorator': function Decorator(decorator) {
288
- var _constructor$value$bo, _constructor$value$bo2;
288
+ var _clazz$body$body$find, _constructor$value$bo, _constructor$value$bo2;
289
289
 
290
290
  if (!isMobxDecorator(decorator)) return;
291
291
  var clazz = findAncestor(decorator, function (node) {
@@ -293,7 +293,9 @@ function create(context) {
293
293
  });
294
294
  if (!clazz) return; // ClassDeclaration > ClassBody > []
295
295
 
296
- var constructor = clazz.body.body.find(function (node) {
296
+ var constructor = (_clazz$body$body$find = clazz.body.body.find(function (node) {
297
+ return node.kind === 'constructor' && node.value.type === 'FunctionExpression';
298
+ })) !== null && _clazz$body$body$find !== void 0 ? _clazz$body$body$find : clazz.body.body.find(function (node) {
297
299
  return node.kind === 'constructor';
298
300
  }); // MethodDefinition > FunctionExpression > BlockStatement > []
299
301
 
@@ -357,27 +359,154 @@ var missingMakeObservable$1 = {
357
359
  secondArgMustBeNullish: "`makeObservable`'s second argument must be nullish or not provided when using decorators."
358
360
  }
359
361
  },
362
+ create: create$2
363
+ };
364
+
365
+ function create$1(context) {
366
+ var sourceCode = context.getSourceCode();
367
+ return {
368
+ 'FunctionDeclaration,FunctionExpression,ArrowFunctionExpression,ClassDeclaration,ClassExpression': function FunctionDeclarationFunctionExpressionArrowFunctionExpressionClassDeclarationClassExpression(cmp) {
369
+ var _cmp$id, _cmp$parent;
370
+
371
+ // Already has observer
372
+ if (cmp.parent && cmp.parent.type === 'CallExpression' && cmp.parent.callee.name === 'observer') return;
373
+ var name = (_cmp$id = cmp.id) === null || _cmp$id === void 0 ? void 0 : _cmp$id.name; // If anonymous try to infer name from variable declaration
374
+
375
+ if (!name && ((_cmp$parent = cmp.parent) === null || _cmp$parent === void 0 ? void 0 : _cmp$parent.type) === 'VariableDeclarator') {
376
+ name = cmp.parent.id.name;
377
+ }
378
+
379
+ if (cmp.type.startsWith('Class')) {
380
+ // Must extend Component or React.Component
381
+ var superClass = cmp.superClass;
382
+ if (!superClass) return;
383
+ var superClassText = sourceCode.getText(superClass);
384
+ if (superClassText !== 'Component' && superClassText !== 'React.Component') return;
385
+ } else {
386
+ var _name;
387
+
388
+ // Name must start with uppercase letter
389
+ if (!((_name = name) !== null && _name !== void 0 && _name.charAt(0).match(/^[A-Z]$/))) return;
390
+ }
391
+
392
+ var fix = function fix(fixer) {
393
+ return [fixer.insertTextBefore(sourceCode.getFirstToken(cmp), (name && cmp.type.endsWith('Declaration') ? "const ".concat(name, " = ") : '') + 'observer('), fixer.insertTextAfter(sourceCode.getLastToken(cmp), ')')];
394
+ };
395
+
396
+ context.report({
397
+ node: cmp,
398
+ messageId: 'missingObserver',
399
+ data: {
400
+ name: name || '<anonymous>'
401
+ },
402
+ fix: fix
403
+ });
404
+ }
405
+ };
406
+ }
407
+
408
+ var missingObserver$1 = {
409
+ meta: {
410
+ type: 'problem',
411
+ fixable: 'code',
412
+ docs: {
413
+ description: 'prevents missing `observer` on react component',
414
+ recommended: true
415
+ },
416
+ messages: {
417
+ missingObserver: "Component `{{ name }}` is missing `observer`."
418
+ }
419
+ },
420
+ create: create$1
421
+ };
422
+
423
+ function create(context) {
424
+ var sourceCode = context.getSourceCode();
425
+ return {
426
+ 'CallExpression[callee.name="observer"]': function CallExpressionCalleeNameObserver(observer) {
427
+ var _cmp$id;
428
+
429
+ var cmp = observer.arguments[0];
430
+ if (!cmp) return;
431
+ if (cmp !== null && cmp !== void 0 && (_cmp$id = cmp.id) !== null && _cmp$id !== void 0 && _cmp$id.name) return;
432
+
433
+ var fix = function fix(fixer) {
434
+ var _observer$parent;
435
+
436
+ // Use name from variable for autofix
437
+ var name = ((_observer$parent = observer.parent) === null || _observer$parent === void 0 ? void 0 : _observer$parent.type) === "VariableDeclarator" ? observer.parent.id.name : undefined;
438
+ if (!name) return;
439
+
440
+ if (cmp.type === "ArrowFunctionExpression") {
441
+ var arrowToken = sourceCode.getTokenBefore(cmp.body);
442
+ var fixes = [fixer.replaceText(arrowToken, ""), fixer.insertTextBefore(cmp, "function ".concat(name))];
443
+
444
+ if (cmp.body.type !== "BlockStatement") {
445
+ fixes.push(fixer.insertTextBefore(cmp.body, "{ return "), fixer.insertTextAfter(cmp.body, " }"));
446
+ }
447
+
448
+ return fixes;
449
+ }
450
+
451
+ if (cmp.type === "FunctionExpression") {
452
+ var functionToken = sourceCode.getFirstToken(cmp);
453
+ return fixer.replaceText(functionToken, "function ".concat(name));
454
+ }
455
+
456
+ if (cmp.type === "ClassExpression") {
457
+ var classToken = sourceCode.getFirstToken(cmp);
458
+ return fixer.replaceText(classToken, "class ".concat(name));
459
+ }
460
+ };
461
+
462
+ context.report({
463
+ node: cmp,
464
+ messageId: "observerComponentMustHaveName",
465
+ fix: fix
466
+ });
467
+ }
468
+ };
469
+ }
470
+
471
+ var noAnonymousObserver$1 = {
472
+ meta: {
473
+ type: "problem",
474
+ fixable: "code",
475
+ docs: {
476
+ description: "forbids anonymous functions or classes as `observer` components",
477
+ recommended: true
478
+ },
479
+ messages: {
480
+ observerComponentMustHaveName: "`observer` component must have a name."
481
+ }
482
+ },
360
483
  create: create
361
484
  };
362
485
 
363
486
  var exhaustiveMakeObservable = exhaustiveMakeObservable$1;
364
487
  var unconditionalMakeObservable = unconditionalMakeObservable$1;
365
488
  var missingMakeObservable = missingMakeObservable$1;
489
+ var missingObserver = missingObserver$1;
490
+ var noAnonymousObserver = noAnonymousObserver$1;
366
491
  var src = {
367
492
  configs: {
368
493
  recommended: {
369
- plugins: ['mobx'],
494
+ plugins: ["mobx"],
370
495
  rules: {
371
- 'mobx/exhaustive-make-observable': 'warn',
372
- 'mobx/unconditional-make-observable': 'error',
373
- 'mobx/missing-make-observable': 'error'
496
+ "mobx/exhaustive-make-observable": "warn",
497
+ "mobx/unconditional-make-observable": "error",
498
+ "mobx/missing-make-observable": "error",
499
+ "mobx/missing-observer": "warn",
500
+ "mobx/no-anonymous-observer": "warn"
374
501
  }
375
502
  }
376
503
  },
377
504
  rules: {
378
- 'exhaustive-make-observable': exhaustiveMakeObservable,
379
- 'unconditional-make-observable': unconditionalMakeObservable,
380
- 'missing-make-observable': missingMakeObservable
505
+ "exhaustive-make-observable": exhaustiveMakeObservable,
506
+ "unconditional-make-observable": unconditionalMakeObservable,
507
+ "missing-make-observable": missingMakeObservable,
508
+ "missing-observer": missingObserver,
509
+ "no-anonymous-observer": noAnonymousObserver
381
510
  }
382
511
  };
383
512
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-mobx",
3
- "version": "0.0.4",
3
+ "version": "0.0.8",
4
4
  "description": "ESLint rules for MobX",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -1,23 +1,29 @@
1
- 'use strict';
1
+ "use strict"
2
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');
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")
6
8
 
7
9
  module.exports = {
8
- configs: {
9
- recommended: {
10
- plugins: ['mobx'],
11
- rules: {
12
- 'mobx/exhaustive-make-observable': 'warn',
13
- 'mobx/unconditional-make-observable': 'error',
14
- 'mobx/missing-make-observable': 'error',
15
- },
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/missing-observer": "warn",
18
+ "mobx/no-anonymous-observer": "warn"
19
+ }
20
+ }
16
21
  },
17
- },
18
- rules: {
19
- 'exhaustive-make-observable': exhaustiveMakeObservable,
20
- 'unconditional-make-observable': unconditionalMakeObservable,
21
- 'missing-make-observable': missingMakeObservable,
22
- }
23
- }
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
+ }
@@ -11,7 +11,8 @@ function create(context) {
11
11
  const clazz = findAncestor(decorator, node => node.type === 'ClassDeclaration' || node.type === 'ClassExpression');
12
12
  if (!clazz) return;
13
13
  // ClassDeclaration > ClassBody > []
14
- const constructor = clazz.body.body.find(node => node.kind === 'constructor');
14
+ const constructor = clazz.body.body.find(node => node.kind === 'constructor' && node.value.type === 'FunctionExpression') ??
15
+ clazz.body.body.find(node => node.kind === 'constructor');
15
16
  // MethodDefinition > FunctionExpression > BlockStatement > []
16
17
  const isMakeObservable = node => node.expression?.callee?.name === 'makeObservable' && node.expression?.arguments[0]?.type === 'ThisExpression';
17
18
  const makeObservable = constructor?.value.body?.body.find(isMakeObservable)?.expression;
@@ -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,65 @@
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 =
15
+ observer.parent?.type === "VariableDeclarator"
16
+ ? observer.parent.id.name
17
+ : undefined
18
+
19
+ if (!name) return
20
+ if (cmp.type === "ArrowFunctionExpression") {
21
+ const arrowToken = sourceCode.getTokenBefore(cmp.body)
22
+ const fixes = [
23
+ fixer.replaceText(arrowToken, ""),
24
+ fixer.insertTextBefore(cmp, `function ${name}`)
25
+ ]
26
+ if (cmp.body.type !== "BlockStatement") {
27
+ fixes.push(
28
+ fixer.insertTextBefore(cmp.body, `{ return `),
29
+ fixer.insertTextAfter(cmp.body, ` }`)
30
+ )
31
+ }
32
+ return fixes
33
+ }
34
+ if (cmp.type === "FunctionExpression") {
35
+ const functionToken = sourceCode.getFirstToken(cmp)
36
+ return fixer.replaceText(functionToken, `function ${name}`)
37
+ }
38
+ if (cmp.type === "ClassExpression") {
39
+ const classToken = sourceCode.getFirstToken(cmp)
40
+ return fixer.replaceText(classToken, `class ${name}`)
41
+ }
42
+ }
43
+ context.report({
44
+ node: cmp,
45
+ messageId: "observerComponentMustHaveName",
46
+ fix
47
+ })
48
+ }
49
+ }
50
+ }
51
+
52
+ module.exports = {
53
+ meta: {
54
+ type: "problem",
55
+ fixable: "code",
56
+ docs: {
57
+ description: "forbids anonymous functions or classes as `observer` components",
58
+ recommended: true
59
+ },
60
+ messages: {
61
+ observerComponentMustHaveName: "`observer` component must have a name."
62
+ }
63
+ },
64
+ create
65
+ }