lint-rules-alvin 1.0.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.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ export const jsxNoSingleObjectCurlyNewlineRule = {
5
+ meta: {
6
+ type: 'layout',
7
+ docs: {
8
+ description: 'Disallows newlines inside curly braces for single object or array expressions in JSX.',
9
+ category: 'Stylistic Issues',
10
+ recommended: false
11
+ },
12
+ fixable: 'code',
13
+ schema: [],
14
+ messages: { error: 'Newlines around single object or array expressions in JSX curly braces are not allowed.' }
15
+ },
16
+
17
+ create(context) {
18
+
19
+ const sourceCode = context.sourceCode;
20
+
21
+ return {
22
+ JSXExpressionContainer(node) {
23
+
24
+ const expression = node.expression;
25
+
26
+ // This rule applies only to single ArrayExpressions or ObjectExpressions.
27
+ if (expression.type !== 'ArrayExpression' && expression.type !== 'ObjectExpression') {
28
+
29
+ return;
30
+
31
+ }
32
+
33
+ // If the expression itself isn't multiline, there are no newlines to collapse.
34
+ if (expression.loc.start.line === expression.loc.end.line) {
35
+
36
+ return;
37
+
38
+ }
39
+
40
+ const openingBrace = sourceCode.getFirstToken(node); // `{`
41
+ const closingBrace = sourceCode.getLastToken(node); // `}`
42
+ const firstTokenInExpression = sourceCode.getFirstToken(expression);
43
+ const lastTokenInExpression = sourceCode.getLastToken(expression);
44
+
45
+ const hasNewlineBefore = openingBrace.loc.end.line < firstTokenInExpression.loc.start.line;
46
+ const hasNewlineAfter = lastTokenInExpression.loc.end.line < closingBrace.loc.start.line;
47
+
48
+ if (hasNewlineBefore || hasNewlineAfter) {
49
+
50
+ context.report({
51
+ node,
52
+ messageId: 'error',
53
+ fix(fixer) {
54
+
55
+ const fixes = [];
56
+
57
+ if (hasNewlineBefore) {
58
+
59
+ // Range from the end of `{` to the start of the expression's first token.
60
+ const range = [
61
+ openingBrace.range[1],
62
+ firstTokenInExpression.range[0]
63
+ ];
64
+ fixes.push(
65
+ fixer.replaceTextRange(
66
+ range,
67
+ ''
68
+ )
69
+ );
70
+
71
+ }
72
+
73
+ if (hasNewlineAfter) {
74
+
75
+ // Range from the end of the expression's last token to the start of `}`.
76
+ const range = [
77
+ lastTokenInExpression.range[1],
78
+ closingBrace.range[0]
79
+ ];
80
+ fixes.push(
81
+ fixer.replaceTextRange(
82
+ range,
83
+ ''
84
+ )
85
+ );
86
+
87
+ }
88
+
89
+ return fixes;
90
+
91
+ }
92
+ });
93
+
94
+ }
95
+
96
+ }
97
+ };
98
+
99
+ }
100
+ };
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ export const maxChainPerLineRule = {
5
+ meta: {
6
+ type: 'layout',
7
+ docs: {
8
+
9
+ description: 'Require a newline after each item in a chain if it\'s too long and contains a call.',
10
+ category: 'Stylistic Issues'
11
+ },
12
+ fixable: 'whitespace',
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ maxChain: {
18
+ type: 'integer',
19
+ minimum: 1
20
+ }
21
+ },
22
+ additionalProperties: false
23
+ }
24
+ ],
25
+ messages: { expectedNewline: 'This call chain is too long and should be broken into multiple lines.' }
26
+ },
27
+ create(context) {
28
+
29
+ const options = context.options[0] || {};
30
+ const maxChain = options.maxChain || 2;
31
+ const sourceCode = context.sourceCode;
32
+
33
+ function isChainable(node) {
34
+
35
+ if (!node)
36
+ return false;
37
+ return node.type === 'MemberExpression' || node.type === 'CallExpression';
38
+
39
+ }
40
+
41
+ function check(node) {
42
+
43
+ const members = [];
44
+ let callCount = 0;
45
+ let current = node;
46
+
47
+ while (isChainable(current)) {
48
+
49
+ if (current.type === 'CallExpression') {
50
+
51
+ callCount++;
52
+ current = current.callee;
53
+
54
+ } else { // MemberExpression
55
+
56
+ members.unshift(current);
57
+ current = current.object;
58
+
59
+ }
60
+
61
+ }
62
+ const root = current;
63
+ const totalChainLength = members.length + 1;
64
+
65
+ let iter = node;
66
+ while(isChainable(iter)) {
67
+
68
+ if (iter.type === 'CallExpression') {
69
+
70
+ callCount++;
71
+
72
+ }
73
+ iter = iter.type === 'CallExpression'
74
+ ? iter.callee
75
+ : iter.object;
76
+
77
+ }
78
+
79
+ const isMultiline = root.loc.start.line !== node.loc.end.line;
80
+
81
+ if (callCount > 0) {
82
+
83
+ if (!isMultiline && totalChainLength > maxChain) {
84
+
85
+ context.report({
86
+ node: node,
87
+ messageId: 'expectedNewline',
88
+ fix(fixer) {
89
+
90
+ const fixes = [];
91
+ members.forEach(
92
+ (memberNode) => {
93
+
94
+ const propertyNode = memberNode.property;
95
+ const dotToken = sourceCode.getTokenBefore(propertyNode);
96
+ const rootLine = sourceCode
97
+ .getLines()
98
+ [root.loc.start.line - 1];
99
+ const indent = (rootLine
100
+ .match(/^\s*/)
101
+ [0] || '') + ' ';
102
+ fixes.push(
103
+ fixer.insertTextBefore(
104
+ dotToken,
105
+ '\n' + indent
106
+ )
107
+ );
108
+
109
+ }
110
+ );
111
+ return fixes;
112
+
113
+ }
114
+ });
115
+
116
+ }
117
+
118
+ }
119
+
120
+ }
121
+
122
+ return {
123
+ 'MemberExpression:exit'(node) {
124
+
125
+ if (node.parent.type === 'MemberExpression' && node.parent.object === node)
126
+ return;
127
+ if (node.parent.type === 'CallExpression' && node.parent.callee === node)
128
+ return;
129
+ check(node);
130
+
131
+ },
132
+ 'CallExpression:exit'(node) {
133
+
134
+ if (node.parent.type === 'MemberExpression' && node.parent.object === node)
135
+ return;
136
+ if (node.parent.type === 'CallExpression' && node.parent.callee === node)
137
+ return;
138
+
139
+ if (isChainable(node.callee)) {
140
+
141
+ check(node);
142
+
143
+ }
144
+
145
+ }
146
+ };
147
+
148
+ }
149
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ export const multilineParenNewlineRule = {
5
+ meta: {
6
+ type: 'layout',
7
+ docs: {
8
+ description: 'Require newlines inside parentheses of call expressions that are multiline',
9
+ category: 'Stylistic Issues',
10
+ recommended: false
11
+ },
12
+ fixable: 'whitespace',
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ singleArgument: {
18
+ type: 'boolean',
19
+ default: false
20
+ }
21
+ },
22
+ additionalProperties: false
23
+ }
24
+ ],
25
+ messages: {
26
+ expectedNewlineAfterParen: 'Expected a newline after \'(\'.',
27
+ expectedNewlineBeforeParen: 'Expected a newline before \')\'.'
28
+ }
29
+ },
30
+
31
+ create(context) {
32
+
33
+ const sourceCode = context.sourceCode;
34
+ const options = context.options[0] || {};
35
+ const allowSingleArgumentOnSameLine = options.singleArgument === true;
36
+
37
+ /**
38
+ * @param {import('estree').CallExpression} node
39
+ */
40
+ function check(node) {
41
+
42
+ // Get the opening parenthesis token.
43
+ const openParen = sourceCode.getTokenAfter(node.callee);
44
+
45
+ // Get the closing parenthesis token.
46
+ const closeParen = sourceCode.getLastToken(node);
47
+
48
+ // If we can't find the parentheses, or they aren't parentheses, exit.
49
+ if (!openParen || openParen.value !== '(' || !closeParen || closeParen.value !== ')') {
50
+
51
+ return;
52
+
53
+ }
54
+
55
+ // If the parentheses are on the same line, this rule doesn't apply.
56
+ if (openParen.loc.start.line === closeParen.loc.end.line) {
57
+
58
+ return;
59
+
60
+ }
61
+
62
+ // Handle the singleArgument exception
63
+ if (
64
+ allowSingleArgumentOnSameLine
65
+ && node.arguments.length === 1
66
+ ) {
67
+
68
+ const arg = node.arguments[0];
69
+ const isObjectOrArray = arg.type === 'ObjectExpression' || arg.type === 'ArrayExpression';
70
+
71
+ if (isObjectOrArray) {
72
+
73
+ // For this exception, we *disallow* newlines between parens and the single argument.
74
+ const firstTokenOfArg = sourceCode.getFirstToken(arg);
75
+ if (openParen.loc.end.line !== firstTokenOfArg.loc.start.line) {
76
+
77
+ context.report({
78
+ node: openParen,
79
+ message: 'Unexpected newline after \'(\' for single object/array argument.',
80
+ fix: (fixer) => fixer.removeRange([
81
+ openParen.range[1],
82
+ firstTokenOfArg.range[0]
83
+ ])
84
+ });
85
+
86
+ }
87
+
88
+ const lastTokenOfArg = sourceCode.getLastToken(arg);
89
+ if (lastTokenOfArg.loc.end.line !== closeParen.loc.start.line) {
90
+
91
+ context.report({
92
+ node: closeParen,
93
+ message: 'Unexpected newline before \')\' for single object/array argument.',
94
+ fix: (fixer) => fixer.removeRange([
95
+ lastTokenOfArg.range[1],
96
+ closeParen.range[0]
97
+ ])
98
+ });
99
+
100
+ }
101
+
102
+ return; // End processing for this node
103
+
104
+ }
105
+
106
+ }
107
+
108
+ // Check for a newline after the opening parenthesis.
109
+ const firstArg = node.arguments[0];
110
+ if (firstArg) {
111
+
112
+ const firstTokenOfFirstArg = sourceCode.getFirstToken(firstArg);
113
+ if (openParen.loc.end.line === firstTokenOfFirstArg.loc.start.line) {
114
+
115
+ context.report({
116
+ node: openParen,
117
+ messageId: 'expectedNewlineAfterParen',
118
+ fix: (fixer) => fixer.insertTextAfter(
119
+ openParen,
120
+ '\n'
121
+ )
122
+ });
123
+
124
+ }
125
+
126
+ }
127
+
128
+ // Check for a newline before the closing parenthesis.
129
+ const lastArg = node.arguments[node.arguments.length - 1];
130
+ if (lastArg) {
131
+
132
+ const lastTokenOfLastArg = sourceCode.getLastToken(lastArg);
133
+ if (lastTokenOfLastArg.loc.end.line === closeParen.loc.start.line) {
134
+
135
+ context.report({
136
+ node: closeParen,
137
+ messageId: 'expectedNewlineBeforeParen',
138
+ fix: (fixer) => fixer.insertTextBefore(
139
+ closeParen,
140
+ '\n'
141
+ )
142
+ });
143
+
144
+ }
145
+
146
+ }
147
+
148
+ }
149
+
150
+ return { CallExpression: check };
151
+
152
+ }
153
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ export const newlineBetweenImportsRule = {
5
+ meta: {
6
+ type: 'layout',
7
+ docs: {
8
+ description: 'Enforce newlines between import specifiers',
9
+ category: 'Stylistic Issues',
10
+ recommended: false
11
+ },
12
+ fixable: 'whitespace',
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ minItems: {
18
+ type: 'integer',
19
+ minimum: 2
20
+ }
21
+ },
22
+ additionalProperties: false
23
+ }
24
+ ],
25
+ messages: { error: 'There should be a newline between import specifiers.' }
26
+ },
27
+
28
+ create(context) {
29
+
30
+ const sourceCode = context.sourceCode;
31
+ const { minItems = 2 } = context.options[0] || {};
32
+
33
+ return {
34
+ ImportDeclaration(node) {
35
+
36
+ const specifiers = node
37
+ .specifiers
38
+ .filter((specifier) => specifier.type === 'ImportSpecifier');
39
+
40
+ if (specifiers.length < minItems) {
41
+
42
+ return;
43
+
44
+ }
45
+
46
+ for (let i = 0; i < specifiers.length - 1; i++) {
47
+
48
+ const currentSpecifier = specifiers[i];
49
+ const nextSpecifier = specifiers[i + 1];
50
+
51
+ const lastTokenOfCurrent = sourceCode.getLastToken(currentSpecifier);
52
+ const firstTokenOfNext = sourceCode.getFirstToken(nextSpecifier);
53
+
54
+ if (lastTokenOfCurrent.loc.end.line === firstTokenOfNext.loc.start.line) {
55
+
56
+ context.report({
57
+ node: nextSpecifier,
58
+ messageId: 'error',
59
+ fix(fixer) {
60
+
61
+ return fixer.insertTextBefore(
62
+ nextSpecifier,
63
+ '\n'
64
+ );
65
+
66
+ }
67
+ });
68
+
69
+ }
70
+
71
+ }
72
+
73
+ }
74
+ };
75
+
76
+ }
77
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @type {import('eslint').Rule.RuleModule}
3
+ */
4
+ export const unnamedImportsLastRule = {
5
+ meta: {
6
+ type: 'layout',
7
+ docs: {
8
+ description: 'Enforce that unnamed imports (side-effect imports) are last',
9
+ category: 'Stylistic Issues',
10
+ recommended: false
11
+ },
12
+ fixable: 'code',
13
+ schema: [],
14
+ messages: { error: 'Unnamed (side-effect) imports should be at the bottom of the import block.' }
15
+ },
16
+
17
+ create(context) {
18
+
19
+ return {
20
+ 'Program:exit'(program) {
21
+
22
+ const sourceCode = context.sourceCode;
23
+ const allImports = program
24
+ .body
25
+ .filter((node) => node.type === 'ImportDeclaration');
26
+
27
+ if (allImports.length < 2) {
28
+
29
+ return;
30
+
31
+ }
32
+
33
+ const regularImports = allImports.filter((node) => node.specifiers.length > 0);
34
+ const sideEffectImports = allImports.filter((node) => node.specifiers.length === 0);
35
+
36
+ if (regularImports.length === 0 || sideEffectImports.length === 0) {
37
+
38
+ return;
39
+
40
+ }
41
+
42
+ const lastRegularImport = regularImports[regularImports.length - 1];
43
+
44
+ const misplacedImports = sideEffectImports.filter((node) => node.range[0] < lastRegularImport.range[0]);
45
+
46
+ if (misplacedImports.length === 0) {
47
+
48
+ return;
49
+
50
+ }
51
+
52
+ // Report on the first misplaced import
53
+ const firstMisplaced = misplacedImports[0];
54
+
55
+ context.report({
56
+ node: firstMisplaced,
57
+ messageId: 'error',
58
+ fix(fixer) {
59
+
60
+ const sortedImports = [
61
+ ...regularImports,
62
+ ...sideEffectImports
63
+ ];
64
+
65
+ const importTexts = sortedImports.map((node) => sourceCode.getText(node));
66
+
67
+ const range = [
68
+ allImports[0].range[0],
69
+ allImports[allImports.length - 1].range[1]
70
+ ];
71
+
72
+ return fixer.replaceTextRange(
73
+ range,
74
+ importTexts.join('\n')
75
+ );
76
+
77
+ }
78
+ });
79
+
80
+ }
81
+ };
82
+
83
+ }
84
+ };
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from 'eslint/config';
2
+ import { importX as eslintPluginImportX } from 'eslint-plugin-import-x';
3
+ import {
4
+ base,
5
+ astro,
6
+ importX,
7
+ reactHooks,
8
+ stylistic,
9
+ custom
10
+ } from '../configs/index.js';
11
+
12
+ /**
13
+ * The `ESLint` Astro/React config with typescript.
14
+ */
15
+ export const astroReact = defineConfig(
16
+ base,
17
+ astro,
18
+ {
19
+ ...eslintPluginImportX.configs['flat/react'],
20
+ ...importX
21
+ },
22
+ reactHooks,
23
+ stylistic,
24
+ custom,
25
+ {
26
+ name: 'eslint-plugin-astro-stylistic-override',
27
+ files: [ '**/*.astro' ],
28
+ rules: {
29
+ '@stylistic/jsx-one-expression-per-line': 'off',
30
+ '@stylistic/jsx-curly-brace-presence': 'off'
31
+ }
32
+ }
33
+ );
@@ -0,0 +1,39 @@
1
+ import {
2
+ defineConfig,
3
+ globalIgnores
4
+ } from 'eslint/config';
5
+ import { importX as eslintPluginImportX } from 'eslint-plugin-import-x';
6
+ import {
7
+ base,
8
+ custom,
9
+ astro,
10
+ typescript,
11
+ reactHooks,
12
+ stylistic,
13
+ importXTs
14
+ } from '../configs/index.js';
15
+
16
+ /**
17
+ * The `ESLint` Astro/React config with typescript.
18
+ */
19
+ export const astroReactTs = defineConfig(
20
+ base,
21
+ globalIgnores([ '**/*.astro/*.ts' ]),
22
+ typescript,
23
+ astro,
24
+ {
25
+ ...eslintPluginImportX.configs['flat/react'],
26
+ ...importXTs
27
+ },
28
+ reactHooks,
29
+ stylistic,
30
+ custom,
31
+ {
32
+ name: 'eslint-plugin-astro-stylistic-override',
33
+ files: [ '**/*.astro' ],
34
+ rules: {
35
+ '@stylistic/jsx-one-expression-per-line': 'off',
36
+ '@stylistic/jsx-curly-brace-presence': 'off'
37
+ }
38
+ }
39
+ );
@@ -0,0 +1,14 @@
1
+ import type { Linter } from 'eslint';
2
+
3
+ /**
4
+ * ESLint preset for Astro/React with JavaScript.
5
+ *
6
+ * This package uses ESLint's flat config format, so the preset is an array
7
+ * of flat-config items.
8
+ */
9
+ export declare const astroReact: readonly Linter.Config[];
10
+
11
+ /**
12
+ * ESLint preset for Astro/React with TypeScript.
13
+ */
14
+ export declare const astroReactTs: readonly Linter.Config[];
@@ -0,0 +1,2 @@
1
+ export { astroReact } from './astroReact';
2
+ export { astroReactTs } from './astroReactTs';
@@ -0,0 +1,17 @@
1
+ import { defineConfig, globalIgnores } from 'eslint/config';
2
+ import {
3
+ base,
4
+ importX,
5
+ json,
6
+ stylistic,
7
+ custom
8
+ } from './eslint/configs/index.js';
9
+
10
+ export default defineConfig(
11
+ globalIgnores(['**/*.d.ts']),
12
+ base,
13
+ importX,
14
+ stylistic,
15
+ json,
16
+ custom
17
+ );