linter-bundle 2.21.0 → 2.23.0

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
@@ -6,7 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- [Show all code changes](https://github.com/jens-duttke/linter-bundle/compare/v2.21.0...HEAD)
9
+ [Show all code changes](https://github.com/jens-duttke/linter-bundle/compare/v2.23.0...HEAD)
10
+
11
+ ## [2.23.0] - 2022-09-21
12
+
13
+ ### Added
14
+
15
+ - [general] Add functionality to ensure that the installed dependencies match to the required dependency of the linter-bundle
16
+
17
+ ### Changed
18
+
19
+ - [eslint] Updated `eslint-plugin-functional` from `4.3.2` to `4.4.0`
20
+ - [stylelint] Updated `stylelint` from `14.12.0` to `14.12.1`
21
+ - [eslint] Improved [`no-unnecessary-typeof`](./eslint/rules/no-unnecessary-typeof.md) to cover even more cases and fix false-positives
22
+
23
+ [Show all code changes](https://github.com/jens-duttke/linter-bundle/compare/v2.22.0...v2.23.0)
24
+
25
+ ## [2.22.0] - 2022-09-19
26
+
27
+ ### Changed
28
+
29
+ - [eslint] Updated `@typescript-eslint` from `5.37.0` to `5.38.0`
30
+ - [eslint] Updated `eslint-plugin-functional` from `4.3.1` to `4.3.2`
31
+ - [stylelint] Updated `postcss-scss` from `4.0.4` to `4.0.5`
32
+ - [stylelint] Updated `stylelint` from `14.11.0` to `14.12.0`
33
+ - [eslint] Improved [`no-unnecessary-typeof`](./eslint/rules/no-unnecessary-typeof.md) to cover more cases
34
+
35
+ [Show all code changes](https://github.com/jens-duttke/linter-bundle/compare/v2.21.0...v2.22.0)
10
36
 
11
37
  ## [2.21.0] - 2022-09-14
12
38
 
package/README.md CHANGED
@@ -425,3 +425,7 @@ If you get such an error message:
425
425
  the problem is most likely, that your `tsconfig.json` does not cover your JavaScript files and that you don't have a `jsconfig.json` file in your root directory. This is required by the `@typescript-eslint` to use TypeScript for linting of JavaScript files.
426
426
 
427
427
  To solve this problem, either `"include"` your JavaScript files in your `tsconfig.json` (don't forget to set the compiler option `"checkJs"` to `true`) or create a `jsconfig.json` file in your root directory (this can be a copy of your `tsconfig.json` with an `"include"` of your JavaScript files).
428
+
429
+ ### In VSCode, in every file, the first line shows the error `Definition for rule "no-unnecessary-typeof" was not found. eslint(no-unnecessary-typeof)`
430
+
431
+ Please ensure that you've added the configuration options as described above ("VSCode setup" > "ESLint").
@@ -1,9 +1,11 @@
1
1
  /**
2
- * @file ESLint rule which ensures that `typeof window === 'undefined'` is not used, since it's often the source of rehydration issues in Gatsby.
3
- *
4
- * @see https://www.joshwcomeau.com/react/the-perils-of-rehydration/
2
+ * @file ESLint rule which ensures that a `typeof` operant has more than one type in TypeScript, to prevent unnecessary checks of types at runtime.
5
3
  */
6
4
 
5
+ /** @typedef {ts.Type & { intrinsicName?: string; types?: ts.Type[]; objectFlags?: ts.ObjectFlags; }} Type */
6
+
7
+ const ts = require('typescript');
8
+
7
9
  const { ESLintUtils } = require('@typescript-eslint/utils');
8
10
 
9
11
  /**
@@ -16,13 +18,13 @@ module.exports = {
16
18
  recommended: true
17
19
  },
18
20
  messages: {
19
- text: 'Unnecessary `typeof`, because the only possible type of `{{ name }}` is `{{ type }}`.'
21
+ text: 'Unnecessary `typeof`, because the only possible type of {{ variableName }} is `{{ typeName }}`.'
20
22
  }
21
23
  },
22
24
  create (context) {
23
25
  return {
24
26
  UnaryExpression (node) {
25
- if (node.operator !== 'typeof' || node.argument.type !== 'Identifier') {
27
+ if (node.operator !== 'typeof') {
26
28
  return;
27
29
  }
28
30
 
@@ -33,17 +35,19 @@ module.exports = {
33
35
  // @ts-expect-error -- ESLint `Identifier` is not recognized as `Node` by @typescript-eslint
34
36
  const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node.argument);
35
37
 
36
- /** @type {import('typescript').Type & { intrinsicName?: string; }} */
38
+ /** @type {Type} */
37
39
  const nodeType = checker.getTypeAtLocation(originalNode);
38
40
 
39
- // `intrinsicName` only exists if there is exactly one type
40
- if (nodeType.intrinsicName && !['any', 'error', 'unknown'].includes(nodeType.intrinsicName)) {
41
+ const variableName = checker.getSymbolAtLocation(originalNode)?.getName();
42
+ const typeName = getSingleType(checker, nodeType);
43
+
44
+ if (typeName !== null) {
41
45
  context.report({
42
46
  node,
43
47
  messageId: 'text',
44
48
  data: {
45
- name: node.argument.name,
46
- type: nodeType.intrinsicName
49
+ variableName: (variableName ? `\`${variableName}\`` : `the ${node.argument.type.replace(/[A-Z]/gu, (substring) => ` ${substring.toLowerCase()}`).trim()}`),
50
+ typeName
47
51
  }
48
52
  });
49
53
  }
@@ -51,3 +55,157 @@ module.exports = {
51
55
  };
52
56
  }
53
57
  };
58
+
59
+ /**
60
+ * Check if the number of types equals one, and returns the type.
61
+ *
62
+ * @param {ts.TypeChecker} checker - TypeScript type checker.
63
+ * @param {Type} type - TypeScript type node.
64
+ * @returns {string | null} Type as string match the `typeof` string, or `null` if it's not a primitive type.
65
+ */
66
+ function getSingleType (checker, type) {
67
+ if (isAnyOrUnknown(type)) {
68
+ return null;
69
+ }
70
+
71
+ if (!type.types) {
72
+ return getTypeString(checker, type);
73
+ }
74
+
75
+ const firstType = getTypeString(checker, type.types[0]);
76
+
77
+ if (firstType === null) {
78
+ return null;
79
+ }
80
+
81
+ for (let i = 1; i < type.types.length; i++) {
82
+ if (getTypeString(checker, type.types[i]) !== firstType) {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ return firstType;
88
+ }
89
+
90
+ /**
91
+ * Converts a TypeScript type into a `typeof` compatible string, or `null` if it's not a primitive type.
92
+ *
93
+ * @param {ts.TypeChecker} checker - TypeScript type checker.
94
+ * @param {Type} type - TypeScript type node.
95
+ * @returns {string | null} Type as string match the `typeof` string, or `null` if it's not a primitive type.
96
+ */
97
+ function getTypeString (checker, type) {
98
+ if (isAnyOrUnknown(type)) {
99
+ return null;
100
+ }
101
+
102
+ const typeString = checker.typeToString(type);
103
+
104
+ if (['undefined', 'boolean', 'number', 'bigint', 'string', 'symbol', 'function', 'object'].includes(typeString)) {
105
+ return typeString;
106
+ }
107
+
108
+ if (isUndefined(type)) { return 'undefined'; }
109
+ if (isBoolean(type)) { return 'boolean'; }
110
+ if (isNumber(type)) { return 'number'; }
111
+ if (isBigInt(type)) { return 'bigint'; }
112
+ if (isString(type)) { return 'string'; }
113
+ if (isSymbol(type)) { return 'symbol'; }
114
+ if (isFunction(type)) { return 'function'; }
115
+ if (isObject(type)) { return 'object'; }
116
+
117
+ return null;
118
+ }
119
+
120
+ /**
121
+ * Check if the type is either `any` or `unknown`, which represents multiple types.
122
+ *
123
+ * @param {ts.Type} type - TypeScript type node.
124
+ * @returns {boolean} Returns `true` if the type is either `any` or `unknown`, or an object which is based on `unknown`.
125
+ */
126
+ function isAnyOrUnknown (type) {
127
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- `symbol` on Object is `undefined` for `Omit<unknown, 'undefined'>`
128
+ return (type.flags === ts.TypeFlags.Any || type.flags === ts.TypeFlags.Unknown || (type.flags === ts.TypeFlags.Object && type.symbol === undefined));
129
+ }
130
+
131
+ /**
132
+ * Checks if the given `type` is a `string`.
133
+ *
134
+ * @param {Type} type - TypeScript type node.
135
+ * @returns {boolean} Returns `true` if the type is a `string`.
136
+ */
137
+ function isString (type) {
138
+ return [ts.TypeFlags.String, ts.TypeFlags.StringLiteral, ts.TypeFlags.StringMapping].includes(type.flags);
139
+ }
140
+
141
+ /**
142
+ * Checks if the given `type` is a `number`.
143
+ *
144
+ * @param {Type} type - TypeScript type node.
145
+ * @returns {boolean} Returns `true` if the type is a `number`.
146
+ */
147
+ function isNumber (type) {
148
+ return [ts.TypeFlags.Number, ts.TypeFlags.NumberLiteral].includes(type.flags);
149
+ }
150
+
151
+ /**
152
+ * Checks if the given `type` is a `bigint`.
153
+ *
154
+ * @param {Type} type - TypeScript type node.
155
+ * @returns {boolean} Returns `true` if the type is a `bigint`.
156
+ */
157
+ function isBigInt (type) {
158
+ return [ts.TypeFlags.BigInt, ts.TypeFlags.BigIntLiteral].includes(type.flags);
159
+ }
160
+
161
+ /**
162
+ * Checks if the given `type` is a `boolean`.
163
+ *
164
+ * @param {Type} type - TypeScript type node.
165
+ * @returns {boolean} Returns `true` if the type is a `boolean`.
166
+ */
167
+ function isBoolean (type) {
168
+ return [ts.TypeFlags.Boolean, ts.TypeFlags.BooleanLiteral].includes(type.flags);
169
+ }
170
+
171
+ /**
172
+ * Checks if the given `type` is a `symbol`.
173
+ *
174
+ * @param {Type} type - TypeScript type node.
175
+ * @returns {boolean} Returns `true` if the type is a `symbol`.
176
+ */
177
+ function isSymbol (type) {
178
+ return [ts.TypeFlags.ESSymbol, ts.TypeFlags.UniqueESSymbol].includes(type.flags);
179
+ }
180
+
181
+ /**
182
+ * Checks if the given `type` is a `function`.
183
+ *
184
+ * @param {Type} type - TypeScript type node.
185
+ * @returns {boolean} Returns `true` if the type is a `function`.
186
+ */
187
+ function isFunction (type) {
188
+ return (type.flags === ts.TypeFlags.Object && type.objectFlags === ts.ObjectFlags.Anonymous);
189
+ }
190
+
191
+ /**
192
+ * Checks if the given `type` is a `undefined`.
193
+ *
194
+ * @param {Type} type - TypeScript type node.
195
+ * @returns {boolean} Returns `true` if the type is `undefined`.
196
+ */
197
+ function isUndefined (type) {
198
+ return [ts.TypeFlags.Undefined].includes(type.flags);
199
+ }
200
+
201
+ /**
202
+ * Checks if the given `type` is a `object`.
203
+ *
204
+ * We don't check `ts.TypeFlags.Object`, because for TypeScript Object seems to be the fallback for everything - even unknown types.
205
+ *
206
+ * @param {Type} type - TypeScript type node.
207
+ * @returns {boolean} Returns `true` if the type is an `object`.
208
+ */
209
+ function isObject (type) {
210
+ return [ts.TypeFlags.Null].includes(type.flags);
211
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @file Ensures that the installed dependencies of the project matches to the versions used by the linter-bundle.
3
+ *
4
+ * @see https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides
5
+ * @see https://classic.yarnpkg.com/en/docs/selective-version-resolutions/
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /** @typedef {{ name: string; configuredVersion: string; expectedVersion: string; }} Dependency */
12
+
13
+ /**
14
+ * Detects if installed versions of dependencies don't match to the required dependencies.
15
+ *
16
+ * @returns {Dependency[]} An array of missing overrides (=wrong versions).
17
+ */
18
+ function findMissingOverrides () {
19
+ const linterBundleDependencies = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8')).dependencies;
20
+
21
+ const result = [];
22
+
23
+ for (const [name, expectedVersion] of Object.entries(linterBundleDependencies)) {
24
+ let dependenyPackageJson;
25
+
26
+ try {
27
+ dependenyPackageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'node_modules', name, 'package.json'), 'utf8'));
28
+ }
29
+ catch {
30
+ // If the file does not exist, we ignore it, because in this case it's most likely a linter-bundle-only dependency.
31
+ continue;
32
+ }
33
+
34
+ if (dependenyPackageJson.version !== expectedVersion) {
35
+ result.push({
36
+ name,
37
+ configuredVersion: dependenyPackageJson.version,
38
+ expectedVersion
39
+ });
40
+ }
41
+ }
42
+
43
+ return result;
44
+ }
45
+
46
+ module.exports = {
47
+ findMissingOverrides
48
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @file Check if the project is using npm or yarn by checking the existance of a `package-lock.json` or a `yarn.lock`.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Returns if the project is using npm or yarn.
10
+ *
11
+ * @returns {'none' | 'npm' | 'yarn' | 'both'} Returns which package manager name.
12
+ */
13
+ function isNpmOrYarn () {
14
+ let npm = false;
15
+ let yarn = false;
16
+
17
+ try {
18
+ fs.accessSync(path.join(process.cwd(), 'package-lock.json'), fs.constants.R_OK);
19
+
20
+ npm = true;
21
+ }
22
+ catch { /* `package-lock.json` cannot be accessed. */ }
23
+
24
+ try {
25
+ fs.accessSync(path.join(process.cwd(), 'yarn.lock'), fs.constants.R_OK);
26
+
27
+ yarn = true;
28
+ }
29
+ catch { /* `yarn.lock` cannot be accessed. */ }
30
+
31
+ if (npm && yarn) {
32
+ return 'both';
33
+ }
34
+
35
+ if (npm) {
36
+ return 'npm';
37
+ }
38
+
39
+ if (yarn) {
40
+ return 'yarn';
41
+ }
42
+
43
+ return 'none';
44
+ }
45
+
46
+ module.exports = {
47
+ isNpmOrYarn
48
+ };
package/lint.js CHANGED
@@ -4,12 +4,13 @@
4
4
  * @file Entry point of the linter-bundle.
5
5
  */
6
6
 
7
- const fs = require('fs');
8
7
  const path = require('path');
9
8
  const tty = require('tty');
10
9
 
11
10
  const micromatch = require('micromatch');
12
11
 
12
+ const { findMissingOverrides } = require('./helper/find-missing-overrides.js');
13
+ const { isNpmOrYarn } = require('./helper/is-npm-or-yarn.js');
13
14
  const { runProcess } = require('./helper/run-process.js');
14
15
  const { validatePackageOverrides } = require('./helper/validate-package-overrides.js');
15
16
 
@@ -21,21 +22,9 @@ const { validatePackageOverrides } = require('./helper/validate-package-override
21
22
  const isTerminal = tty.isatty(1);
22
23
 
23
24
  void (async () => {
24
- const outdatedOverrides = validatePackageOverrides();
25
-
26
- if (outdatedOverrides.overrides.length > 0 || outdatedOverrides.resolutions.length > 0) {
27
- if (outdatedOverrides.overrides.length > 0) {
28
- process.stderr.write(`Outdated "overrides" in package.json detected:\n- ${outdatedOverrides.overrides.map((dependency) => `${dependency.name}: ${dependency.configuredVersion} is configured, but ${dependency.expectedVersion} is expected`).join('\n- ')}\n\n`);
29
- }
25
+ const npmOrYarn = isNpmOrYarn();
30
26
 
31
- if (outdatedOverrides.resolutions.length > 0) {
32
- process.stderr.write(`Outdated "resolutions" in package.json detected:\n- ${outdatedOverrides.resolutions.map((dependency) => `${dependency.name}: ${dependency.configuredVersion} is configured, but ${dependency.expectedVersion} is expected`).join('\n- ')}\n\n`);
33
- }
34
-
35
- process.exitCode = 1;
36
-
37
- return;
38
- }
27
+ validateEnvironment(npmOrYarn);
39
28
 
40
29
  /** @type {{ diff: Promise<ProcessResult>; modified: Promise<ProcessResult>; deleted: Promise<ProcessResult>; } | undefined} */
41
30
  let gitFilesProcessPromise;
@@ -151,7 +140,7 @@ void (async () => {
151
140
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- This is not a valid `audit` property, so we need to remove it.
152
141
  delete config['git'];
153
142
 
154
- if (fs.existsSync('package-lock.json')) {
143
+ if (npmOrYarn === 'npm') {
155
144
  return runTask({
156
145
  taskName,
157
146
  config,
@@ -167,7 +156,7 @@ void (async () => {
167
156
  ].filter((argument) => Boolean(argument)).join(' ')
168
157
  });
169
158
  }
170
- else if (fs.existsSync('yarn.lock')) {
159
+ else if (npmOrYarn === 'yarn') {
171
160
  return runTask({
172
161
  taskName,
173
162
  config,
@@ -246,6 +235,56 @@ void (async () => {
246
235
  process.stdout.write('\n');
247
236
  })();
248
237
 
238
+ /**
239
+ * Ensures that the environment in which the linter is running has the correct versions of the required dependencies.
240
+ *
241
+ * @param {ReturnType<isNpmOrYarn>} npmOrYarn - This should be the return value of `isNpmOrYarn()`.
242
+ * @returns {void}
243
+ */
244
+ function validateEnvironment (npmOrYarn) {
245
+ const outdatedOverrides = validatePackageOverrides();
246
+
247
+ if (outdatedOverrides.overrides.length > 0 || outdatedOverrides.resolutions.length > 0) {
248
+ if (outdatedOverrides.overrides.length > 0) {
249
+ process.stderr.write(`Outdated "overrides" in package.json detected:\n- ${outdatedOverrides.overrides.map((dependency) => `${dependency.name}: ${dependency.configuredVersion} is configured, but ${dependency.expectedVersion} is expected`).join('\n- ')}\n\n`);
250
+ }
251
+
252
+ if (outdatedOverrides.resolutions.length > 0) {
253
+ process.stderr.write(`Outdated "resolutions" in package.json detected:\n- ${outdatedOverrides.resolutions.map((dependency) => `${dependency.name}: ${dependency.configuredVersion} is configured, but ${dependency.expectedVersion} is expected`).join('\n- ')}\n\n`);
254
+ }
255
+
256
+ process.exitCode = 1;
257
+
258
+ return;
259
+ }
260
+
261
+ const missingOverrides = findMissingOverrides().filter(({ name }) => !(npmOrYarn === 'npm' && outdatedOverrides.overrides.some((override) => name === override.name)) && !(npmOrYarn === 'yarn' && outdatedOverrides.resolutions.some((override) => name === override.name)));
262
+
263
+ if (missingOverrides.length > 0) {
264
+ let installCommand;
265
+ let propertyName;
266
+
267
+ if (npmOrYarn === 'yarn') {
268
+ installCommand = 'yarn install';
269
+ propertyName = 'resolutions';
270
+ }
271
+ else {
272
+ installCommand = 'npm install';
273
+ propertyName = 'overrides';
274
+ }
275
+
276
+ process.stderr.write(`The installed version of ${missingOverrides.length === 1 ? 'one dependency' : `${missingOverrides.length} dependencies`} does not match to the version required by the linter-bundle:\n`);
277
+ process.stderr.write(`- ${missingOverrides.map((dependency) => `${dependency.name}: ${dependency.configuredVersion} is installed, but ${dependency.expectedVersion} is expected`).join('\n- ')}\n\n`);
278
+ process.stderr.write(`This could be caused by forgetting to execute \`${installCommand}\` after changing a version number in the package.json, or by some other package shipping outdated versions of the ${missingOverrides.length === 1 ? 'dependency' : 'dependencies'}.\n`);
279
+ process.stderr.write('If another package is causing this problem, you can fix it by adding the following entry to your package.json:\n');
280
+ process.stderr.write(`{\n "${propertyName}: {\n ${missingOverrides.map((dependency) => `"${dependency.name}": "${dependency.expectedVersion}"`).join(',\n ')}\n }\n}\n\n`);
281
+
282
+ process.exitCode = 1;
283
+
284
+ return;
285
+ }
286
+ }
287
+
249
288
  /**
250
289
  * Extracts the tasks which should be run from the command-line arguments passed in.
251
290
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linter-bundle",
3
- "version": "2.21.0",
3
+ "version": "2.23.0",
4
4
  "description": "Ready-to use bundle of linting tools, containing configurations for ESLint, stylelint and markdownlint.",
5
5
  "keywords": [
6
6
  "eslint",
@@ -40,14 +40,14 @@
40
40
  "check-outdated": "npx --yes -- check-outdated --ignore-pre-releases"
41
41
  },
42
42
  "dependencies": {
43
- "@typescript-eslint/eslint-plugin": "5.37.0",
44
- "@typescript-eslint/parser": "5.37.0",
45
- "@typescript-eslint/utils": "5.37.0",
43
+ "@typescript-eslint/eslint-plugin": "5.38.0",
44
+ "@typescript-eslint/parser": "5.38.0",
45
+ "@typescript-eslint/utils": "5.38.0",
46
46
  "eslint": "8.23.1",
47
47
  "eslint-import-resolver-typescript": "3.5.1",
48
48
  "eslint-import-resolver-webpack": "0.13.2",
49
49
  "eslint-plugin-eslint-comments": "3.2.0",
50
- "eslint-plugin-functional": "4.3.1",
50
+ "eslint-plugin-functional": "4.4.0",
51
51
  "eslint-plugin-import": "2.26.0",
52
52
  "eslint-plugin-jest": "27.0.4",
53
53
  "eslint-plugin-jsdoc": "39.3.6",
@@ -59,8 +59,8 @@
59
59
  "eslint-plugin-unicorn": "43.0.2",
60
60
  "markdownlint-cli": "0.32.2",
61
61
  "micromatch": "4.0.5",
62
- "postcss-scss": "4.0.4",
63
- "stylelint": "14.11.0",
62
+ "postcss-scss": "4.0.5",
63
+ "stylelint": "14.12.1",
64
64
  "stylelint-declaration-block-no-ignored-properties": "2.5.0",
65
65
  "stylelint-order": "5.0.0",
66
66
  "stylelint-scss": "4.3.0",