@automattic/eslint-plugin-wpvip 0.4.7 → 0.4.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/.eslintrc.js CHANGED
@@ -6,6 +6,8 @@ module.exports = {
6
6
  extends: [
7
7
  'plugin:eslint-plugin/recommended', // linting for eslint plugins!
8
8
  'plugin:@automattic/wpvip/base',
9
+ 'plugin:@automattic/wpvip/testing',
10
+ 'plugin:@automattic/wpvip/typescript',
9
11
  ],
10
12
  parserOptions: {
11
13
  project: './tsconfig.json',
package/configs/base.js CHANGED
@@ -1,138 +1,8 @@
1
- const isPackageInstalled = require('@wordpress/eslint-plugin/utils/is-package-installed');
2
-
3
- const baseConfig = {
4
- env: {
5
- node: true,
6
- },
1
+ module.exports = {
7
2
  extends: [
8
3
  'eslint:recommended',
9
- 'plugin:@wordpress/eslint-plugin/recommended',
10
4
  'plugin:json/recommended',
11
5
  'plugin:security/recommended',
6
+ require.resolve( './javascript' ),
12
7
  ],
13
- /**
14
- * We must explicitly add this plugin to use our custom rules.
15
- */
16
- plugins: ['@automattic/wpvip'],
17
- /**
18
- * Please include a short description of the rule. For rules that downgrade or
19
- * disable errors, include a brief justification or reasoning.
20
- */
21
- rules: {
22
- // Async/await must not be used in a `.forEach` method, because the result
23
- // will not be awaited in the outer scope.
24
- '@automattic/wpvip/no-async-foreach': 'error',
25
-
26
- // Identifiers should be in camelCase. Object properties are excluded
27
- // (including when destructuring) since they often come from external
28
- // sources (like APIs).
29
- camelcase: [
30
- 'warn',
31
- {
32
- properties: 'never',
33
- ignoreDestructuring: true,
34
- },
35
- ],
36
-
37
- // Maximum cyclomatic complexity must not be above 20.
38
- complexity: 'error',
39
-
40
- // Files must end in a newline.
41
- 'eol-last': ['error', 'always'],
42
-
43
- // Identifiers should be between 2 and 40 characters in length.
44
- 'id-length': [
45
- 'warn',
46
- {
47
- min: 2,
48
- max: 40,
49
- },
50
- ],
51
-
52
- // Disable JSDoc rule to require param definitions. While other JSDoc rules
53
- // are still present and active, they are effectively dormant unless
54
- // triggered by invalid or insufficient JSDoc. In other words, if you don't
55
- // attempt JSDoc, none will be required by the linter.
56
- //
57
- // Reenable this rule with the jsdoc preset.
58
- 'jsdoc/require-param': 'off',
59
-
60
- // Lines containing code should be a maximum of 200 characters in length.
61
- 'max-len': [
62
- 'warn',
63
- {
64
- code: 200,
65
- },
66
- ],
67
-
68
- // Async/await must not be used in a loop, because it leads to sequential
69
- // execution, when parallel execution is almost always preferred.
70
- 'no-await-in-loop': 'error',
71
-
72
- // `console.log` should not be used directly in code. Ideally, delegate to a
73
- // logging function that logs on your behalf (and ignore this rule there).
74
- 'no-console': 'warn',
75
-
76
- // A single `import` statement should be used when importing multiple things
77
- // from a module.
78
- 'no-duplicate-imports': 'warn',
79
-
80
- // `process.exit` must not be used.
81
- 'no-process-exit': 'error',
82
-
83
- // Negating the left operand of a statment frequently leads to logical
84
- // errors. Example: `!key in obj` vs. `!(key in obj)`.
85
- 'no-unsafe-negation': 'error',
86
-
87
- // Defined variables must not go unused.
88
- 'no-unused-vars': 'error',
89
-
90
- // Arrow functions should be used for function arguments and callbacks.
91
- 'prefer-arrow-callback': 'warn',
92
-
93
- // `parseInt` calls must always supply a radix argument.
94
- radix: 'error',
95
-
96
- // Comments should always include consistent spacing for readability.
97
- 'spaced-comment': 'warn',
98
-
99
- // The result of `typeof` must always be compared to a literal string.
100
- 'valid-typeof': [
101
- 'error',
102
- {
103
- requireStringLiterals: true,
104
- },
105
- ],
106
- },
107
- overrides: [],
108
8
  };
109
-
110
- // Make our default TypeScript rules more useful.
111
- if (isPackageInstalled('typescript')) {
112
- baseConfig.overrides.push({
113
- extends: [
114
- 'plugin:@typescript-eslint/recommended-requiring-type-checking',
115
- 'plugin:@typescript-eslint/strict',
116
- ],
117
- files: ['**/*.ts', '**/*.tsx'],
118
- rules: {
119
- // TypeScript `any` type must not be used. This is a warning in the base
120
- // config, and is elevated to an error here.
121
- '@typescript-eslint/no-explicit-any': 'error',
122
- },
123
- });
124
- }
125
-
126
- // Make our default React rules more useful.
127
- if (isPackageInstalled('react')) {
128
- // @TODO
129
- }
130
-
131
- // Add Jest-specific rules if it is installed.
132
- if (isPackageInstalled('jest')) {
133
- baseConfig.env['jest/globals'] = true;
134
- baseConfig.extends.push('plugin:jest/recommended');
135
- baseConfig.plugins.push('jest');
136
- }
137
-
138
- module.exports = baseConfig;
package/configs/cli.js CHANGED
@@ -5,9 +5,9 @@ module.exports = {
5
5
  */
6
6
  rules: {
7
7
  // Allow child_process and non-literal `exec` arguments.
8
- 'security/detect-child-process': 0,
8
+ 'security/detect-child-process': 'off',
9
9
 
10
10
  // Process.exit is used in CLI context to stop execution.
11
- 'no-process-exit': 0,
11
+ 'no-process-exit': 'off',
12
12
  },
13
13
  };
package/configs/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  module.exports = {
2
- base: require('./base'),
3
- cli: require('./cli'),
4
- jsdoc: require('./jsdoc'),
5
- 'prettier-off': require('./prettier-off'),
6
- weak: require('./weak'),
7
- 'weak-typescript': require('./weak-typescript'),
2
+ base: require( './base' ),
3
+ cli: require( './cli' ),
4
+ javascript: require( './javascript' ),
5
+ jsdoc: require( './jsdoc' ),
6
+ prettier: require( './prettier' ),
7
+ react: require( './react' ),
8
+ testing: require( './testing' ),
9
+ typescript: require( './typescript' ),
10
+ 'weak-javascript': require( './weak-javascript' ),
11
+ 'weak-typescript': require( './weak-typescript' ),
8
12
  };
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Based on:
3
+ * - https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/jshint.js
4
+ * - https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/es5.js
5
+ * - https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/esnext.js
6
+ * - https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/recommended-with-formatting.js
7
+ */
8
+
9
+ module.exports = {
10
+ env: {
11
+ es2022: true, // 100% covered by Node.js v16
12
+ node: true,
13
+ },
14
+
15
+ parser: '@babel/eslint-parser',
16
+
17
+ parserOptions: {
18
+ requireConfigFile: false,
19
+ sourceType: 'module',
20
+ },
21
+
22
+ /**
23
+ * Note: We must explicitly add this plugin to use our custom rules.
24
+ */
25
+ plugins: [ '@automattic/wpvip', 'import' ],
26
+
27
+ /**
28
+ * Please include a short description of the rule. For rules that downgrade or
29
+ * disable errors, include a brief justification or reasoning.
30
+ */
31
+ rules: {
32
+ // Async/await must not be used in a `.forEach` method, because the result
33
+ // will not be awaited in the outer scope.
34
+ '@automattic/wpvip/no-async-foreach': 'error',
35
+
36
+ // Variables that are potentially not needed before a return statement can
37
+ // be assigned after the return.
38
+ '@automattic/wpvip/no-unused-vars-before-return': 'error',
39
+
40
+ // Unguarded getRangeAt calls can throw errors in some browsers.
41
+ '@automattic/wpvip/no-unguarded-get-range-at': 'error',
42
+
43
+ 'array-bracket-spacing': [ 'error', 'always' ],
44
+
45
+ 'array-callback-return': 'error',
46
+
47
+ 'arrow-parens': [ 'error', 'always' ],
48
+
49
+ 'arrow-spacing': 'error',
50
+
51
+ 'brace-style': [ 'error', '1tbs' ],
52
+
53
+ // Identifiers should be in camelCase. Object properties are excluded
54
+ // (including when destructuring) since they often come from external
55
+ // sources (like APIs).
56
+ camelcase: [
57
+ 'error',
58
+ {
59
+ properties: 'never',
60
+ ignoreDestructuring: true,
61
+ },
62
+ ],
63
+
64
+ 'comma-dangle': [ 'error', 'always-multiline' ],
65
+
66
+ 'comma-spacing': 'error',
67
+
68
+ 'comma-style': [ 'error', 'last' ],
69
+
70
+ // Maximum cyclomatic complexity must not be above 20.
71
+ complexity: 'error',
72
+
73
+ 'computed-property-spacing': [ 'error', 'always' ],
74
+
75
+ 'constructor-super': 'error',
76
+
77
+ curly: [ 'error', 'all' ],
78
+
79
+ 'dot-notation': 'error',
80
+
81
+ // Files must end in a newline.
82
+ 'eol-last': [ 'error', 'always' ],
83
+
84
+ eqeqeq: 'error',
85
+
86
+ 'func-call-spacing': 'error',
87
+
88
+ // Identifiers should be between 2 and 40 characters in length in order to
89
+ // provide a concise semantic meaning.
90
+ 'id-length': [
91
+ 'warn',
92
+ {
93
+ min: 2,
94
+ max: 40,
95
+ },
96
+ ],
97
+
98
+ 'import/default': 'warn',
99
+
100
+ 'import/named': 'warn',
101
+
102
+ 'import/no-extraneous-dependencies': [
103
+ 'error',
104
+ {
105
+ peerDependencies: true,
106
+ },
107
+ ],
108
+
109
+ 'import/no-unresolved': 'error',
110
+
111
+ indent: [ 'error', 'tab', { SwitchCase: 1 } ],
112
+
113
+ 'key-spacing': 'error',
114
+
115
+ 'keyword-spacing': 'error',
116
+
117
+ 'linebreak-style': [ 'error', 'unix' ],
118
+
119
+ // Lines containing code should be a maximum of 200 characters in length.
120
+ 'max-len': [
121
+ 'warn',
122
+ {
123
+ code: 200,
124
+ },
125
+ ],
126
+
127
+ 'no-alert': 'error',
128
+
129
+ // Async/await must not be used in a loop, because it leads to sequential
130
+ // execution, when parallel execution is almost always preferred.
131
+ 'no-await-in-loop': 'error',
132
+
133
+ 'no-bitwise': 'error',
134
+
135
+ 'no-caller': 'error',
136
+
137
+ 'no-cond-assign': [ 'error', 'except-parens' ],
138
+
139
+ // `console.log` should not be used directly in code. Ideally, delegate to a
140
+ // logging function that logs on your behalf (and ignore this rule there).
141
+ 'no-console': 'warn',
142
+
143
+ 'no-const-assign': 'error',
144
+
145
+ 'no-debugger': 'error',
146
+
147
+ 'no-dupe-args': 'error',
148
+
149
+ 'no-dupe-class-members': 'error',
150
+
151
+ 'no-dupe-keys': 'error',
152
+
153
+ 'no-duplicate-case': 'error',
154
+
155
+ // A single `import` statement should be used when importing multiple things
156
+ // from a module.
157
+ 'no-duplicate-imports': 'error',
158
+
159
+ 'no-else-return': 'error',
160
+
161
+ 'no-eq-null': 'error',
162
+
163
+ 'no-eval': 'error',
164
+
165
+ 'no-extra-semi': 'error',
166
+
167
+ 'no-fallthrough': 'error',
168
+
169
+ 'no-irregular-whitespace': 'error',
170
+
171
+ 'no-lonely-if': 'error',
172
+
173
+ 'no-mixed-operators': 'error',
174
+
175
+ 'no-mixed-spaces-and-tabs': 'error',
176
+
177
+ 'no-multi-spaces': 'error',
178
+
179
+ 'no-multi-str': 'error',
180
+
181
+ 'no-multiple-empty-lines': [ 'error', { max: 1 } ],
182
+
183
+ 'no-nested-ternary': 'error',
184
+
185
+ 'no-redeclare': 'error',
186
+
187
+ 'no-shadow': 'error',
188
+
189
+ 'no-trailing-spaces': 'error',
190
+
191
+ 'no-undef': 'error',
192
+
193
+ 'no-undef-init': 'error',
194
+
195
+ 'no-unreachable': 'error',
196
+
197
+ // Negating the left operand of a statment frequently leads to logical
198
+ // errors. Example: `!key in obj` vs. `!(key in obj)`.
199
+ 'no-unsafe-negation': 'error',
200
+
201
+ 'no-unused-expressions': 'error',
202
+
203
+ 'no-unused-vars': [ 'error', { ignoreRestSiblings: true } ],
204
+
205
+ 'no-useless-computed-key': 'error',
206
+
207
+ 'no-useless-constructor': 'error',
208
+
209
+ 'no-useless-return': 'error',
210
+
211
+ 'no-var': 'error',
212
+
213
+ 'no-whitespace-before-property': 'error',
214
+
215
+ 'no-with': 'error',
216
+
217
+ 'object-curly-spacing': [ 'error', 'always' ],
218
+
219
+ 'object-shorthand': 'error',
220
+
221
+ 'one-var': [ 'error', 'never' ],
222
+
223
+ 'operator-linebreak': 'error',
224
+
225
+ 'padded-blocks': [ 'error', 'never' ],
226
+
227
+ // Arrow functions should be used for function arguments and callbacks.
228
+ 'prefer-arrow-callback': 'warn',
229
+
230
+ 'prefer-const': [ 'error', { destructuring: 'all' } ],
231
+
232
+ radix: 'error',
233
+
234
+ quotes: [
235
+ 'error',
236
+ 'single',
237
+ { allowTemplateLiterals: true, avoidEscape: true },
238
+ ],
239
+
240
+ 'quote-props': [ 'error', 'as-needed' ],
241
+
242
+ semi: 'error',
243
+
244
+ 'semi-spacing': 'error',
245
+
246
+ 'space-before-blocks': [ 'error', 'always' ],
247
+
248
+ 'space-before-function-paren': [
249
+ 'error',
250
+ { anonymous: 'never', named: 'never', asyncArrow: 'always' },
251
+ ],
252
+
253
+ 'space-in-parens': [ 'error', 'always' ],
254
+
255
+ 'space-infix-ops': 'error',
256
+
257
+ 'space-unary-ops': [ 'error', { overrides: { '!': true, yield: true } } ],
258
+
259
+ // Comments should always include consistent spacing for readability.
260
+ 'spaced-comment': 'warn',
261
+
262
+ 'template-curly-spacing': [ 'error', 'always' ],
263
+
264
+ // The result of `typeof` must always be compared to a literal string.
265
+ 'valid-typeof': [
266
+ 'error',
267
+ {
268
+ requireStringLiterals: true,
269
+ },
270
+ ],
271
+
272
+ 'wrap-iife': [ 'error', 'any' ],
273
+ },
274
+ };
package/configs/jsdoc.js CHANGED
@@ -1,12 +1,112 @@
1
+ /**
2
+ * Based on:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/jsdoc.js
4
+ */
5
+
6
+ const globals = require( 'globals' );
7
+
8
+ /**
9
+ * Helpful utilities that are globally defined and known to the TypeScript compiler.
10
+ *
11
+ * @see http://www.typescriptlang.org/docs/handbook/utility-types.html
12
+ */
13
+ const typescriptUtilityTypes = [
14
+ 'ArrayLike',
15
+ 'Exclude',
16
+ 'Extract',
17
+ 'InstanceType',
18
+ 'Iterable',
19
+ 'IterableIterator',
20
+ 'NonNullable',
21
+ 'Omit',
22
+ 'Parameters',
23
+ 'Partial',
24
+ 'Pick',
25
+ 'PromiseLike',
26
+ 'Readonly',
27
+ 'ReadonlyArray',
28
+ 'ReadonlyMap',
29
+ 'ReadonlySet',
30
+ 'Record',
31
+ 'Required',
32
+ 'ReturnType',
33
+ 'ThisType',
34
+ 'unknown',
35
+ 'never',
36
+ 'NodeJS',
37
+ 'AsyncIterableIterator',
38
+ 'NodeRequire',
39
+ 'true',
40
+ 'false',
41
+ ];
42
+
1
43
  module.exports = {
2
- /**
3
- * Please include a short description of the rule. For rules that downgrade or
4
- * disable errors, include a brief justification or reasoning.
5
- */
44
+ extends: [ 'plugin:jsdoc/recommended' ],
45
+ settings: {
46
+ jsdoc: {
47
+ preferredTypes: {
48
+ object: 'Object',
49
+ },
50
+ tagNamePreference: {
51
+ returns: 'return',
52
+ yields: 'yield',
53
+ },
54
+ },
55
+ },
6
56
  rules: {
7
- // Reenable the requirement to document function parameters, which in turn
8
- // enables additional lint rules to ensure accuracy and proper formatting.
9
- // This overrides the base preset.
57
+ 'jsdoc/no-undefined-types': [
58
+ 'error',
59
+ {
60
+ definedTypes: [
61
+ // Required to reference browser types because we don't have the `browser` environment enabled for the project.
62
+ // Here we filter out all browser globals that don't begin with an uppercase letter because those
63
+ // generally refer to window-level event listeners and are not a valid type to reference (e.g. `onclick`).
64
+ ...Object.keys( globals.browser ).filter( ( key ) =>
65
+ /^[A-Z]/.test( key ),
66
+ ),
67
+ ...typescriptUtilityTypes,
68
+ 'void',
69
+ 'JSX',
70
+ ],
71
+ },
72
+ ],
73
+ 'jsdoc/require-jsdoc': 'off',
74
+ 'jsdoc/require-param-description': 'off',
75
+ 'jsdoc/require-returns': 'off',
76
+ 'jsdoc/require-yields': 'off',
77
+ 'jsdoc/tag-lines': 'off',
78
+ 'jsdoc/no-multi-asterisks': [
79
+ 'error',
80
+ { preventAtMiddleLines: false },
81
+ ],
82
+ 'jsdoc/check-access': 'error',
83
+ 'jsdoc/check-alignment': 'error',
84
+ 'jsdoc/check-line-alignment': [
85
+ 'error',
86
+ 'always',
87
+ {
88
+ tags: [ 'param', 'arg', 'argument', 'property', 'prop' ],
89
+ preserveMainDescriptionPostDelimiter: true,
90
+ },
91
+ ],
92
+ 'jsdoc/check-param-names': 'error',
93
+ 'jsdoc/check-property-names': 'error',
94
+ 'jsdoc/check-tag-names': 'error',
95
+ 'jsdoc/check-types': 'error',
96
+ 'jsdoc/check-values': 'off',
97
+ 'jsdoc/empty-tags': 'error',
98
+ 'jsdoc/implements-on-classes': 'error',
99
+ 'jsdoc/newline-after-description': 'error',
10
100
  'jsdoc/require-param': 'error',
101
+ 'jsdoc/require-param-name': 'error',
102
+ 'jsdoc/require-param-type': 'error',
103
+ 'jsdoc/require-property': 'error',
104
+ 'jsdoc/require-property-description': 'error',
105
+ 'jsdoc/require-property-name': 'error',
106
+ 'jsdoc/require-property-type': 'error',
107
+ 'jsdoc/require-returns-check': 'error',
108
+ 'jsdoc/require-returns-description': 'error',
109
+ 'jsdoc/require-returns-type': 'error',
110
+ 'jsdoc/valid-types': 'error',
11
111
  },
12
112
  };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Based on:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/jsx-a11y.js
4
+ */
5
+
6
+ module.exports = {
7
+ extends: [ 'plugin:jsx-a11y/recommended' ],
8
+ plugins: [ 'jsx-a11y' ],
9
+ rules: {
10
+ 'jsx-a11y/label-has-associated-control': [
11
+ 'error',
12
+ {
13
+ assert: 'htmlFor',
14
+ },
15
+ ],
16
+ 'jsx-a11y/media-has-caption': 'off',
17
+ 'jsx-a11y/no-noninteractive-tabindex': 'off',
18
+ 'jsx-a11y/role-has-required-aria-props': 'off',
19
+ 'jsx-quotes': 'error',
20
+ },
21
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Based on:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/recommended.js
4
+ */
5
+
6
+ module.exports = {
7
+ extends: [ 'plugin:prettier/recommended' ],
8
+ plugins: [ 'prettier' ],
9
+ rules: {
10
+ 'prettier/prettier': 'error',
11
+ },
12
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Based on:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/react.js
4
+ */
5
+
6
+ module.exports = {
7
+ extends: [
8
+ 'plugin:react/recommended',
9
+ 'plugin:react-hooks/recommended',
10
+ require( './jsx-ally' ),
11
+ ],
12
+ parserOptions: {
13
+ ecmaFeatures: {
14
+ jsx: true,
15
+ },
16
+ },
17
+ settings: {
18
+ react: {
19
+ version: 'detect',
20
+ },
21
+ },
22
+ plugins: [ '@automattic/wpvip', 'react', 'react-hooks' ],
23
+ rules: {
24
+ '@automattic/wpvip/no-unused-vars-before-return': [
25
+ 'error',
26
+ {
27
+ excludePattern: '^use',
28
+ },
29
+ ],
30
+ 'react/display-name': 'off',
31
+ 'react/jsx-curly-spacing': [
32
+ 'error',
33
+ {
34
+ when: 'always',
35
+ children: true,
36
+ },
37
+ ],
38
+ 'react/jsx-equals-spacing': 'error',
39
+ 'react/jsx-indent': [ 'error', 'tab' ],
40
+ 'react/jsx-indent-props': [ 'error', 'tab' ],
41
+ 'react/jsx-key': 'error',
42
+ 'react/jsx-tag-spacing': 'error',
43
+ 'react/no-children-prop': 'off',
44
+ 'react/prop-types': 'off',
45
+ 'react/react-in-jsx-scope': 'off',
46
+ },
47
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Based on:
3
+ * - https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/test-unit.js
4
+ * - https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/test-e2e.js
5
+ */
6
+
7
+ module.exports = {
8
+ extends: [ 'plugin:jest/recommended' ],
9
+
10
+ env: {
11
+ 'jest/globals': true,
12
+ },
13
+
14
+ plugins: [ 'jest' ],
15
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Based on:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/configs/recommended.js
4
+ */
5
+
6
+ module.exports = {
7
+ ignorePatterns: [ '**/*.d.ts' ],
8
+
9
+ overrides: [
10
+ {
11
+ extends: [
12
+ 'plugin:@typescript-eslint/eslint-recommended',
13
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
14
+ 'plugin:@typescript-eslint/strict',
15
+ ],
16
+
17
+ files: [ '**/*.ts', '**/*.tsx' ],
18
+
19
+ parser: '@typescript-eslint/parser',
20
+
21
+ rules: {
22
+ // TypeScript `any` type must not be used. This is a warning in the base
23
+ // config, and is elevated to an error here.
24
+ '@typescript-eslint/no-explicit-any': 'error',
25
+
26
+ // Don't require redundant JSDoc types in TypeScript files.
27
+ 'jsdoc/require-param-type': 'off',
28
+ 'jsdoc/require-returns-type': 'off',
29
+
30
+ // Use TypeScript-specific rules.
31
+ 'no-duplicate-imports': 'off',
32
+ 'no-shadow': 'off',
33
+ '@typescript-eslint/no-duplicate-imports': 'error',
34
+ '@typescript-eslint/no-shadow': 'error',
35
+
36
+ // Handled by TS itself.
37
+ 'no-unused-vars': 'off',
38
+ },
39
+ },
40
+ ],
41
+
42
+ plugins: [ '@typescript-eslint', 'jsdoc' ],
43
+
44
+ settings: {
45
+ 'import/resolver': {
46
+ node: {
47
+ extensions: [ '.js', '.jsx', '.ts', '.tsx' ],
48
+ },
49
+ },
50
+ },
51
+ };
@@ -8,9 +8,8 @@
8
8
  */
9
9
  module.exports = {
10
10
  /**
11
- * Downgrade rules from the base preset to "warning". Do not disable
12
- * rules (set to "off"). If a rule is already set to a warning, do not
13
- * disable it.
11
+ * Downgrade rules from the base preset to "warn". Do not disable rules (set
12
+ * to "off"). If a rule is already set to a warning, do not disable it.
14
13
  */
15
14
  rules: {},
16
15
  };
@@ -6,13 +6,8 @@
6
6
  */
7
7
  module.exports = {
8
8
  /**
9
- * Downgrade rules from the base preset to "warning". Do not disable
10
- * rules (set to "off"). If a rule is already set to a warning, do not
11
- * disable it.
12
- *
13
- * An example is the `@typescript-eslint/no-explicit-any` rule, which is set
14
- * to `warning` in the base preset (via `@typescript-eslint/recommended`) and
15
- * is intentionally not overridden here.
9
+ * Downgrade rules from the base preset to "warn". Do not disable rules (set
10
+ * to "off"). If a rule is already set to a warning, do not disable it.
16
11
  */
17
12
  rules: {},
18
13
  };
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  module.exports = {
2
- configs: require('./configs'),
3
- rules: require('./rules'),
2
+ configs: require( './configs' ),
3
+ rules: require( './rules' ),
4
4
  };
package/init.js CHANGED
@@ -1 +1 @@
1
- require('@rushstack/eslint-patch/modern-module-resolution');
1
+ require( '@rushstack/eslint-patch/modern-module-resolution' );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/eslint-plugin-wpvip",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "ESLint plugin for internal WordPress VIP projects",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,17 +25,26 @@
25
25
  },
26
26
  "homepage": "https://github.com/Automattic/eslint-config-wpvip#readme",
27
27
  "dependencies": {
28
+ "@babel/eslint-parser": "7.21.3",
28
29
  "@rushstack/eslint-patch": "1.2.0",
29
- "@wordpress/eslint-plugin": "14.1.0",
30
+ "@typescript-eslint/eslint-plugin": "5.55.0",
31
+ "@typescript-eslint/parser": "5.55.0",
32
+ "eslint-plugin-import": "2.27.5",
30
33
  "eslint-plugin-jest": "27.2.1",
34
+ "eslint-plugin-jsdoc": "40.0.2",
31
35
  "eslint-plugin-json": "3.1.0",
32
- "eslint-plugin-security": "1.7.1"
36
+ "eslint-plugin-jsx-a11y": "6.7.1",
37
+ "eslint-plugin-react": "7.32.2",
38
+ "eslint-plugin-react-hooks": "4.6.0",
39
+ "eslint-plugin-security": "1.7.1",
40
+ "globals": "13.20.0"
33
41
  },
34
42
  "peerDependencies": {
35
43
  "eslint": ">=8"
36
44
  },
37
45
  "devDependencies": {
38
46
  "eslint": "8.35.0",
47
+ "eslint-config-prettier": "8.7.0",
39
48
  "eslint-plugin-eslint-plugin": "5.0.8",
40
49
  "jest": "29.5.0",
41
50
  "prettier": "2.8.4",
@@ -0,0 +1,263 @@
1
+ /**
2
+ * From:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/rules/dependency-group.js
4
+ */
5
+
6
+ /** @typedef {import('estree').Comment} Comment */
7
+ /** @typedef {import('estree').Node} Node */
8
+
9
+ /** @type {import('eslint').Rule.RuleModule} */
10
+ module.exports = {
11
+ meta: {
12
+ type: 'layout',
13
+ docs: {
14
+ description: 'Enforce dependencies docblocks formatting',
15
+ url: 'https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/dependency-group.md',
16
+ },
17
+ schema: [],
18
+ fixable: 'code',
19
+ },
20
+ create( context ) {
21
+ const comments = context.getSourceCode().getAllComments();
22
+
23
+ /**
24
+ * Locality classification of an import, one of "External",
25
+ * "WordPress", "Internal".
26
+ *
27
+ * @typedef {string} WPPackageLocality
28
+ */
29
+
30
+ /**
31
+ * Object describing a dependency block correction to be made.
32
+ *
33
+ * @typedef WPDependencyBlockCorrection
34
+ *
35
+ * @property {Comment} [comment] Comment node on which to replace value,
36
+ * if one can be salvaged.
37
+ * @property {string} value Expected comment node value.
38
+ */
39
+
40
+ /**
41
+ * Given a desired locality, generates the expected comment node value
42
+ * property.
43
+ *
44
+ * @param {WPPackageLocality} locality Desired package locality.
45
+ *
46
+ * @return {string} Expected comment node value.
47
+ */
48
+ function getCommentValue( locality ) {
49
+ return `*\n * ${ locality } dependencies\n `;
50
+ }
51
+
52
+ /**
53
+ * Given an import source string, returns the locality classification
54
+ * of the import sort.
55
+ *
56
+ * @param {string} source Import source string.
57
+ *
58
+ * @return {WPPackageLocality} Package locality.
59
+ */
60
+ function getPackageLocality( source ) {
61
+ if ( source.startsWith( '.' ) ) {
62
+ return 'Internal';
63
+ } else if ( source.startsWith( '@wordpress/' ) ) {
64
+ return 'WordPress';
65
+ }
66
+
67
+ return 'External';
68
+ }
69
+
70
+ /**
71
+ * Returns true if the given comment node satisfies a desired locality,
72
+ * or false otherwise.
73
+ *
74
+ * @param {Comment} node Comment node to check.
75
+ * @param {WPPackageLocality} locality Desired package locality.
76
+ *
77
+ * @return {boolean} Whether comment node satisfies locality.
78
+ */
79
+ function isLocalityDependencyBlock( node, locality ) {
80
+ const { type, value } = node;
81
+ if ( type !== 'Block' ) {
82
+ return false;
83
+ }
84
+
85
+ // Tolerances:
86
+ // - Normalize `/**` and `/*`
87
+ // - Case insensitive "Dependencies" vs. "dependencies"
88
+ // - Ending period
89
+ // - "Node" dependencies as an alias for External.
90
+
91
+ if ( locality === 'External' ) {
92
+ locality = '(External|Node)';
93
+ }
94
+
95
+ // eslint-disable-next-line security/detect-non-literal-regexp
96
+ const pattern = new RegExp(
97
+ `^\\*?\\n \\* ${ locality } dependencies\\.?\\n $`,
98
+ 'i',
99
+ );
100
+ return pattern.test( value );
101
+ }
102
+
103
+ /**
104
+ * Returns true if the given node occurs prior in code to a reference,
105
+ * or false otherwise.
106
+ *
107
+ * @param {Comment} node Node to test being before reference.
108
+ * @param {Node} reference Node against which to compare.
109
+ *
110
+ * @return {boolean} Whether node occurs before reference.
111
+ */
112
+ function isBefore( node, reference ) {
113
+ if ( ! node.range || ! reference.range ) {
114
+ return false;
115
+ }
116
+
117
+ return node.range[ 0 ] < reference.range[ 0 ];
118
+ }
119
+
120
+ /**
121
+ * Tests source comments to determine whether a comment exists which
122
+ * satisfies the desired locality. If a match is found and requires no
123
+ * updates, the function returns undefined. Otherwise, it will return
124
+ * a WPDependencyBlockCorrection object describing a correction.
125
+ *
126
+ * @param {Node} node Node to test.
127
+ * @param {WPPackageLocality} locality Desired package locality.
128
+ *
129
+ * @return {WPDependencyBlockCorrection | undefined} Correction, if applicable.
130
+ */
131
+ function getDependencyBlockCorrection( node, locality ) {
132
+ const value = getCommentValue( locality );
133
+
134
+ for ( const comment of comments ) {
135
+ if ( ! isBefore( comment, node ) ) {
136
+ // Exhausted options.
137
+ break;
138
+ }
139
+
140
+ if ( ! isLocalityDependencyBlock( comment, locality ) ) {
141
+ // Not usable (either not an block comment, or not one
142
+ // matching a tolerable pattern).
143
+ continue;
144
+ }
145
+
146
+ if ( comment.value === value ) {
147
+ // No change needed. (OK)
148
+ return;
149
+ }
150
+
151
+ // Found a comment needing correction.
152
+ return { comment, value };
153
+ }
154
+
155
+ return { value };
156
+ }
157
+
158
+ return {
159
+ /**
160
+ * @param {import('estree').Program} node Program node.
161
+ */
162
+ Program( node ) {
163
+ /**
164
+ * The set of package localities which have been reported for
165
+ * the current program. Each locality is reported at most one
166
+ * time, since otherwise the fixer would insert a comment
167
+ * block for each individual import statement.
168
+ *
169
+ * @type {Set<WPPackageLocality>}
170
+ */
171
+ const verified = new Set();
172
+
173
+ /**
174
+ * Nodes to check for violations associated with module import,
175
+ * an array of tuples of the node and its import source string.
176
+ *
177
+ * @type {Array<[Node,string]>}
178
+ */
179
+ const candidates = [];
180
+
181
+ // Since we only care to enforce imports which occur at the
182
+ // top-level scope, match on Program and test its children,
183
+ // rather than matching the import nodes directly.
184
+ node.body.forEach( ( child ) => {
185
+ /** @type {string} */
186
+ let source;
187
+ switch ( child.type ) {
188
+ case 'ImportDeclaration':
189
+ source = /** @type {string} */ (
190
+ child.source.value
191
+ );
192
+ candidates.push( [ child, source ] );
193
+ break;
194
+
195
+ case 'VariableDeclaration':
196
+ child.declarations.forEach( ( declaration ) => {
197
+ const { init } = declaration;
198
+ if (
199
+ ! init ||
200
+ init.type !== 'CallExpression' ||
201
+ /** @type {import('estree').CallExpression} */ (
202
+ init
203
+ ).callee.type !== 'Identifier' ||
204
+ /** @type {import('estree').Identifier} */ (
205
+ init.callee
206
+ ).name !== 'require'
207
+ ) {
208
+ return;
209
+ }
210
+
211
+ const { arguments: args } = init;
212
+ if (
213
+ args.length === 1 &&
214
+ args[ 0 ].type === 'Literal' &&
215
+ typeof args[ 0 ].value === 'string'
216
+ ) {
217
+ source = args[ 0 ].value;
218
+ candidates.push( [ child, source ] );
219
+ }
220
+ } );
221
+ }
222
+ } );
223
+
224
+ for ( const [ child, source ] of candidates ) {
225
+ const locality = getPackageLocality( source );
226
+ if ( verified.has( locality ) ) {
227
+ continue;
228
+ }
229
+
230
+ // Avoid verifying any other imports for the locality,
231
+ // regardless whether a correction must be made.
232
+ verified.add( locality );
233
+
234
+ // Determine whether a correction must be made.
235
+ const correction = getDependencyBlockCorrection(
236
+ child,
237
+ locality,
238
+ );
239
+ if ( ! correction ) {
240
+ continue;
241
+ }
242
+
243
+ context.report( {
244
+ node: child,
245
+ message: `Expected preceding "${ locality } dependencies" comment block`,
246
+ fix( fixer ) {
247
+ const { comment, value } = correction;
248
+ const text = `/*${ value }*/`;
249
+ if ( comment && comment.range ) {
250
+ return fixer.replaceTextRange(
251
+ comment.range,
252
+ text,
253
+ );
254
+ }
255
+
256
+ return fixer.insertTextBefore( child, text + '\n' );
257
+ },
258
+ } );
259
+ }
260
+ },
261
+ };
262
+ },
263
+ };
package/rules/index.js CHANGED
@@ -1,4 +1,9 @@
1
+ /**
2
+ * Custom ESLint rules
3
+ */
1
4
  module.exports = {
2
- // Custom ESLint rules
3
- 'no-async-foreach': require('./no-async-foreach'),
5
+ 'dependency-group': require( './dependency-group' ),
6
+ 'no-async-foreach': require( './no-async-foreach' ),
7
+ 'no-unguarded-get-range-at': require( './no-unguarded-get-range-at' ),
8
+ 'no-unused-vars-before-return': require( './no-unused-vars-before-return' ),
4
9
  };
@@ -8,26 +8,27 @@
8
8
  */
9
9
 
10
10
  module.exports = {
11
- create(context) {
11
+ create( context ) {
12
12
  return {
13
- ExpressionStatement(node) {
13
+ ExpressionStatement( node ) {
14
14
  const { callee } = node.expression;
15
- if (!callee || !callee.property || !callee.property.name)
15
+ if ( ! callee || ! callee.property || ! callee.property.name ) {
16
16
  return;
17
- if (callee.property.name === 'forEach') {
17
+ }
18
+ if ( callee.property.name === 'forEach' ) {
18
19
  const functionArguments = node.expression.arguments.find(
19
- (exp) => {
20
+ ( exp ) => {
20
21
  return (
21
22
  exp.type === 'ArrowFunctionExpression' ||
22
23
  exp.type === 'FunctionExpression'
23
24
  );
24
- }
25
+ },
25
26
  );
26
- if (functionArguments) {
27
- if (functionArguments.async) {
27
+ if ( functionArguments ) {
28
+ if ( functionArguments.async ) {
28
29
  context.report(
29
30
  node,
30
- 'Avoid passing an async function to Array.prototype.forEach'
31
+ 'Avoid passing an async function to Array.prototype.forEach',
31
32
  );
32
33
  }
33
34
  }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * From:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/rules/no-unguarded-get-range-at.js
4
+ */
5
+
6
+ module.exports = {
7
+ meta: {
8
+ type: 'problem',
9
+ schema: [],
10
+ },
11
+ create( context ) {
12
+ return {
13
+ 'CallExpression[callee.object.callee.property.name="getSelection"][callee.property.name="getRangeAt"]'(
14
+ node,
15
+ ) {
16
+ context.report( {
17
+ node,
18
+ message: 'Avoid unguarded getRangeAt',
19
+ } );
20
+ },
21
+ };
22
+ },
23
+ };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * From:
3
+ * https://github.com/WordPress/gutenberg/blob/%40wordpress/eslint-plugin%4014.1.0/packages/eslint-plugin/rules/no-unused-vars-before-return.js
4
+ */
5
+
6
+ /** @typedef {import('eslint').Scope.Scope} ESLintScope */
7
+ /** @typedef {import('eslint').Rule.RuleContext} ESLintRuleContext */
8
+ /** @typedef {import('estree').Node} ESTreeNode */
9
+
10
+ /**
11
+ * Mapping of function scope objects to a set of identified JSX identifiers
12
+ * within that scope.
13
+ *
14
+ * @type {WeakMap<ESLintScope,Set<ESTreeNode>>}
15
+ */
16
+ const FUNCTION_SCOPE_JSX_IDENTIFIERS = new WeakMap();
17
+
18
+ /**
19
+ * Returns the closest function scope for the current ESLint context object, or
20
+ * undefined if it cannot be determined.
21
+ *
22
+ * @param {ESLintRuleContext} context ESLint context object.
23
+ *
24
+ * @return {ESLintScope|undefined} Function scope, if known.
25
+ */
26
+ function getClosestFunctionScope( context ) {
27
+ let functionScope = context.getScope();
28
+ while ( functionScope.type !== 'function' && functionScope.upper ) {
29
+ functionScope = functionScope.upper;
30
+ }
31
+
32
+ return functionScope;
33
+ }
34
+
35
+ module.exports = /** @type {import('eslint').Rule} */ ( {
36
+ meta: {
37
+ type: 'problem',
38
+ schema: [
39
+ {
40
+ type: 'object',
41
+ properties: {
42
+ excludePattern: {
43
+ type: 'string',
44
+ },
45
+ },
46
+ additionalProperties: false,
47
+ },
48
+ ],
49
+ },
50
+ /**
51
+ * @param {ESLintRuleContext} context Rule context.
52
+ */
53
+ create( context ) {
54
+ const options = context.options[ 0 ] || {};
55
+ const { excludePattern } = options;
56
+
57
+ /**
58
+ * Given an Espree VariableDeclarator node, returns true if the node
59
+ * can be exempted from consideration as unused, or false otherwise. A
60
+ * node can be exempt if it destructures to multiple variables, since
61
+ * those other variables may be used prior to the return statement. A
62
+ * future enhancement could validate that they are in-fact referenced.
63
+ *
64
+ * @param {Object} node Node to test.
65
+ *
66
+ * @return {boolean} Whether declarator is emempt from consideration.
67
+ */
68
+ function isExemptObjectDestructureDeclarator( node ) {
69
+ return (
70
+ node.id.type === 'ObjectPattern' &&
71
+ node.id.properties.length > 1
72
+ );
73
+ }
74
+
75
+ return {
76
+ JSXIdentifier( node ) {
77
+ // Currently, a scope's variable references does not include JSX
78
+ // identifiers. Account for this by visiting JSX identifiers
79
+ // first, and tracking them in a map per function scope, which
80
+ // is later merged with the known variable references.
81
+ const functionScope = getClosestFunctionScope( context );
82
+ if ( ! functionScope ) {
83
+ return;
84
+ }
85
+
86
+ if ( ! FUNCTION_SCOPE_JSX_IDENTIFIERS.has( functionScope ) ) {
87
+ FUNCTION_SCOPE_JSX_IDENTIFIERS.set(
88
+ functionScope,
89
+ new Set(),
90
+ );
91
+ }
92
+
93
+ FUNCTION_SCOPE_JSX_IDENTIFIERS.get( functionScope ).add( node );
94
+ },
95
+ 'ReturnStatement:exit'( node ) {
96
+ const functionScope = getClosestFunctionScope( context );
97
+ if ( ! functionScope ) {
98
+ return;
99
+ }
100
+
101
+ for ( const variable of functionScope.variables ) {
102
+ const declaratorCandidate = variable.defs.find( ( def ) => {
103
+ return (
104
+ def.node.type === 'VariableDeclarator' &&
105
+ // Allow declarations which are not initialized.
106
+ def.node.init &&
107
+ // Target function calls as "expensive".
108
+ def.node.init.type === 'CallExpression' &&
109
+ // Allow unused if part of an object destructuring.
110
+ ! isExemptObjectDestructureDeclarator( def.node ) &&
111
+ // Only target assignments preceding `return`.
112
+ def.node.range[ 1 ] < node.range[ 1 ]
113
+ );
114
+ } );
115
+
116
+ if ( ! declaratorCandidate ) {
117
+ continue;
118
+ }
119
+
120
+ if (
121
+ excludePattern !== undefined &&
122
+ // eslint-disable-next-line security/detect-non-literal-regexp
123
+ new RegExp( excludePattern ).test(
124
+ declaratorCandidate.node.init.callee.name,
125
+ )
126
+ ) {
127
+ continue;
128
+ }
129
+
130
+ // The first entry in `references` is the declaration
131
+ // itself, which can be ignored.
132
+ const identifiers = variable.references
133
+ .slice( 1 )
134
+ .map( ( reference ) => reference.identifier );
135
+
136
+ // Merge with any JSX identifiers in scope, if any.
137
+ if ( FUNCTION_SCOPE_JSX_IDENTIFIERS.has( functionScope ) ) {
138
+ const jsxIdentifiers =
139
+ FUNCTION_SCOPE_JSX_IDENTIFIERS.get( functionScope );
140
+
141
+ identifiers.push( ...jsxIdentifiers );
142
+ }
143
+
144
+ const isUsedBeforeReturn = identifiers.some(
145
+ ( identifier ) =>
146
+ identifier.range[ 1 ] < node.range[ 1 ],
147
+ );
148
+
149
+ if ( isUsedBeforeReturn ) {
150
+ continue;
151
+ }
152
+
153
+ context.report(
154
+ declaratorCandidate.node,
155
+ 'Variables should not be assigned until just prior its first reference. ' +
156
+ 'An early return statement may leave this variable unused.',
157
+ );
158
+ }
159
+ },
160
+ };
161
+ },
162
+ } );
@@ -1,9 +0,0 @@
1
- module.exports = {
2
- /**
3
- * Please include a short description of the rule. For rules that downgrade or
4
- * disable errors, include a brief justification or reasoning.
5
- */
6
- rules: {
7
- 'prettier/prettier': 'off',
8
- },
9
- };