linter-bundle 7.0.0 → 7.1.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/.linter-bundle.js CHANGED
@@ -18,11 +18,11 @@ export default {
18
18
  '.git/**',
19
19
  '.github/FUNDING.yml',
20
20
  '.vscode/settings.json',
21
- 'eslint/rules/package.json',
22
21
  `eslint/rules/${snippets.kebabCase}.{js,mjs,md}`,
22
+ `eslint/rules/helper/${snippets.kebabCase}.{js,mjs,md}`,
23
23
  `eslint/${snippets.kebabCase}.mjs`,
24
24
  'files/index.js',
25
- `helper/${snippets.kebabCase}.{js,cjs,mjs,d.ts}`,
25
+ `helper/${snippets.kebabCase}.{js,mjs,cjs,d.ts}`,
26
26
  'markdownlint/base.json',
27
27
  'node_modules/**',
28
28
  'stylelint/index.mjs',
package/CHANGELOG.md CHANGED
@@ -6,7 +6,27 @@ 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/v7.0.0...HEAD)
9
+ [Show all code changes](https://github.com/jens-duttke/linter-bundle/compare/v7.1.0...HEAD)
10
+
11
+ ## [7.1.0] - 2025-03-15
12
+
13
+ ### Fixed
14
+
15
+ - [eslint/jest] Fix code for Jest Version detection
16
+
17
+ ### Changed
18
+
19
+ - [general] Suppress Node.js warnings, such as 'ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time' (which is triggered by ESLint)
20
+ - [eslint] Added custom linter rule [`linter-bundle/enforce-logical-expression-parens`](./eslint/rules/enforce-logical-expression-parens.md) which enforces parentheses around logical operations
21
+ - [eslint] Added custom linter rule [`linter-bundle/enforce-ternary-parens`](./eslint/rules/enforce-ternary-parens.md) which ensures ternary expressions are wrapped in parentheses
22
+ - [eslint] Added custom linter rule [`linter-bundle/no-extra-spaces-in-generics`](./eslint/rules/no-extra-spaces-in-generics.md) to disallows spaces after the `<` and before the `>` in TypeScript generics
23
+ - [eslint] Added custom linter rule [`linter-bundle/no-ternary-return`](./eslint/rules/no-ternary-return.md) which disallows ternary expressions as return values for better readability
24
+ - [eslint] Configure [`padding-line-between-statements`](https://eslint.org/docs/latest/rules/padding-line-between-statements) to enforce line-breaks before `return`, `throw`, `break`, `continue`, around multi-line block statements and around `const`, `let`, `var` groups
25
+ - [eslint/react] Added custom linter rule [`linter-bundle/ensure-lucide-import-consistency`](./eslint/rules/ensure-lucide-import-consistency.md) to enforces using [Lucide](https://lucide.dev/guide/packages/lucide-react) prefix for lucide-react imports and their usage
26
+ - [eslint] Updated `eslint-import-resolver-typescript` from `3.8.4` to `3.9.0`
27
+ - [stylelint] Updated `stylelint` from `16.15.0` to `16.16.0`
28
+
29
+ [Show all code changes](https://github.com/jens-duttke/linter-bundle/compare/v7.0.0...v7.1.0)
10
30
 
11
31
  ## [7.0.0] - 2025-03-11
12
32
 
@@ -145,6 +165,7 @@ Beside these changes:
145
165
  - [eslint] Make use of [`@typescript-eslint/prefer-promise-reject-errors`](https://typescript-eslint.io/rules/prefer-promise-reject-errors/) rule
146
166
  - [markdownlint] Make use of new [`MD054`/link-image-style](https://github.com/DavidAnson/markdownlint/blob/main/doc/md054.md) rule
147
167
  - [markdownlint] Make use of new [`MD056`/table-column-count](https://github.com/DavidAnson/markdownlint/blob/main/doc/md056.md) rule
168
+ - [audit] Updated `improved-yarn-audit` from `3.0.0` to `3.0.3`
148
169
 
149
170
  ### Added
150
171
 
package/README.md CHANGED
@@ -40,8 +40,13 @@ The `linter-bundle` is using the Flat Configuration Format which was introduced
40
40
 
41
41
  Beside that, the following additional rules are part of this bundle:
42
42
 
43
- - [linter-bundle/no-unnecessary-typeof](./eslint/rules/no-unnecessary-typeof.md)
44
- - [linter-bundle/restricted-filenames](./eslint/rules/restricted-filenames.md)
43
+ - [`linter-bundle/enforce-logical-expression-parens`](./eslint/rules/enforce-logical-expression-parens.md)
44
+ - [`linter-bundle/enforce-ternary-parens`](./eslint/rules/enforce-ternary-parens.md)
45
+ - [`linter-bundle/ensure-lucide-import-consistency`](./eslint/rules/ensure-lucide-import-consistency.md)
46
+ - [`linter-bundle/no-extra-spaces-in-generics`](./eslint/rules/no-extra-spaces-in-generics.md)
47
+ - [`linter-bundle/no-ternary-return`](./eslint/rules/no-ternary-return.md)
48
+ - [`linter-bundle/no-unnecessary-typeof`](./eslint/rules/no-unnecessary-typeof.md)
49
+ - [`linter-bundle/restricted-filenames`](./eslint/rules/restricted-filenames.md)
45
50
 
46
51
  ### stylelint
47
52
 
package/TODO.md ADDED
File without changes
package/eslint/index.mjs CHANGED
@@ -22,6 +22,10 @@ import * as typescriptEslint from 'typescript-eslint';
22
22
  import * as ensureType from '../helper/ensure-type.mjs';
23
23
  import { linterBundleConfig } from '../helper/linter-bundle-config.js';
24
24
 
25
+ import enforceLogicalExpressionParens from './rules/enforce-logical-expression-parens.mjs';
26
+ import enforceTernaryParensRule from './rules/enforce-ternary-parens.mjs';
27
+ import noExtraSpacesInGenericsRule from './rules/no-extra-spaces-in-generics.mjs';
28
+ import noTernaryReturnRule from './rules/no-ternary-return.mjs';
25
29
  import noUnnecessaryTypeofRule from './rules/no-unnecessary-typeof.mjs';
26
30
  import restrictedFilenamesRule from './rules/restricted-filenames.mjs';
27
31
 
@@ -47,6 +51,10 @@ export default [
47
51
 
48
52
  'linter-bundle': {
49
53
  rules: {
54
+ 'enforce-logical-expression-parens': enforceLogicalExpressionParens,
55
+ 'enforce-ternary-parens': enforceTernaryParensRule,
56
+ 'no-extra-spaces-in-generics': noExtraSpacesInGenericsRule,
57
+ 'no-ternary-return': noTernaryReturnRule,
50
58
  'no-unnecessary-typeof': noUnnecessaryTypeofRule,
51
59
  'restricted-filenames': restrictedFilenamesRule
52
60
  }
@@ -133,6 +141,10 @@ export default [
133
141
  /**
134
142
  * ./rules
135
143
  */
144
+ 'linter-bundle/enforce-logical-expression-parens': 'error',
145
+ 'linter-bundle/enforce-ternary-parens': 'error',
146
+ 'linter-bundle/no-extra-spaces-in-generics': 'error',
147
+ 'linter-bundle/no-ternary-return': 'error',
136
148
  'linter-bundle/no-unnecessary-typeof': 'error',
137
149
 
138
150
  /**
@@ -374,7 +386,14 @@ export default [
374
386
  'operator-assignment': 'error',
375
387
  'operator-linebreak': ['error', 'after', { overrides: { ':': 'ignore' } }],
376
388
  'padded-blocks': ['error', 'never'],
377
- 'padding-line-between-statements': 'error',
389
+ 'padding-line-between-statements': [
390
+ 'error',
391
+ { blankLine: 'always', prev: '*', next: ['return', 'throw', 'break', 'continue'] },
392
+ { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' },
393
+ { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] },
394
+ { blankLine: 'always', prev: '*', next: 'multiline-block-like' },
395
+ { blankLine: 'always', prev: 'multiline-block-like', next: '*' }
396
+ ],
378
397
  'prefer-arrow-callback': ['error', { allowNamedFunctions: true }],
379
398
  'prefer-const': 'error',
380
399
  'prefer-destructuring': 'off', // We don't prefer destructuring if a type is specified ['error', { VariableDeclarator: { array: true, object: true }, AssignmentExpression: { array: false, object: false } }],
package/eslint/jest.mjs CHANGED
@@ -155,10 +155,13 @@ export default [
155
155
  async function getJestVersion () {
156
156
  try {
157
157
  const require = createRequire(import.meta.url);
158
+ const fileUrl = new URL('file:');
158
159
 
159
- const jestModule = await import(require.resolve('jest', { paths: [process.cwd()] }));
160
+ fileUrl.pathname = require.resolve('jest', { paths: [process.cwd()] });
160
161
 
161
- const version = jestModule.getVersion().split('.')[0];
162
+ const jestModule = await import(fileUrl.toString());
163
+ const jest = ('default' in jestModule ? jestModule.default : jestModule);
164
+ const version = jest.getVersion().split('.')[0];
162
165
 
163
166
  process.stdout.write(`Detected Jest version: ${version}\n\n`);
164
167
 
package/eslint/react.mjs CHANGED
@@ -9,17 +9,29 @@ import * as reactHooksPlugin from 'eslint-plugin-react-hooks';
9
9
  import * as ensureType from '../helper/ensure-type.mjs';
10
10
  import { linterBundleConfig } from '../helper/linter-bundle-config.js';
11
11
 
12
+ import ensureLucideImportConsistencyRule from './rules/ensure-lucide-import-consistency.mjs';
13
+
12
14
  export default [
13
15
  {
14
16
  plugins: {
15
17
  'react-hooks': reactHooksPlugin,
16
18
  'react': reactPlugin,
17
- '@stylistic/jsx': stylisticJSXPlugin
19
+ '@stylistic/jsx': stylisticJSXPlugin,
20
+ 'linter-bundle': {
21
+ rules: {
22
+ 'ensure-lucide-import-consistency': ensureLucideImportConsistencyRule
23
+ }
24
+ }
18
25
  }
19
26
  },
20
27
  {
21
28
  files: ['**/*.tsx'],
22
29
  rules: {
30
+ /**
31
+ * ./rules
32
+ */
33
+ 'linter-bundle/ensure-lucide-import-consistency': 'error',
34
+
23
35
  /**
24
36
  * typescript-eslint
25
37
  *
@@ -0,0 +1,31 @@
1
+ # Enforce parentheses around logical operations (`linter-bundle/enforce-logical-expression-parens`)
2
+
3
+ ## Rule Details
4
+
5
+ This rule ensures that logical operations (using `&&`, `||`, etc.) are always enclosed in parentheses unless they are already part of an existing expression. This improves readability and prevents potential issues with operator precedence.
6
+
7
+ Examples of **incorrect** code for this rule:
8
+
9
+ ```ts
10
+ return foo !== null && foo.bar === 42 && baz === 84;
11
+ ```
12
+
13
+ The logical operations in the above code are not enclosed in parentheses. The rule will automatically add parentheses to make the code clearer and ensure proper precedence.
14
+
15
+ Corrected code would look like this:
16
+
17
+ ```ts
18
+ return (foo !== null && foo.bar === 42 && baz === 84);
19
+ ```
20
+
21
+ ### When the rule will not apply
22
+
23
+ If the logical operations are already inside parentheses, no changes will be made:
24
+
25
+ ```ts
26
+ if (foo !== null && foo.bar === 42 && baz === 84) {
27
+ // code
28
+ }
29
+ ```
30
+
31
+ In this case, the code is already correctly wrapped in parentheses, so no further modifications are necessary.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file ESLint rule which ensures logical operations are wrapped in parentheses.
3
+ */
4
+
5
+ import { isParenthesized } from './helper/is-parenthesized.mjs';
6
+
7
+ /**
8
+ * @type {import('eslint').Rule.RuleModule}
9
+ */
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Add parentheses around logical operations if not already present',
15
+ category: 'Best Practices',
16
+ recommended: false
17
+ },
18
+ fixable: 'code'
19
+ },
20
+ create (context) {
21
+ return {
22
+ LogicalExpression (node) {
23
+ // Check if the parent node is a logical expression
24
+ if (node.parent.type === 'LogicalExpression' || isParenthesized(context, node)) {
25
+ return; // Do not add parentheses if they are already present
26
+ }
27
+
28
+ // If no parentheses are present, add parentheses around the logical operation
29
+ context.report({
30
+ node,
31
+ message: 'Add parentheses around the logical operation.',
32
+ fix (fixer) {
33
+ return fixer.replaceText(node, `(${context.getSourceCode().getText(node)})`);
34
+ }
35
+ });
36
+ }
37
+ };
38
+ }
39
+ };
@@ -0,0 +1,29 @@
1
+ # Enforce parentheses around ternary expressions (`linter-bundle/enforce-ternary-parens`)
2
+
3
+ ## Rule Details
4
+
5
+ This rule enforces that ternary expressions are always wrapped in parentheses to improve readability and maintain consistency in code formatting.
6
+
7
+ ### Examples of **incorrect** code for this rule
8
+
9
+ ```ts
10
+ const foo = bar ? 1 : 2;
11
+ const value = condition ? 'yes' : 'no';
12
+ ```
13
+
14
+ ### Examples of **correct** code for this rule
15
+
16
+ ```ts
17
+ const foo = (bar ? 1 : 2);
18
+ const value = (condition ? 'yes' : 'no');
19
+ ```
20
+
21
+ ## Why is this rule useful?
22
+
23
+ - **Improved readability**: Wrapping ternary expressions in parentheses makes it clear that the entire expression is evaluated together.
24
+ - **Consistency**: Ensures a uniform style when using the ternary operator.
25
+ - **Avoids ambiguity**: Helps prevent misunderstandings in complex expressions.
26
+
27
+ ## Fixable
28
+
29
+ This rule is fixable. It will automatically wrap ternary expressions in parentheses.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file ESLint rule which ensures ternary expressions are wrapped in parentheses.
3
+ */
4
+
5
+ import { isParenthesized } from './helper/is-parenthesized.mjs';
6
+
7
+ /**
8
+ * @type {import('eslint').Rule.RuleModule}
9
+ */
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ fixable: 'code',
14
+ docs: {
15
+ description: 'Requires ternary expressions to be wrapped in parentheses.',
16
+ category: 'Styling',
17
+ recommended: false
18
+ }
19
+ },
20
+
21
+ create (context) {
22
+ return {
23
+ ConditionalExpression (node) {
24
+ if (isParenthesized(context, node)) {
25
+ return;
26
+ }
27
+
28
+ context.report({
29
+ node,
30
+ message: 'Ternary expressions must be wrapped in parentheses.',
31
+ fix (fixer) {
32
+ // Wrap the entire ternary expression in parentheses.
33
+ return fixer.replaceText(node, `(${context.sourceCode.getText(node)})`);
34
+ }
35
+ });
36
+ }
37
+ };
38
+ }
39
+ };
@@ -0,0 +1,38 @@
1
+ # Enforces using Lucide prefix for lucide-react imports and their usage (`linter-bundle/ensure-lucide-import-consistency`)
2
+
3
+ ## Rule Details
4
+
5
+ This rule ensures that components imported from `lucide-react` are used with a `Lucide` prefix. It checks both the import statements and the JSX or JavaScript usage of these components.
6
+ If a component does not follow this convention, it will be automatically fixed by replacing the component name with the properly prefixed name (`Lucide`).
7
+
8
+ ### Correct Usage
9
+
10
+ When importing components from `lucide-react`, the component name should start with the `Lucide` prefix.
11
+
12
+ #### Correct Code Example
13
+
14
+ ```ts
15
+ import { LucideHome } from 'lucide-react';
16
+
17
+ <LucideHome />
18
+ ```
19
+
20
+ #### Incorrect Code Example
21
+
22
+ ```ts
23
+ import { Home } from 'lucide-react';
24
+
25
+ <Home />
26
+ ```
27
+
28
+ ### Rule Behavior
29
+
30
+ - **Import Declarations**: If a component from `lucide-react` is imported without the `Lucide` prefix, the rule will automatically rename it to add the `Lucide` prefix.
31
+
32
+ - **JSX Usage**: In JSX, if a component is used without the `Lucide` prefix (even if correctly imported), it will be reported and fixed.
33
+
34
+ - **JavaScript Usage**: For non-JSX usage (e.g., when components are referenced via `React.createElement` or directly as identifiers), the rule will also ensure the correct `Lucide` prefix is applied.
35
+
36
+ ## Fixable
37
+
38
+ This rule is fixable. It will automatically rename components and their usages to ensure they follow the `Lucide` prefix convention.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file ESLint rule which enforces using Lucide prefix for lucide-react imports and their usage.
3
+ */
4
+
5
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
6
+
7
+ /**
8
+ * @typedef {'alternative'} MessageIds
9
+ */
10
+
11
+ /** @type {import('@typescript-eslint/utils/ts-eslint').RuleModule<MessageIds>} */
12
+ export default {
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: {
16
+ description: 'Enforces using Lucide prefix for lucide-react imports and their usage',
17
+ category: 'Best Practices',
18
+ recommended: true
19
+ },
20
+ messages: {
21
+ alternative: 'Please use "{{lucideName}}" instead of "{{componentName}}"'
22
+ },
23
+ schema: [],
24
+ fixable: 'code'
25
+ },
26
+ defaultOptions: [],
27
+ create (context) {
28
+ // Track renamed imports from lucide-react
29
+ const renamedImports = new Map();
30
+
31
+ return {
32
+ ImportDeclaration (node) {
33
+ if (node.source.value === 'lucide-react') {
34
+ for (const specifier of node.specifiers) {
35
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
36
+ const importedName = /** @type {any} */(specifier.imported).name;
37
+ const localName = specifier.local.name;
38
+
39
+ if (!importedName.startsWith('Lucide')) {
40
+ const lucideName = `Lucide${importedName.replace(/Icon$/u, '')}`;
41
+
42
+ // Store the mapping of local name to Lucide name
43
+ renamedImports.set(localName, lucideName);
44
+
45
+ context.report({
46
+ node: specifier,
47
+ messageId: 'alternative',
48
+ data: { lucideName, importedName },
49
+ fix (fixer) {
50
+ return fixer.replaceText(
51
+ specifier.imported,
52
+ lucideName
53
+ );
54
+ }
55
+ });
56
+ }
57
+ }
58
+ }
59
+ }
60
+ },
61
+
62
+ // Fix JSX usage
63
+ JSXIdentifier (node) {
64
+ // Only check opening elements, not attributes
65
+ if (node.parent.type === AST_NODE_TYPES.JSXOpeningElement || node.parent.type === AST_NODE_TYPES.JSXClosingElement) {
66
+ const componentName = node.name;
67
+
68
+ if (renamedImports.has(componentName)) {
69
+ /** @type {string} */
70
+ const lucideName = renamedImports.get(componentName);
71
+
72
+ context.report({
73
+ node,
74
+ messageId: 'alternative',
75
+ data: { lucideName, componentName },
76
+ fix (fixer) {
77
+ return fixer.replaceText(node, lucideName);
78
+ }
79
+ });
80
+ }
81
+ }
82
+ },
83
+
84
+ // Fix JS usage (for non-JSX cases like React.createElement)
85
+ Identifier (node) {
86
+ // Exclude import declarations (already handled) and JSX (handled separately)
87
+ if (
88
+ node.parent.type !== AST_NODE_TYPES.ImportSpecifier &&
89
+ node.parent.type !== AST_NODE_TYPES.JSXIdentifier &&
90
+ node.parent.type !== AST_NODE_TYPES.JSXOpeningElement &&
91
+ node.parent.type !== AST_NODE_TYPES.JSXClosingElement
92
+ ) {
93
+ const componentName = node.name;
94
+
95
+ if (renamedImports.has(componentName)) {
96
+ /** @type {string} */
97
+ const lucideName = renamedImports.get(componentName);
98
+
99
+ context.report({
100
+ node,
101
+ messageId: 'alternative',
102
+ data: { lucideName, componentName },
103
+ fix (fixer) {
104
+ return fixer.replaceText(node, lucideName);
105
+ }
106
+ });
107
+ }
108
+ }
109
+ }
110
+ };
111
+ }
112
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @file ESLint helper function to check if an ESLint Rule Node is wrapped in parentheses.
3
+ */
4
+
5
+ /**
6
+ * Helper function to check if the node is wrapped in parentheses.
7
+ *
8
+ * @param {import('eslint').Rule.RuleContext} context - The rule context.
9
+ * @param {import('eslint').Rule.Node} node - The expression node.
10
+ * @returns {boolean} Returns `true` with the node is wrapped by parens.
11
+ */
12
+ export function isParenthesized (context, node) {
13
+ const { sourceCode } = context;
14
+
15
+ const firstToken = sourceCode.getFirstToken(node);
16
+
17
+ if (!firstToken) {
18
+ return false;
19
+ }
20
+
21
+ const lastToken = sourceCode.getLastToken(node);
22
+
23
+ if (!lastToken) {
24
+ return false;
25
+ }
26
+
27
+ const tokenBefore = sourceCode.getTokenBefore(firstToken);
28
+
29
+ if (!tokenBefore) {
30
+ return false;
31
+ }
32
+
33
+ const tokenAfter = sourceCode.getTokenAfter(lastToken);
34
+
35
+ if (!tokenAfter) {
36
+ return false;
37
+ }
38
+
39
+ return (tokenBefore.value === '(' && tokenAfter.value === ')');
40
+ }
@@ -0,0 +1,25 @@
1
+ # Disallow spaces in TypeScript generics (`linter-bundle/no-spaces-in-generics`)
2
+
3
+ ## Rule Details
4
+
5
+ This rule disallows spaces after the `<` and before the `>` in TypeScript generics. Ensuring that no unnecessary spaces are used around generics helps maintain consistency and readability in your code.
6
+
7
+ Examples of **incorrect** code for this rule:
8
+
9
+ ```ts
10
+ declare function foo< T>(x: T): T; // Space after '<'
11
+
12
+ function bar<U >(): U { // Space before '>'
13
+ return null as U;
14
+ }
15
+ ```
16
+
17
+ Examples of **correct** code for this rule:
18
+
19
+ ```ts
20
+ declare function foo<T>(x: T): T;
21
+
22
+ function bar<U>(): U {
23
+ return null as U;
24
+ }
25
+ ```
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @file ESLint rule which disallows spaces after '<' and before '>' in TypeScript generics.
3
+ */
4
+
5
+ /**
6
+ * @typedef {'noSpaceAfterLessThan' | 'noSpaceBeforeGreaterThan'} MessageIds
7
+ */
8
+
9
+ /** @type {import('@typescript-eslint/utils/ts-eslint').RuleModule<MessageIds>} */
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: "Disallow spaces after '<' and before '>' in TypeScript generics.",
15
+ category: 'Stylistic Issues',
16
+ recommended: false
17
+ },
18
+ fixable: 'whitespace',
19
+ schema: [],
20
+ messages: {
21
+ noSpaceAfterLessThan: "No space allowed after '<' in generics.",
22
+ noSpaceBeforeGreaterThan: "No space allowed before '>' in generics."
23
+ }
24
+ },
25
+ defaultOptions: [],
26
+ create (context) {
27
+ return {
28
+ TSTypeParameterInstantiation (node) {
29
+ const text = context.sourceCode.getText(node);
30
+
31
+ // Check for space after "<"
32
+ if ((/<[ \t]+/u).test(text)) {
33
+ context.report({
34
+ node,
35
+ messageId: 'noSpaceAfterLessThan',
36
+ fix (fixer) {
37
+ return fixer.replaceText(node, text.replace(/<[ \t]+/u, '<'));
38
+ }
39
+ });
40
+ }
41
+
42
+ // Check for space before ">"
43
+ if ((/[ \t]+>/u).test(text)) {
44
+ context.report({
45
+ node,
46
+ messageId: 'noSpaceBeforeGreaterThan',
47
+ fix (fixer) {
48
+ return fixer.replaceText(node, text.replace(/[ \t]+>/u, '>'));
49
+ }
50
+ });
51
+ }
52
+ }
53
+ };
54
+ }
55
+ };
@@ -0,0 +1,29 @@
1
+ # Disallow ternary expressions as return values (`linter-bundle/no-ternary-return`)
2
+
3
+ ## Rule Details
4
+
5
+ To improve code readability, this rule disallows using ternary expressions directly as return values. Instead, it enforces using explicit `if` statements.
6
+
7
+ ### Examples of **incorrect** code for this rule
8
+
9
+ ```ts
10
+ function getValue(condition: boolean) {
11
+ return condition ? "yes" : "no";
12
+ }
13
+ ```
14
+
15
+ ### Examples of **correct** code for this rule
16
+
17
+ ```ts
18
+ function getValue(condition: boolean) {
19
+ if (condition) {
20
+ return "yes";
21
+ }
22
+
23
+ return "no";
24
+ }
25
+ ```
26
+
27
+ ## Fixable
28
+
29
+ This rule is **fixable**. The autofix will transform ternary return expressions into an `if` statement followed by a separate `return`.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file ESLint rule which disallows ternary expressions as return values for better readability.
3
+ */
4
+
5
+ /**
6
+ * @type {import('eslint').Rule.RuleModule}
7
+ */
8
+ export default {
9
+ meta: {
10
+ type: 'suggestion',
11
+ docs: {
12
+ description: 'Disallow ternary expressions as return values for better readability',
13
+ category: 'Stylistic Issues',
14
+ recommended: true
15
+ },
16
+ fixable: 'code',
17
+ schema: []
18
+ },
19
+ create (context) {
20
+ return {
21
+ ReturnStatement (node) {
22
+ if (node.argument && node.argument.type === 'ConditionalExpression') {
23
+ context.report({
24
+ node,
25
+ message: 'Avoid using ternary expressions as return values; use an if-else statement instead.',
26
+ fix (fixer) {
27
+ const sourceCode = context.sourceCode;
28
+ const argumentText = /** @type {any} */(node.argument)?.test;
29
+ const argumentConsequent = /** @type {any} */(node.argument)?.consequent;
30
+ const argumentAlternate = /** @type {any} */(node.argument)?.alternate;
31
+
32
+ if (!argumentText || !argumentConsequent || !argumentAlternate) {
33
+ return null;
34
+ }
35
+
36
+ const test = sourceCode.getText(argumentText);
37
+ const consequent = sourceCode.getText(argumentConsequent);
38
+ const alternate = sourceCode.getText(argumentAlternate);
39
+ const indent = (/^\s*/u).exec(sourceCode.getText(node))?.[0];
40
+
41
+ const fixedCode = `if (${test}) {\n${indent} return ${consequent};\n}\n\n${indent}return ${alternate};`;
42
+
43
+ return fixer.replaceText(node, fixedCode);
44
+ }
45
+ });
46
+ }
47
+ }
48
+ };
49
+ }
50
+ };
@@ -8,6 +8,11 @@ import * as ts from 'typescript';
8
8
 
9
9
  import { ESLintUtils } from '@typescript-eslint/utils';
10
10
 
11
+ /**
12
+ * @typedef {'text'} MessageIds
13
+ */
14
+
15
+ /** @type {import('@typescript-eslint/utils/ts-eslint').RuleModule<MessageIds>} */
11
16
  export default {
12
17
  meta: {
13
18
  type: 'problem',
@@ -18,23 +23,13 @@ export default {
18
23
  },
19
24
  messages: {
20
25
  text: 'Unnecessary `typeof`, because the only possible type of {{ variableName }} is `{{ typeName }}`.'
21
- }
26
+ },
27
+ schema: []
22
28
  },
29
+ defaultOptions: [],
23
30
 
24
- /**
25
- * Create a new rule.
26
- *
27
- * @param {import('@typescript-eslint/utils/ts-eslint').RuleContext<any, any>} context - RuleContext of @typescript-eslint instead of ESlint
28
- * @returns {import('@typescript-eslint/utils/ts-eslint').RuleListener} RuleListener of @typescript-eslint, instead of ESlint
29
- */
30
31
  create (context) {
31
32
  return {
32
- /**
33
- * Rule function to handle unary expressions.
34
- *
35
- * @param {import('@typescript-eslint/typescript-estree').TSESTree.UnaryExpression} node - UnaryExpression of @typescript-eslint instead of ESlint
36
- * @returns {import('@typescript-eslint/utils/ts-eslint').RuleFunction<import('@typescript-eslint/typescript-estree').TSESTree.UnaryExpression> | void} RuleFunction of @typescript-eslint instead of ESlint
37
- */
38
33
  UnaryExpression (node) {
39
34
  if (node.operator !== 'typeof') {
40
35
  return;
@@ -9,7 +9,6 @@ import micromatch from 'micromatch';
9
9
  // eslint-disable-next-line n/no-process-env -- If the ESLint sub-process is running from within the linter-bundle, we make use of its configuration.
10
10
  const isInLinterBundle = !process.env['LINTER_BUNDLE'];
11
11
 
12
- // eslint-disable-next-line n/no-unpublished-import -- @todo Is that a false-positive?
13
12
  const { linterBundleConfig } = (isInLinterBundle ? await import('../../helper/linter-bundle-config.js') : {});
14
13
 
15
14
  /**
@@ -56,7 +55,7 @@ export default {
56
55
  create: (context) => {
57
56
  const filePath = context.filename;
58
57
  /** @type {{ basePath: string, allowed?: string[]; disallowed?: string[]; }[]} */
59
- const options = linterBundleConfig?.files?.restrictions ? [...linterBundleConfig.files.restrictions, ...context.options] : context.options;
58
+ const options = (linterBundleConfig?.files?.restrictions ? [...linterBundleConfig.files.restrictions, ...context.options] : context.options);
60
59
 
61
60
  for (const { basePath, allowed, disallowed } of options) {
62
61
  const normalizedName = path.relative(path.join(process.cwd(), basePath), filePath);
@@ -13,6 +13,11 @@
13
13
  * @returns {T extends Array<any> ? T : []} Either the input array, or an empty array, if the input array is not an array
14
14
  */
15
15
  export function array (value) {
16
+ if (Array.isArray(value)) {
17
+ // @ts-expect-error -- Right now the type definition of `Array.isArray()` is incorrect since it uses `arg is any[]` instead of the correct type of `arg`.
18
+ return value;
19
+ }
20
+
16
21
  // @ts-expect-error -- Right now the type definition of `Array.isArray()` is incorrect since it uses `arg is any[]` instead of the correct type of `arg`.
17
- return (Array.isArray(value) ? value : []);
22
+ return [];
18
23
  }
@@ -41,7 +41,7 @@ export async function getGitFiles () {
41
41
  gitFiles = [
42
42
  ...gitProcessResult.diff.stdout.trim().split('\0'),
43
43
  ...gitProcessResult.modified.stdout.trim().split('\0')
44
- ].filter((file, index, self) => !deletedFiles.includes(file) && self.indexOf(file) === index);
44
+ ].filter((file, index, self) => (!deletedFiles.includes(file) && self.indexOf(file) === index));
45
45
  }
46
46
 
47
47
  return gitFiles;
@@ -31,9 +31,11 @@ export async function runProcess (command, options) {
31
31
  // eslint-disable-next-line n/no-process-env -- Pass all environment variables to the child process
32
32
  ...process.env,
33
33
  ...options?.env,
34
- LINTER_BUNDLE: '1'
34
+ LINTER_BUNDLE: '1',
35
+ FORCE_COLOR: 'true',
36
+ NODE_NO_WARNINGS: '1'
35
37
  },
36
- shell: os.userInfo().shell ?? undefined
38
+ shell: (os.userInfo().shell ?? undefined)
37
39
  });
38
40
 
39
41
  lintingProcess.stdout?.on('data', (/** @type {string} */data) => {
@@ -45,7 +47,7 @@ export async function runProcess (command, options) {
45
47
  });
46
48
 
47
49
  lintingProcess.on('exit', (code) => resolve({
48
- code: code ?? 0,
50
+ code: (code ?? 0),
49
51
  stdout: stdout.join(''),
50
52
  stderr: stderr.join(''),
51
53
  runtime: performance.now() - startTimestamp
package/lint.js CHANGED
@@ -319,7 +319,7 @@ async function runAuditTask (taskName, taskConfig) {
319
319
  '--',
320
320
  'better-npm-audit@3.11.0',
321
321
  'audit',
322
- `-l ${newTaskConfig.minSeverity?.[0] ?? 'moderate'}`,
322
+ `-l ${(newTaskConfig.minSeverity?.[0] ?? 'moderate')}`,
323
323
  '-p',
324
324
  newTaskConfig.exclude?.map((exclude) => `-i ${exclude}`).join(' ')
325
325
  ].filter((argument) => Boolean(argument)).join(' ')
@@ -333,8 +333,8 @@ async function runAuditTask (taskName, taskConfig) {
333
333
  'npx',
334
334
  '--yes',
335
335
  '--',
336
- 'improved-yarn-audit@3.0.0',
337
- `--min-severity ${newTaskConfig.minSeverity?.[0] ?? 'moderate'}`,
336
+ 'improved-yarn-audit@3.0.3',
337
+ `--min-severity ${(newTaskConfig.minSeverity?.[0] ?? 'moderate')}`,
338
338
  '--fail-on-missing-exclusions',
339
339
  '--ignore-dev-deps',
340
340
  newTaskConfig.exclude?.map((exclude) => `--exclude ${exclude}`).join(' ')
@@ -378,7 +378,7 @@ async function validateEnvironment () {
378
378
  }
379
379
 
380
380
  const outdatedDependencies = await getOutdatedDependencies();
381
- const missingOverrides = outdatedDependencies.filter(({ name }) => !(npmOrYarn === 'npm' && outdatedOverrides.overrides.some((override) => name === override.name)) && !(npmOrYarn === 'yarn' && outdatedOverrides.resolutions.some((override) => name === override.name)));
381
+ const missingOverrides = outdatedDependencies.filter(({ name }) => (!(npmOrYarn === 'npm' && outdatedOverrides.overrides.some((override) => name === override.name)) && !(npmOrYarn === 'yarn' && outdatedOverrides.resolutions.some((override) => name === override.name))));
382
382
 
383
383
  if (missingOverrides.length > 0) {
384
384
  let installCommand;
@@ -393,9 +393,9 @@ async function validateEnvironment () {
393
393
  propertyName = 'overrides';
394
394
  }
395
395
 
396
- 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`);
396
+ 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`);
397
397
  process.stderr.write(`- ${missingOverrides.map((dependency) => `${dependency.name}: ${dependency.configuredVersion} is installed, but ${dependency.expectedVersion} is expected`).join('\n- ')}\n\n`);
398
- 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`);
398
+ 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`);
399
399
  process.stderr.write('If another package is causing this problem, you can fix it by adding the following entry to your package.json:\n');
400
400
  process.stderr.write(`{\n "${propertyName}": {\n ${missingOverrides.map((dependency) => `"${dependency.name}": "${dependency.expectedVersion}"`).join(',\n ')}\n }\n}\n\n`);
401
401
 
@@ -477,7 +477,7 @@ function getTasksToRun (argv) {
477
477
  async function getIncludes (taskConfig, pattern) {
478
478
  const include = taskConfig['include'];
479
479
 
480
- let includedFiles = (Array.isArray(include) && include.length > 0 ? /** @type {string[]} */(include.filter((item) => typeof item === 'string')) : undefined);
480
+ let includedFiles = ((Array.isArray(include) && include.length > 0) ? /** @type {string[]} */(include.filter((item) => typeof item === 'string')) : undefined);
481
481
 
482
482
  if (taskConfig['git']?.[0]) {
483
483
  const gitFiles = await getGitFiles();
@@ -498,7 +498,11 @@ async function getIncludes (taskConfig, pattern) {
498
498
  }
499
499
 
500
500
  if (!includedFiles) {
501
- return (pattern ? `"${pattern}"` : '');
501
+ if (pattern) {
502
+ return `"${pattern}"`;
503
+ }
504
+
505
+ return '';
502
506
  }
503
507
 
504
508
  return `"${includedFiles.join('" "')}"`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linter-bundle",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "type": "module",
5
5
  "description": "Ready-to use bundle of linting tools, containing configurations for ESLint, stylelint and markdownlint.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "@stylistic/eslint-plugin-jsx": "4.2.0",
46
46
  "eslint": "9.22.0",
47
47
  "eslint-formatter-unix": "8.40.0",
48
- "eslint-import-resolver-typescript": "3.8.4",
48
+ "eslint-import-resolver-typescript": "3.9.0",
49
49
  "eslint-import-resolver-webpack": "0.13.10",
50
50
  "eslint-plugin-eslint-comments": "3.2.0",
51
51
  "eslint-plugin-functional": "9.0.1",
@@ -62,7 +62,7 @@
62
62
  "markdownlint-cli": "0.44.0",
63
63
  "micromatch": "4.0.8",
64
64
  "postcss-scss": "4.0.9",
65
- "stylelint": "16.15.0",
65
+ "stylelint": "16.16.0",
66
66
  "stylelint-declaration-block-no-ignored-properties": "2.8.0",
67
67
  "stylelint-high-performance-animation": "1.11.0",
68
68
  "stylelint-order": "6.0.4",
@@ -71,6 +71,7 @@
71
71
  "typescript-eslint": "8.26.1"
72
72
  },
73
73
  "peerDependencies": {
74
+ "@typescript-eslint/utils": "*",
74
75
  "postcss-value-parser": "*",
75
76
  "typescript": ">=4.0.0"
76
77
  },
@@ -1,8 +0,0 @@
1
- {
2
- "type": "commonjs",
3
- "peerDependencies": {
4
- "@typescript-eslint/utils": "*",
5
- "micromatch": "*",
6
- "typescript": "*"
7
- }
8
- }