@dartess/eslint-plugin 0.0.1

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +134 -0
  3. package/dist/configs/mobx.d.ts +3 -0
  4. package/dist/configs/mobx.js +13 -0
  5. package/dist/configs/next.d.ts +3 -0
  6. package/dist/configs/next.js +17 -0
  7. package/dist/configs/react.d.ts +3 -0
  8. package/dist/configs/react.js +78 -0
  9. package/dist/configs/recommended.d.ts +3 -0
  10. package/dist/configs/recommended.js +211 -0
  11. package/dist/configs/storybook.d.ts +3 -0
  12. package/dist/configs/storybook.js +20 -0
  13. package/dist/configs/vendor-rules/best-practices.d.ts +120 -0
  14. package/dist/configs/vendor-rules/best-practices.js +248 -0
  15. package/dist/configs/vendor-rules/errors.d.ts +40 -0
  16. package/dist/configs/vendor-rules/errors.js +92 -0
  17. package/dist/configs/vendor-rules/es6.d.ts +47 -0
  18. package/dist/configs/vendor-rules/es6.js +108 -0
  19. package/dist/configs/vendor-rules/imports.d.ts +41 -0
  20. package/dist/configs/vendor-rules/imports.js +119 -0
  21. package/dist/configs/vendor-rules/next-config.d.ts +16 -0
  22. package/dist/configs/vendor-rules/next-config.js +22 -0
  23. package/dist/configs/vendor-rules/react-hooks.d.ts +10 -0
  24. package/dist/configs/vendor-rules/react-hooks.js +17 -0
  25. package/dist/configs/vendor-rules/react.d.ts +228 -0
  26. package/dist/configs/vendor-rules/react.js +513 -0
  27. package/dist/configs/vendor-rules/strict.d.ts +4 -0
  28. package/dist/configs/vendor-rules/strict.js +9 -0
  29. package/dist/configs/vendor-rules/style.d.ts +34 -0
  30. package/dist/configs/vendor-rules/style.js +78 -0
  31. package/dist/configs/vendor-rules/typescript-disablings.d.ts +5 -0
  32. package/dist/configs/vendor-rules/typescript-disablings.js +13 -0
  33. package/dist/configs/vendor-rules/typescript.d.ts +40 -0
  34. package/dist/configs/vendor-rules/typescript.js +69 -0
  35. package/dist/configs/vendor-rules/variables.d.ts +28 -0
  36. package/dist/configs/vendor-rules/variables.js +39 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.js +25 -0
  39. package/dist/rules/jsx-text-as-child.d.ts +13 -0
  40. package/dist/rules/jsx-text-as-child.js +90 -0
  41. package/dist/rules/max-parent-import-depth.d.ts +3 -0
  42. package/dist/rules/max-parent-import-depth.js +50 -0
  43. package/dist/rules/prevent-mixing-external-and-internal-classes.d.ts +13 -0
  44. package/dist/rules/prevent-mixing-external-and-internal-classes.js +72 -0
  45. package/dist/rules/require-observer.d.ts +3 -0
  46. package/dist/rules/require-observer.js +138 -0
  47. package/dist/rules/stories-export-meta.d.ts +3 -0
  48. package/dist/rules/stories-export-meta.js +37 -0
  49. package/dist/rules/stories-export-typed.d.ts +3 -0
  50. package/dist/rules/stories-export-typed.js +51 -0
  51. package/dist/rules/strict-observable-components-declaration.d.ts +14 -0
  52. package/dist/rules/strict-observable-components-declaration.js +118 -0
  53. package/dist/utils/index.d.ts +1 -0
  54. package/dist/utils/index.js +1 -0
  55. package/dist/utils/parse-git-ignore.d.ts +2 -0
  56. package/dist/utils/parse-git-ignore.js +7 -0
  57. package/docs/rules/jsx-text-as-child.md +101 -0
  58. package/docs/rules/max-parent-import-depth.md +31 -0
  59. package/docs/rules/prevent-mixing-external-and-internal-classes.md +42 -0
  60. package/docs/rules/require-observer.md +43 -0
  61. package/docs/rules/stories-export-meta.md +37 -0
  62. package/docs/rules/stories-export-typed.md +42 -0
  63. package/docs/rules/strict-observable-components-declaration.md +57 -0
  64. package/package.json +91 -0
@@ -0,0 +1,13 @@
1
+ // This file contains code from the `eslint-config-airbnb-typescript` project
2
+ // Original author: Matt Turnbull <matt@iamturns.com> (https://iamturns.com)
3
+ // License: MIT (see LICENSE-eslint-config-airbnb-typescript.md file)
4
+ // Permalink: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/303e346214847385bee4016367ff3b1b9978e337/lib/shared.js
5
+ const rules = {
6
+ // The following rules are enabled in Airbnb config, but are already checked (more thoroughly) by the TypeScript compiler
7
+ // Some of the rules also fail in TypeScript files, for example: https://github.com/typescript-eslint/typescript-eslint/issues/662#issuecomment-507081586
8
+ // Rules are inspired by: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/eslint-recommended.ts
9
+ 'valid-typeof': 'off',
10
+ // Disable `import/no-unresolved`, see README.md for details
11
+ 'import-x/no-unresolved': 'off',
12
+ };
13
+ export default rules;
@@ -0,0 +1,40 @@
1
+ declare const rules: {
2
+ 'default-param-last': "off";
3
+ '@typescript-eslint/default-param-last': "error";
4
+ 'no-empty-function': "off";
5
+ '@typescript-eslint/no-empty-function': ["error", {
6
+ allow: string[];
7
+ }];
8
+ 'no-loop-func': "off";
9
+ '@typescript-eslint/no-loop-func': "error";
10
+ 'no-shadow': "off";
11
+ '@typescript-eslint/no-shadow': "error";
12
+ 'no-unused-vars': "off";
13
+ '@typescript-eslint/no-unused-vars': ["error", {
14
+ vars: string;
15
+ args: string;
16
+ ignoreRestSiblings: boolean;
17
+ }];
18
+ 'no-use-before-define': "off";
19
+ '@typescript-eslint/no-use-before-define': ["error", {
20
+ functions: boolean;
21
+ classes: boolean;
22
+ variables: boolean;
23
+ }];
24
+ 'require-await': "off";
25
+ '@typescript-eslint/require-await': "off";
26
+ 'no-return-await': "off";
27
+ '@typescript-eslint/return-await': ["error", string];
28
+ 'import-x/extensions': ["error", string, {
29
+ ts: string;
30
+ tsx: string;
31
+ js: string;
32
+ mjs: string;
33
+ jsx: string;
34
+ }];
35
+ 'import-x/no-extraneous-dependencies': ["error", {
36
+ devDependencies: string[];
37
+ optionalDependencies: boolean;
38
+ }];
39
+ };
40
+ export default rules;
@@ -0,0 +1,69 @@
1
+ // This file contains code from the `eslint-config-airbnb-typescript` project
2
+ // Original author: Matt Turnbull <matt@iamturns.com> (https://iamturns.com)
3
+ // License: MIT (see LICENSE-eslint-config-airbnb-typescript.md file)
4
+ // Permalink: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/303e346214847385bee4016367ff3b1b9978e337/lib/shared.js
5
+ import baseBestPracticesRules from "./best-practices.js";
6
+ import baseVariablesRules from "./variables.js";
7
+ import baseImportsRules from "./imports.js";
8
+ const rules = {
9
+ // Replace Airbnb 'default-param-last' rule with '@typescript-eslint' version
10
+ // https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/default-param-last.md
11
+ 'default-param-last': 'off',
12
+ '@typescript-eslint/default-param-last': baseBestPracticesRules['default-param-last'],
13
+ // Replace Airbnb 'no-empty-function' rule with '@typescript-eslint' version
14
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-empty-function.md
15
+ 'no-empty-function': 'off',
16
+ '@typescript-eslint/no-empty-function': baseBestPracticesRules['no-empty-function'],
17
+ // Replace Airbnb 'no-loop-func' rule with '@typescript-eslint' version
18
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-loop-func.md
19
+ 'no-loop-func': 'off',
20
+ '@typescript-eslint/no-loop-func': baseBestPracticesRules['no-loop-func'],
21
+ // Replace Airbnb 'no-shadow' rule with '@typescript-eslint' version
22
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-shadow.md
23
+ 'no-shadow': 'off',
24
+ '@typescript-eslint/no-shadow': baseVariablesRules['no-shadow'],
25
+ // Replace Airbnb 'no-unused-vars' rule with '@typescript-eslint' version
26
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md
27
+ 'no-unused-vars': 'off',
28
+ '@typescript-eslint/no-unused-vars': baseVariablesRules['no-unused-vars'],
29
+ // Replace Airbnb 'no-use-before-define' rule with '@typescript-eslint' version
30
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md
31
+ 'no-use-before-define': 'off',
32
+ '@typescript-eslint/no-use-before-define': baseVariablesRules['no-use-before-define'],
33
+ // Replace Airbnb 'require-await' rule with '@typescript-eslint' version
34
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/require-await.md
35
+ 'require-await': 'off',
36
+ '@typescript-eslint/require-await': baseBestPracticesRules['require-await'],
37
+ // Replace Airbnb 'no-return-await' rule with '@typescript-eslint' version
38
+ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/return-await.md
39
+ 'no-return-await': 'off',
40
+ '@typescript-eslint/return-await': ['error', 'in-try-catch'],
41
+ // Append 'ts' and 'tsx' to Airbnb 'import-x/extensions' rule
42
+ // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/extensions.md
43
+ 'import-x/extensions': [
44
+ baseImportsRules['import-x/extensions'][0],
45
+ baseImportsRules['import-x/extensions'][1],
46
+ {
47
+ ...baseImportsRules['import-x/extensions'][2],
48
+ ts: 'never',
49
+ tsx: 'never',
50
+ },
51
+ ],
52
+ // Append 'ts' and 'tsx' extensions to Airbnb 'import-x/no-extraneous-dependencies' rule
53
+ // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md
54
+ 'import-x/no-extraneous-dependencies': [
55
+ baseImportsRules['import-x/no-extraneous-dependencies'][0],
56
+ {
57
+ ...baseImportsRules['import-x/no-extraneous-dependencies'][1],
58
+ devDependencies: baseImportsRules['import-x/no-extraneous-dependencies'][1].devDependencies.reduce((result, devDep) => {
59
+ const toAppend = [devDep];
60
+ const devDepWithTs = devDep.replace(/\bjs(x?)\b/g, 'ts$1');
61
+ if (devDepWithTs !== devDep) {
62
+ toAppend.push(devDepWithTs);
63
+ }
64
+ return [...result, ...toAppend];
65
+ }, []),
66
+ },
67
+ ],
68
+ };
69
+ export default rules;
@@ -0,0 +1,28 @@
1
+ declare const rules: {
2
+ 'no-delete-var': "error";
3
+ 'no-label-var': "error";
4
+ 'no-restricted-globals': ["error", {
5
+ name: string;
6
+ message: string;
7
+ }, {
8
+ name: string;
9
+ message: string;
10
+ }, ...{
11
+ name: "name" | "length" | "find" | "parent" | "menubar" | "self" | "addEventListener" | "blur" | "close" | "closed" | "confirm" | "defaultStatus" | "defaultstatus" | "event" | "external" | "focus" | "frameElement" | "frames" | "history" | "innerHeight" | "innerWidth" | "location" | "locationbar" | "moveBy" | "moveTo" | "onblur" | "onerror" | "onfocus" | "onload" | "onresize" | "onunload" | "open" | "opener" | "opera" | "outerHeight" | "outerWidth" | "pageXOffset" | "pageYOffset" | "print" | "removeEventListener" | "resizeBy" | "resizeTo" | "screen" | "screenLeft" | "screenTop" | "screenX" | "screenY" | "scroll" | "scrollbars" | "scrollBy" | "scrollTo" | "scrollX" | "scrollY" | "status" | "statusbar" | "stop" | "toolbar" | "top";
12
+ message: string;
13
+ }[]];
14
+ 'no-shadow': "error";
15
+ 'no-shadow-restricted-names': "error";
16
+ 'no-undef-init': "error";
17
+ 'no-unused-vars': ["error", {
18
+ vars: string;
19
+ args: string;
20
+ ignoreRestSiblings: boolean;
21
+ }];
22
+ 'no-use-before-define': ["error", {
23
+ functions: boolean;
24
+ classes: boolean;
25
+ variables: boolean;
26
+ }];
27
+ };
28
+ export default rules;
@@ -0,0 +1,39 @@
1
+ // This file contains code from the `eslint-config-airbnb` project
2
+ // Original author: Jake Teton-Landis (https://twitter.com/@jitl)
3
+ // License: MIT (see LICENSE-eslint-config-airbnb.md file)
4
+ // Permalink: https://github.com/airbnb/javascript/blob/c25bce83be4db06e6a221d79686c485cd2ed5d5d/packages/eslint-config-airbnb-base/rules/variables.js
5
+ import confusingBrowserGlobals from 'confusing-browser-globals';
6
+ const rules = {
7
+ // disallow deletion of variables
8
+ 'no-delete-var': 'error',
9
+ // disallow labels that share a name with a variable
10
+ // https://eslint.org/docs/rules/no-label-var
11
+ 'no-label-var': 'error',
12
+ // disallow specific globals
13
+ 'no-restricted-globals': [
14
+ 'error',
15
+ {
16
+ name: 'isFinite',
17
+ message: 'Use Number.isFinite instead https://github.com/airbnb/javascript#standard-library--isfinite',
18
+ },
19
+ {
20
+ name: 'isNaN',
21
+ message: 'Use Number.isNaN instead https://github.com/airbnb/javascript#standard-library--isnan',
22
+ },
23
+ ...confusingBrowserGlobals.map(g => ({
24
+ name: g,
25
+ message: `Use window.${g} instead. https://github.com/facebook/create-react-app/blob/HEAD/packages/confusing-browser-globals/README.md`,
26
+ })),
27
+ ],
28
+ // disallow declaration of variables already declared in the outer scope
29
+ 'no-shadow': 'error',
30
+ // disallow shadowing of names such as arguments
31
+ 'no-shadow-restricted-names': 'error',
32
+ // disallow use of undefined when initializing variables
33
+ 'no-undef-init': 'error',
34
+ // disallow declaration of variables that are not used in the code
35
+ 'no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
36
+ // disallow use of variables before they are defined
37
+ 'no-use-before-define': ['error', { functions: true, classes: true, variables: true }],
38
+ };
39
+ export default rules;
@@ -0,0 +1,3 @@
1
+ import { ESLint } from 'eslint';
2
+ declare const plugin: ESLint.Plugin;
3
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ import packageJson from '../package.json' with { type: 'json' };
2
+ import ruleJsxTextAsChildFrom from "./rules/jsx-text-as-child.js";
3
+ import rulePreventMixingExternalAndInternalClasses from "./rules/prevent-mixing-external-and-internal-classes.js";
4
+ import ruleStoriesExportMeta from "./rules/stories-export-meta.js";
5
+ import ruleStoriesExportTyped from "./rules/stories-export-typed.js";
6
+ import ruleStrictObservableComponentsDeclaration from "./rules/strict-observable-components-declaration.js";
7
+ import ruleRequireObserver from "./rules/require-observer.js";
8
+ import ruleMaxParentImportDepth from "./rules/max-parent-import-depth.js";
9
+ const plugin = {
10
+ meta: {
11
+ name: packageJson.name,
12
+ version: packageJson.version,
13
+ },
14
+ processors: {},
15
+ rules: {
16
+ 'jsx-text-as-child': ruleJsxTextAsChildFrom,
17
+ 'prevent-mixing-external-and-internal-classes': rulePreventMixingExternalAndInternalClasses,
18
+ 'stories-export-meta': ruleStoriesExportMeta,
19
+ 'stories-export-typed': ruleStoriesExportTyped,
20
+ 'strict-observable-components-declaration': ruleStrictObservableComponentsDeclaration,
21
+ 'require-observer': ruleRequireObserver,
22
+ 'max-parent-import-depth': ruleMaxParentImportDepth,
23
+ },
24
+ };
25
+ export default plugin;
@@ -0,0 +1,13 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ type Options = [
3
+ {
4
+ allowDigits?: boolean;
5
+ allowEmoji?: boolean;
6
+ allowSpecialSymbols?: boolean;
7
+ allowExtraStrings?: Array<string>;
8
+ disallowedSymbols?: Array<string>;
9
+ }
10
+ ];
11
+ type MessageIds = 'textAsChild' | 'disallowedSymbols';
12
+ declare const _default: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener>;
13
+ export default _default;
@@ -0,0 +1,90 @@
1
+ import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ /**
3
+ * @fileoverview Disallows text as child in JSX elements, except specified characters like emojis, digits, special symbols and extra strings
4
+ * @author Sergey Kozlov
5
+ */
6
+ import emojiRegex from 'emoji-regex';
7
+ export default ESLintUtils.RuleCreator(() => '')({
8
+ name: 'jsx-text-as-child',
9
+ defaultOptions: [{}],
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description: 'Disallow JSX elements to have text as a child, except for specified allowed characters',
14
+ },
15
+ messages: {
16
+ textAsChild: 'JSX elements should not have text without translation; disable for file and add TODO to comment during development.',
17
+ disallowedSymbols: 'According to the disallowedSymbols option, some symbols are prohibited. Check the option and choose an alternative.',
18
+ },
19
+ schema: [
20
+ {
21
+ type: 'object',
22
+ properties: {
23
+ allowExtraStrings: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'string',
27
+ },
28
+ },
29
+ allowEmoji: {
30
+ type: 'boolean',
31
+ },
32
+ allowDigits: {
33
+ type: 'boolean',
34
+ },
35
+ allowSpecialSymbols: {
36
+ type: 'boolean',
37
+ },
38
+ disallowedSymbols: {
39
+ type: 'array',
40
+ items: {
41
+ type: 'string',
42
+ },
43
+ },
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ create(context) {
49
+ const allowedRegexpParts = [' '];
50
+ const [options] = context.options;
51
+ if (options?.allowDigits) {
52
+ allowedRegexpParts.push('[0-9]');
53
+ }
54
+ if (options?.allowEmoji) {
55
+ allowedRegexpParts.push(`(${emojiRegex().source})`);
56
+ }
57
+ if (options?.allowSpecialSymbols) {
58
+ allowedRegexpParts.push(...'`-=[];\\\',./~!@#$%^&*()_+{}|:"<>?№–≈—…'.split('').map(c => `\\${c}`));
59
+ }
60
+ if (options?.allowExtraStrings) {
61
+ allowedRegexpParts.push(...options.allowExtraStrings.map(str => `(${str})`));
62
+ }
63
+ const allowedRegexp = new RegExp(`^(${allowedRegexpParts.join('|')})+$`);
64
+ const disallowedRegexp = options?.disallowedSymbols
65
+ ? new RegExp(`(${options.disallowedSymbols.join('|')})+`)
66
+ : null;
67
+ const checkChildren = (children) => {
68
+ children
69
+ .filter((node) => node.type === AST_NODE_TYPES.JSXText)
70
+ .map(node => ({ node, text: node.value.trim() }))
71
+ .filter(({ text }) => text !== '')
72
+ .forEach(({ node, text }) => {
73
+ if (!allowedRegexp.test(text)) {
74
+ context.report({ node, messageId: 'textAsChild' });
75
+ }
76
+ if (disallowedRegexp?.test(text)) {
77
+ context.report({ node, messageId: 'disallowedSymbols' });
78
+ }
79
+ });
80
+ };
81
+ return {
82
+ JSXElement(node) {
83
+ checkChildren(node.children);
84
+ },
85
+ JSXFragment(node) {
86
+ checkChildren(node.children);
87
+ },
88
+ };
89
+ },
90
+ });
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const _default: ESLintUtils.RuleModule<"tooDeep", [], unknown, ESLintUtils.RuleListener>;
3
+ export default _default;
@@ -0,0 +1,50 @@
1
+ import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ export default ESLintUtils.RuleCreator(() => '')({
3
+ name: 'max-parent-import-depth',
4
+ defaultOptions: [],
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Disallow relative imports going up more than a specified number of parent directories.',
9
+ },
10
+ messages: {
11
+ tooDeep: 'Import from {{ level }} parent levels is not allowed. Maximum allowed is {{ maxParentImportLevels }}.',
12
+ },
13
+ schema: [],
14
+ },
15
+ create(context) {
16
+ const { maxParentImportLevels = 2 } = context.settings;
17
+ const checkSource = (node, source) => {
18
+ const importPath = source.value;
19
+ if (!importPath.startsWith('../')) {
20
+ return;
21
+ }
22
+ const level = importPath.split('../').length - 1;
23
+ if (level > maxParentImportLevels) {
24
+ context.report({
25
+ node: source,
26
+ messageId: 'tooDeep',
27
+ data: { level, maxParentImportLevels },
28
+ });
29
+ }
30
+ };
31
+ return {
32
+ ImportDeclaration(node) {
33
+ checkSource(node, node.source);
34
+ },
35
+ ExportAllDeclaration(node) {
36
+ checkSource(node, node.source);
37
+ },
38
+ ExportNamedDeclaration(node) {
39
+ if (node.source) {
40
+ checkSource(node, node.source);
41
+ }
42
+ },
43
+ ImportExpression(node) {
44
+ if (node.source.type === AST_NODE_TYPES.Literal) {
45
+ checkSource(node, node.source);
46
+ }
47
+ },
48
+ };
49
+ },
50
+ });
@@ -0,0 +1,13 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ /**
3
+ * @fileoverview Prevent mixing of outer and inner classes to avoid dependency on style order
4
+ * @author Sergey Kozlov
5
+ */
6
+ type Options = [
7
+ {
8
+ libName?: string;
9
+ }
10
+ ];
11
+ type MessageIds = 'avoidMix' | 'avoidRenaming';
12
+ declare const _default: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener>;
13
+ export default _default;
@@ -0,0 +1,72 @@
1
+ import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ export default ESLintUtils.RuleCreator(() => '')({
3
+ name: 'prevent-mixing-external-and-internal-classes',
4
+ defaultOptions: [{}],
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ description: 'Disallow passing the className prop to components from outside to enforce encapsulation of styles and prevent unintended styling overrides.',
9
+ },
10
+ messages: {
11
+ avoidMix: 'Avoid mixing outer and inner classes on the same element. The import order of the style is not guaranteed, so the order in which the style is applied is also not guaranteed.',
12
+ avoidRenaming: "'{{ libName }}' must be imported as 'cn'",
13
+ },
14
+ schema: [
15
+ {
16
+ type: 'object',
17
+ properties: {
18
+ libName: {
19
+ type: 'string',
20
+ },
21
+ },
22
+ required: ['libName'],
23
+ additionalProperties: false,
24
+ },
25
+ ],
26
+ },
27
+ create(context) {
28
+ const [options] = context.options;
29
+ const libName = options?.libName;
30
+ if (!libName || typeof libName !== 'string') {
31
+ throw new Error('libName option is required and must be a string');
32
+ }
33
+ const isClassLike = (string) => /class/i.exec(string);
34
+ return {
35
+ CallExpression(node) {
36
+ if (!('name' in node.callee) || node.callee.name !== 'cn') {
37
+ return;
38
+ }
39
+ const hasStylesItem = node.arguments.some(arg => arg.type === AST_NODE_TYPES.MemberExpression &&
40
+ 'name' in arg.object &&
41
+ arg.object.name === 'styles');
42
+ const hasClassLikeItem = node.arguments.some(arg => {
43
+ const isClassLikeIdentifier = arg.type === AST_NODE_TYPES.Identifier && isClassLike(arg.name);
44
+ const isClassLikeProp = arg.type === AST_NODE_TYPES.MemberExpression &&
45
+ (!('name' in arg.object) || arg.object.name !== 'styles') &&
46
+ 'name' in arg.property &&
47
+ isClassLike(arg.property.name);
48
+ return Boolean(isClassLikeIdentifier || isClassLikeProp);
49
+ });
50
+ if (hasStylesItem && hasClassLikeItem) {
51
+ context.report({
52
+ node,
53
+ messageId: 'avoidMix',
54
+ });
55
+ }
56
+ },
57
+ ImportDeclaration(node) {
58
+ if (node.source.value !== libName) {
59
+ return;
60
+ }
61
+ if (node.specifiers[0].local.name === 'cn') {
62
+ return;
63
+ }
64
+ context.report({
65
+ node,
66
+ messageId: 'avoidRenaming',
67
+ data: { libName },
68
+ });
69
+ },
70
+ };
71
+ },
72
+ });
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const _default: ESLintUtils.RuleModule<"requireObserver", [], unknown, ESLintUtils.RuleListener>;
3
+ export default _default;
@@ -0,0 +1,138 @@
1
+ import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ function isCustomHook(fn) {
3
+ if (fn.type === AST_NODE_TYPES.FunctionDeclaration && fn.id?.name.startsWith('use')) {
4
+ return true;
5
+ }
6
+ const { parent } = fn;
7
+ if ((fn.type === AST_NODE_TYPES.FunctionExpression ||
8
+ fn.type === AST_NODE_TYPES.ArrowFunctionExpression) &&
9
+ parent?.type === AST_NODE_TYPES.VariableDeclarator &&
10
+ parent.id.type === AST_NODE_TYPES.Identifier &&
11
+ parent.id.name.startsWith('use')) {
12
+ return true;
13
+ }
14
+ return false;
15
+ }
16
+ export default ESLintUtils.RuleCreator(() => '')({
17
+ name: 'require-observer',
18
+ meta: {
19
+ type: 'problem',
20
+ docs: {
21
+ description: 'Require that React components using specified store hooks are wrapped in observer().',
22
+ },
23
+ fixable: 'code',
24
+ schema: [],
25
+ messages: {
26
+ requireObserver: 'Components using hooks [{{hooks}}] must be wrapped in observer().',
27
+ },
28
+ },
29
+ defaultOptions: [],
30
+ create(context) {
31
+ const mobxSettings = context.settings.mobx;
32
+ const hooks = Array.isArray(mobxSettings?.storeHooks) ? mobxSettings.storeHooks : [];
33
+ if (hooks.length === 0) {
34
+ throw new Error('Please fill settings.mobx.storeHooks');
35
+ }
36
+ const { sourceCode } = context;
37
+ const program = sourceCode.ast;
38
+ const hasObserverImport = program.body.some(node => node.type === AST_NODE_TYPES.ImportDeclaration &&
39
+ node.specifiers.some(spec => spec.type === AST_NODE_TYPES.ImportSpecifier &&
40
+ (spec.imported.type === AST_NODE_TYPES.Identifier
41
+ ? spec.imported.name
42
+ : spec.imported.value) === 'observer'));
43
+ const hooksUsed = new Map();
44
+ const wrappedNames = new Set();
45
+ const wrappedFns = new WeakSet();
46
+ return {
47
+ CallExpression(node) {
48
+ if (node.callee.type === AST_NODE_TYPES.Identifier && hooks.includes(node.callee.name)) {
49
+ const ancestors = sourceCode.getAncestors(node);
50
+ const fn = ancestors
51
+ .slice()
52
+ .reverse()
53
+ .find(anc => anc.type === AST_NODE_TYPES.FunctionDeclaration ||
54
+ anc.type === AST_NODE_TYPES.FunctionExpression ||
55
+ anc.type === AST_NODE_TYPES.ArrowFunctionExpression);
56
+ if (fn && !isCustomHook(fn)) {
57
+ const set = hooksUsed.get(fn) ?? new Set();
58
+ set.add(node.callee.name);
59
+ hooksUsed.set(fn, set);
60
+ }
61
+ }
62
+ if (node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'observer') {
63
+ for (const arg of node.arguments) {
64
+ if (arg.type === AST_NODE_TYPES.Identifier) {
65
+ wrappedNames.add(arg.name);
66
+ }
67
+ else if (arg.type === AST_NODE_TYPES.FunctionExpression ||
68
+ arg.type === AST_NODE_TYPES.ArrowFunctionExpression) {
69
+ wrappedFns.add(arg);
70
+ }
71
+ }
72
+ }
73
+ },
74
+ 'Program:exit': function () {
75
+ for (const [fn, used] of hooksUsed) {
76
+ if (isCustomHook(fn)) {
77
+ continue;
78
+ }
79
+ let isWrapped = false;
80
+ if (fn.type === AST_NODE_TYPES.FunctionDeclaration &&
81
+ fn.id &&
82
+ wrappedNames.has(fn.id.name)) {
83
+ isWrapped = true;
84
+ }
85
+ if ((fn.type === AST_NODE_TYPES.FunctionExpression ||
86
+ fn.type === AST_NODE_TYPES.ArrowFunctionExpression) &&
87
+ wrappedFns.has(fn)) {
88
+ isWrapped = true;
89
+ }
90
+ if (isWrapped) {
91
+ continue;
92
+ }
93
+ context.report({
94
+ node: fn,
95
+ messageId: 'requireObserver',
96
+ data: { hooks: Array.from(used).join(', ') },
97
+ fix(fixer) {
98
+ const fixes = [];
99
+ if (!hasObserverImport) {
100
+ fixes.push(fixer.insertTextBefore(program.body[0], "import { observer } from 'mobx-react-lite';\n"));
101
+ }
102
+ const paramsText = fn.params.map(p => sourceCode.getText(p)).join(', ');
103
+ const bodyText = sourceCode.getText(fn.body);
104
+ const ancestors = sourceCode.getAncestors(fn);
105
+ const exportDecl = ancestors.find(a => a.type === AST_NODE_TYPES.ExportNamedDeclaration ||
106
+ a.type === AST_NODE_TYPES.ExportDefaultDeclaration);
107
+ const varDecl = fn.parent?.type === AST_NODE_TYPES.VariableDeclarator ? fn.parent.parent : null;
108
+ if (fn.type === AST_NODE_TYPES.FunctionDeclaration && fn.id) {
109
+ // function declaration -> const assignment
110
+ const { name } = fn.id;
111
+ const original = sourceCode.getText(fn);
112
+ const replacement = `const ${name} = observer(${original});`;
113
+ fixes.push(fixer.replaceText(fn, replacement));
114
+ }
115
+ else if (varDecl && varDecl.type === AST_NODE_TYPES.VariableDeclaration) {
116
+ // const Name = () => {} or function expression
117
+ const id = fn.parent.id;
118
+ const { name } = id;
119
+ const isExport = exportDecl?.type === AST_NODE_TYPES.ExportNamedDeclaration;
120
+ const funcExpression = `function ${name}(${paramsText}) ${bodyText}`;
121
+ const replacementNodeText = `${isExport ? 'export ' : ''}const ${name} = observer(${funcExpression});`;
122
+ fixes.push(fixer.replaceText(exportDecl || varDecl, replacementNodeText));
123
+ }
124
+ else {
125
+ // default export or unnamed
126
+ const isDefault = exportDecl?.type === AST_NODE_TYPES.ExportDefaultDeclaration;
127
+ const funcExpression = `function(${paramsText}) ${bodyText}`;
128
+ const replacementNodeText = `${isDefault ? 'export default ' : ''}observer(${funcExpression});`;
129
+ fixes.push(fixer.replaceText(exportDecl || fn, replacementNodeText));
130
+ }
131
+ return fixes;
132
+ },
133
+ });
134
+ }
135
+ },
136
+ };
137
+ },
138
+ });
@@ -0,0 +1,3 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const _default: ESLintUtils.RuleModule<"exportDefaultTypeCheck", [], unknown, ESLintUtils.RuleListener>;
3
+ export default _default;