eslint-plugin-mobx 0.0.4 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # eslint-plugin-mobx
2
2
 
3
+ ## 0.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [`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),
8
+ [`mobx/no-anonymous-observer`](https://github.com/mobxjs/mobx/tree/main/packages/eslint-plugin-mobx#mobxno-anonymous-observer) rules,
9
+
3
10
  ## 0.0.4
4
11
 
5
12
  ### Patch Changes
package/README.md CHANGED
@@ -21,9 +21,11 @@ module.exports = {
21
21
  // ...or specify and customize individual rules:
22
22
  rules: {
23
23
  // these values are the same as recommended
24
- 'mobx/exhaustive-make-observable': 'warn',
24
+ 'mobx/exhaustive-make-observable': 'warn',
25
+ 'mobx/unconditional-make-observable': 'error',
25
26
  'mobx/missing-make-observable': 'error',
26
- 'mobx/unconditional-make-observable': 'error',
27
+ 'mobx/no-missing-observer': 'warn',
28
+ 'mobx/no-anonymous-observer': 'warn',
27
29
  },
28
30
  };
29
31
  ```
@@ -33,7 +35,7 @@ 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...).
@@ -41,8 +43,46 @@ Does not warn about annotated non-existing fields (there is a runtime check, but
41
43
  ### mobx/missing-make-observable
42
44
 
43
45
  *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.
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
+ ```javascript
56
+ class Cmp extends React.Component { }
57
+ class Cmp extends Component { }
58
+ const Cmp = class extends React.Component { }
59
+ const Cmp = class extends Component { }
60
+ class extends Component { }
61
+ class extends React.Component { }
62
+
63
+ function Named() { }
64
+ const foo = function Named() { }
65
+ const Anonym = function () { };
66
+ const Arrow = () => { };
67
+ ```
68
+ **Autofix** wraps the component with `observer` and if necessary declares a constant of the same name: `const Name = observer(function Name() {})`.
69
+ 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:
70
+ ```javascript
71
+ // .eslintrc.js
72
+ "overrides": [
73
+ {
74
+ "files": ["*.jsx"],
75
+ "rules": {
76
+ "mobx/missing-observer": "error"
77
+ }
78
+ }
79
+ ]
80
+ ```
81
+
82
+ ### mobx/no-anonymous-observer
83
+
84
+ Forbids anonymous functions or classes as `observer` components.
85
+ Improves debugging experience and [avoids problem with inability to customize `displayName`](https://github.com/mobxjs/mobx/issues/2721).
86
+ Plays nice with `eslint-plugin-react-hooks` and `mobx/missing-observer` as both of these don't not recognize anonymous function as component.
87
+ **Autofix** infers the name from variable if possible.
88
+
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,13 +275,13 @@ 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) {
@@ -357,12 +357,129 @@ var missingMakeObservable$1 = {
357
357
  secondArgMustBeNullish: "`makeObservable`'s second argument must be nullish or not provided when using decorators."
358
358
  }
359
359
  },
360
+ create: create$2
361
+ };
362
+
363
+ function create$1(context) {
364
+ var sourceCode = context.getSourceCode();
365
+ return {
366
+ 'FunctionDeclaration,FunctionExpression,ArrowFunctionExpression,ClassDeclaration,ClassExpression': function FunctionDeclarationFunctionExpressionArrowFunctionExpressionClassDeclarationClassExpression(cmp) {
367
+ var _cmp$id, _cmp$parent;
368
+
369
+ // Already has observer
370
+ if (cmp.parent && cmp.parent.type === 'CallExpression' && cmp.parent.callee.name === 'observer') return;
371
+ 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
372
+
373
+ if (!name && ((_cmp$parent = cmp.parent) === null || _cmp$parent === void 0 ? void 0 : _cmp$parent.type) === 'VariableDeclarator') {
374
+ name = cmp.parent.id.name;
375
+ }
376
+
377
+ if (cmp.type.startsWith('Class')) {
378
+ // Must extend Component or React.Component
379
+ var superClass = cmp.superClass;
380
+ if (!superClass) return;
381
+ var superClassText = sourceCode.getText(superClass);
382
+ if (superClassText !== 'Component' && superClassText !== 'React.Component') return;
383
+ } else {
384
+ var _name;
385
+
386
+ // Name must start with uppercase letter
387
+ if (!((_name = name) !== null && _name !== void 0 && _name.charAt(0).match(/^[A-Z]$/))) return;
388
+ }
389
+
390
+ var fix = function fix(fixer) {
391
+ return [fixer.insertTextBefore(sourceCode.getFirstToken(cmp), (name && cmp.type.endsWith('Declaration') ? "const ".concat(name, " = ") : '') + 'observer('), fixer.insertTextAfter(sourceCode.getLastToken(cmp), ')')];
392
+ };
393
+
394
+ context.report({
395
+ node: cmp,
396
+ messageId: 'missingObserver',
397
+ data: {
398
+ name: name || '<anonymous>'
399
+ },
400
+ fix: fix
401
+ });
402
+ }
403
+ };
404
+ }
405
+
406
+ var missingObserver$1 = {
407
+ meta: {
408
+ type: 'problem',
409
+ fixable: 'code',
410
+ docs: {
411
+ description: 'prevents missing `observer` on react component',
412
+ recommended: true
413
+ },
414
+ messages: {
415
+ missingObserver: "Component `{{ name }}` is missing `observer`."
416
+ }
417
+ },
418
+ create: create$1
419
+ };
420
+
421
+ function create(context) {
422
+ var sourceCode = context.getSourceCode();
423
+ return {
424
+ 'CallExpression[callee.name="observer"]': function CallExpressionCalleeNameObserver(observer) {
425
+ var _cmp$id;
426
+
427
+ var cmp = observer.arguments[0];
428
+ if (!cmp) return;
429
+ if (cmp !== null && cmp !== void 0 && (_cmp$id = cmp.id) !== null && _cmp$id !== void 0 && _cmp$id.name) return;
430
+
431
+ var fix = function fix(fixer) {
432
+ var _observer$parent;
433
+
434
+ // Use name from variable for autofix
435
+ var name = ((_observer$parent = observer.parent) === null || _observer$parent === void 0 ? void 0 : _observer$parent.type) === 'VariableDeclarator' ? observer.parent.id.name : undefined;
436
+ if (!name) return;
437
+
438
+ if (cmp.type === 'ArrowFunctionExpression') {
439
+ var arrowToken = sourceCode.getTokenBefore(cmp.body);
440
+ return [fixer.replaceText(arrowToken, ''), fixer.insertTextBefore(cmp, "function ".concat(name))];
441
+ }
442
+
443
+ if (cmp.type === 'FunctionExpression') {
444
+ var functionToken = sourceCode.getFirstToken(cmp);
445
+ return fixer.replaceText(functionToken, "function ".concat(name));
446
+ }
447
+
448
+ if (cmp.type === 'ClassExpression') {
449
+ var classToken = sourceCode.getFirstToken(cmp);
450
+ return fixer.replaceText(classToken, "class ".concat(name));
451
+ }
452
+ };
453
+
454
+ context.report({
455
+ node: cmp,
456
+ messageId: 'observerComponentMustHaveName',
457
+ fix: fix
458
+ });
459
+ }
460
+ };
461
+ }
462
+
463
+ var noAnonymousObserver$1 = {
464
+ meta: {
465
+ type: 'problem',
466
+ fixable: 'code',
467
+ docs: {
468
+ description: 'forbids anonymous functions or classes as `observer` components',
469
+ recommended: true
470
+ },
471
+ messages: {
472
+ observerComponentMustHaveName: "`observer` component must have a name."
473
+ }
474
+ },
360
475
  create: create
361
476
  };
362
477
 
363
478
  var exhaustiveMakeObservable = exhaustiveMakeObservable$1;
364
479
  var unconditionalMakeObservable = unconditionalMakeObservable$1;
365
480
  var missingMakeObservable = missingMakeObservable$1;
481
+ var missingObserver = missingObserver$1;
482
+ var noAnonymousObserver = noAnonymousObserver$1;
366
483
  var src = {
367
484
  configs: {
368
485
  recommended: {
@@ -370,14 +487,18 @@ var src = {
370
487
  rules: {
371
488
  'mobx/exhaustive-make-observable': 'warn',
372
489
  'mobx/unconditional-make-observable': 'error',
373
- 'mobx/missing-make-observable': 'error'
490
+ 'mobx/missing-make-observable': 'error',
491
+ 'mobx/no-missing-observer': 'warn',
492
+ 'mobx/no-anonymous-observer': 'warn'
374
493
  }
375
494
  }
376
495
  },
377
496
  rules: {
378
497
  'exhaustive-make-observable': exhaustiveMakeObservable,
379
498
  'unconditional-make-observable': unconditionalMakeObservable,
380
- 'missing-make-observable': missingMakeObservable
499
+ 'missing-make-observable': missingMakeObservable,
500
+ 'missing-observer': missingObserver,
501
+ 'no-anonymous-observer': noAnonymousObserver
381
502
  }
382
503
  };
383
504
 
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.5",
4
4
  "description": "ESLint rules for MobX",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  const exhaustiveMakeObservable = require('./exhaustive-make-observable.js');
4
4
  const unconditionalMakeObservable = require('./unconditional-make-observable.js');
5
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
10
  configs: {
@@ -12,6 +14,8 @@ module.exports = {
12
14
  'mobx/exhaustive-make-observable': 'warn',
13
15
  'mobx/unconditional-make-observable': 'error',
14
16
  'mobx/missing-make-observable': 'error',
17
+ 'mobx/no-missing-observer': 'warn',
18
+ 'mobx/no-anonymous-observer': 'warn',
15
19
  },
16
20
  },
17
21
  },
@@ -19,5 +23,7 @@ module.exports = {
19
23
  'exhaustive-make-observable': exhaustiveMakeObservable,
20
24
  'unconditional-make-observable': unconditionalMakeObservable,
21
25
  'missing-make-observable': missingMakeObservable,
26
+ 'missing-observer': missingObserver,
27
+ 'no-anonymous-observer': noAnonymousObserver,
22
28
  }
23
29
  }
@@ -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
+ };