eslint-plugin-absolute 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.absolutejs/eslint.cache.json +49 -0
- package/.absolutejs/prettier.cache.json +49 -0
- package/.absolutejs/tsconfig.tsbuildinfo +1 -0
- package/.claude/settings.local.json +10 -0
- package/dist/index.js +1787 -1457
- package/eslint.config.mjs +107 -0
- package/package.json +15 -12
- package/src/index.ts +45 -0
- package/src/rules/explicit-object-types.ts +75 -0
- package/src/rules/inline-style-limit.ts +88 -0
- package/src/rules/localize-react-props.ts +454 -0
- package/src/rules/max-depth-extended.ts +153 -0
- package/src/rules/{max-jsx-nesting.js → max-jsx-nesting.ts} +37 -38
- package/src/rules/min-var-length.ts +360 -0
- package/src/rules/no-button-navigation.ts +270 -0
- package/src/rules/no-explicit-return-types.ts +83 -0
- package/src/rules/no-inline-prop-types.ts +68 -0
- package/src/rules/no-multi-style-objects.ts +80 -0
- package/src/rules/no-nested-jsx-return.ts +205 -0
- package/src/rules/no-or-none-component.ts +63 -0
- package/src/rules/no-transition-cssproperties.ts +131 -0
- package/src/rules/no-unnecessary-div.ts +65 -0
- package/src/rules/no-unnecessary-key.ts +111 -0
- package/src/rules/no-useless-function.ts +56 -0
- package/src/rules/seperate-style-files.ts +79 -0
- package/src/rules/sort-exports.ts +424 -0
- package/src/rules/sort-keys-fixable.ts +647 -0
- package/src/rules/spring-naming-convention.ts +160 -0
- package/tsconfig.json +4 -1
- package/src/index.js +0 -45
- package/src/rules/explicit-object-types.js +0 -54
- package/src/rules/inline-style-limit.js +0 -77
- package/src/rules/localize-react-props.js +0 -418
- package/src/rules/max-depth-extended.js +0 -124
- package/src/rules/min-var-length.js +0 -300
- package/src/rules/no-button-navigation.js +0 -232
- package/src/rules/no-explicit-return-types.js +0 -64
- package/src/rules/no-inline-prop-types.js +0 -55
- package/src/rules/no-multi-style-objects.js +0 -70
- package/src/rules/no-nested-jsx-return.js +0 -154
- package/src/rules/no-or-none-component.js +0 -50
- package/src/rules/no-transition-cssproperties.js +0 -102
- package/src/rules/no-unnecessary-div.js +0 -40
- package/src/rules/no-unnecessary-key.js +0 -128
- package/src/rules/no-useless-function.js +0 -43
- package/src/rules/seperate-style-files.js +0 -62
- package/src/rules/sort-exports.js +0 -397
- package/src/rules/sort-keys-fixable.js +0 -459
- package/src/rules/spring-naming-convention.js +0 -111
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce that the key prop is only used on components
|
|
3
|
+
* rendered as part of an array mapping. This rule disallows having a key prop
|
|
4
|
+
* on a JSX element when it is not part of a mapping, except when the element is
|
|
5
|
+
* returned from a helper render function.
|
|
6
|
+
*
|
|
7
|
+
* Note: This rule does not auto-fix.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
11
|
+
|
|
12
|
+
type Options = [];
|
|
13
|
+
type MessageIds = "unnecessaryKey";
|
|
14
|
+
|
|
15
|
+
const isMapCallExpression = (node: TSESTree.Node) => {
|
|
16
|
+
if (
|
|
17
|
+
node.type !== "CallExpression" ||
|
|
18
|
+
node.callee.type !== "MemberExpression"
|
|
19
|
+
) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { property } = node.callee;
|
|
24
|
+
return (
|
|
25
|
+
(property.type === "Identifier" && property.name === "map") ||
|
|
26
|
+
(property.type === "Literal" && property.value === "map")
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const noUnnecessaryKey: TSESLint.RuleModule<MessageIds, Options> = {
|
|
31
|
+
create(context) {
|
|
32
|
+
// Polyfill for context.getAncestors if it's not available.
|
|
33
|
+
const getAncestors = (node: TSESTree.Node) => {
|
|
34
|
+
const ancestors: TSESTree.Node[] = [];
|
|
35
|
+
let current: TSESTree.Node | null | undefined = node.parent;
|
|
36
|
+
while (current) {
|
|
37
|
+
ancestors.push(current);
|
|
38
|
+
current = current.parent;
|
|
39
|
+
}
|
|
40
|
+
return ancestors;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if any of the ancestors is a CallExpression
|
|
45
|
+
* representing an array mapping.
|
|
46
|
+
*/
|
|
47
|
+
const isInsideMapCall = (ancestors: TSESTree.Node[]) =>
|
|
48
|
+
ancestors.some(isMapCallExpression);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks whether the JSX element is being returned from a helper render
|
|
52
|
+
* function.
|
|
53
|
+
*/
|
|
54
|
+
const isReturnedFromFunction = (ancestors: TSESTree.Node[]) =>
|
|
55
|
+
ancestors.some((ancestor) => ancestor.type === "ReturnStatement");
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reports a JSX element if it has a key prop and is not rendered as part
|
|
59
|
+
* of an inline mapping (and not simply returned from a render helper function).
|
|
60
|
+
*/
|
|
61
|
+
const checkJSXOpeningElement = (node: TSESTree.JSXOpeningElement) => {
|
|
62
|
+
// Find a key attribute.
|
|
63
|
+
const keyAttribute = node.attributes.find(
|
|
64
|
+
(attr) =>
|
|
65
|
+
attr.type === "JSXAttribute" &&
|
|
66
|
+
attr.name.type === "JSXIdentifier" &&
|
|
67
|
+
attr.name.name === "key"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!keyAttribute) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Retrieve ancestors.
|
|
75
|
+
const ancestors = getAncestors(node);
|
|
76
|
+
|
|
77
|
+
// If the element is (directly or indirectly) part of a map call, allow it.
|
|
78
|
+
if (isInsideMapCall(ancestors)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If the element is simply returned from a helper function, allow it.
|
|
83
|
+
if (isReturnedFromFunction(ancestors)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Otherwise, report the key prop as unnecessary.
|
|
88
|
+
context.report({
|
|
89
|
+
messageId: "unnecessaryKey",
|
|
90
|
+
node: keyAttribute
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
JSXOpeningElement: checkJSXOpeningElement
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
defaultOptions: [],
|
|
99
|
+
meta: {
|
|
100
|
+
docs: {
|
|
101
|
+
description:
|
|
102
|
+
"enforce that the key prop is only used on components rendered as part of a mapping"
|
|
103
|
+
},
|
|
104
|
+
messages: {
|
|
105
|
+
unnecessaryKey:
|
|
106
|
+
"The key prop should only be used on elements that are directly rendered as part of an array mapping."
|
|
107
|
+
},
|
|
108
|
+
schema: [],
|
|
109
|
+
type: "problem"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
type Options = [];
|
|
4
|
+
type MessageIds = "uselessFunction";
|
|
5
|
+
|
|
6
|
+
export const noUselessFunction: TSESLint.RuleModule<MessageIds, Options> = {
|
|
7
|
+
create(context) {
|
|
8
|
+
const isCallbackFunction = (node: TSESTree.ArrowFunctionExpression) => {
|
|
9
|
+
const { parent } = node;
|
|
10
|
+
if (!parent || parent.type !== "CallExpression") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const arg of parent.arguments) {
|
|
15
|
+
if (arg === node) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression) {
|
|
25
|
+
// Check for functions with no parameters and a body that's an ObjectExpression
|
|
26
|
+
if (
|
|
27
|
+
node.params.length === 0 &&
|
|
28
|
+
node.body &&
|
|
29
|
+
node.body.type === "ObjectExpression"
|
|
30
|
+
) {
|
|
31
|
+
// If the function is used as a callback (like in react-spring), skip reporting.
|
|
32
|
+
if (isCallbackFunction(node)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
context.report({
|
|
36
|
+
messageId: "uselessFunction",
|
|
37
|
+
node
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
defaultOptions: [],
|
|
44
|
+
meta: {
|
|
45
|
+
docs: {
|
|
46
|
+
description:
|
|
47
|
+
"Disallow functions that have no parameters and just return an object literal; consider exporting the object directly, unless the function is used as a callback (e.g., in react-spring)."
|
|
48
|
+
},
|
|
49
|
+
messages: {
|
|
50
|
+
uselessFunction:
|
|
51
|
+
"This function has no parameters and simply returns an object. Consider exporting the object directly instead of wrapping it in a function."
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
type: "suggestion"
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
type Options = [];
|
|
4
|
+
type MessageIds = "moveToFile";
|
|
5
|
+
|
|
6
|
+
export const seperateStyleFiles: TSESLint.RuleModule<MessageIds, Options> = {
|
|
7
|
+
create(context) {
|
|
8
|
+
// Only run this rule on .tsx or .jsx files.
|
|
9
|
+
const { filename } = context;
|
|
10
|
+
if (!filename.endsWith(".tsx") && !filename.endsWith(".jsx")) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
VariableDeclarator(node: TSESTree.VariableDeclarator) {
|
|
16
|
+
// Ensure this is a variable declaration with an Identifier.
|
|
17
|
+
if (!node.id || node.id.type !== "Identifier") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const identifier = node.id;
|
|
22
|
+
|
|
23
|
+
// Check if there's a type annotation on the variable.
|
|
24
|
+
const idTypeAnnotation = identifier.typeAnnotation;
|
|
25
|
+
if (
|
|
26
|
+
!idTypeAnnotation ||
|
|
27
|
+
idTypeAnnotation.type !== "TSTypeAnnotation"
|
|
28
|
+
) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const typeNode = idTypeAnnotation.typeAnnotation;
|
|
33
|
+
if (!typeNode || typeNode.type !== "TSTypeReference") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle both Identifier and TSQualifiedName cases.
|
|
38
|
+
const typeNameNode = typeNode.typeName;
|
|
39
|
+
let typeName: string | null = null;
|
|
40
|
+
|
|
41
|
+
// When typeName is a simple Identifier.
|
|
42
|
+
if (typeNameNode.type === "Identifier") {
|
|
43
|
+
typeName = typeNameNode.name;
|
|
44
|
+
}
|
|
45
|
+
// When typeName is a TSQualifiedName, e.g., React.CSSProperties.
|
|
46
|
+
else if (typeNameNode.type === "TSQualifiedName") {
|
|
47
|
+
const { right } = typeNameNode;
|
|
48
|
+
typeName = right.name;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Report if the type name is CSSProperties.
|
|
52
|
+
if (typeName === "CSSProperties") {
|
|
53
|
+
context.report({
|
|
54
|
+
data: {
|
|
55
|
+
name: identifier.name,
|
|
56
|
+
typeName
|
|
57
|
+
},
|
|
58
|
+
messageId: "moveToFile",
|
|
59
|
+
node
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
defaultOptions: [],
|
|
66
|
+
meta: {
|
|
67
|
+
docs: {
|
|
68
|
+
description:
|
|
69
|
+
"Warn when a component file (.jsx or .tsx) contains a style object typed as CSSProperties. " +
|
|
70
|
+
"Style objects should be moved to their own file under the style folder."
|
|
71
|
+
},
|
|
72
|
+
messages: {
|
|
73
|
+
moveToFile:
|
|
74
|
+
'Style object "{{name}}" is typed as {{typeName}}. Move it to its own file under the style folder.'
|
|
75
|
+
},
|
|
76
|
+
schema: [],
|
|
77
|
+
type: "suggestion"
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { TSESLint, TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview Enforce that top-level export declarations are sorted.
|
|
5
|
+
*
|
|
6
|
+
* This rule supports the following options:
|
|
7
|
+
* - order: "asc" or "desc" (default: "asc")
|
|
8
|
+
* - caseSensitive: boolean (default: false)
|
|
9
|
+
* - natural: boolean (default: false)
|
|
10
|
+
* - minKeys: integer, minimum number of exports in a contiguous block to check (default: 2)
|
|
11
|
+
* - variablesBeforeFunctions: boolean (default: false)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type SortExportsOptions = {
|
|
15
|
+
order?: "asc" | "desc";
|
|
16
|
+
caseSensitive?: boolean;
|
|
17
|
+
natural?: boolean;
|
|
18
|
+
minKeys?: number;
|
|
19
|
+
variablesBeforeFunctions?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type Options = [SortExportsOptions?];
|
|
23
|
+
|
|
24
|
+
type MessageIds = "alphabetical" | "variablesBeforeFunctions";
|
|
25
|
+
|
|
26
|
+
type ExportItem = {
|
|
27
|
+
name: string;
|
|
28
|
+
node: TSESTree.ExportNamedDeclaration;
|
|
29
|
+
isFunction: boolean;
|
|
30
|
+
text: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SORT_BEFORE: -1 = -1;
|
|
34
|
+
|
|
35
|
+
const getVariableDeclaratorName = (
|
|
36
|
+
declaration: TSESTree.VariableDeclaration
|
|
37
|
+
) => {
|
|
38
|
+
if (declaration.declarations.length !== 1) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const [firstDeclarator] = declaration.declarations;
|
|
42
|
+
if (firstDeclarator && firstDeclarator.id.type === "Identifier") {
|
|
43
|
+
return firstDeclarator.id.name;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const getDeclarationName = (
|
|
49
|
+
declaration: TSESTree.ExportNamedDeclaration["declaration"]
|
|
50
|
+
) => {
|
|
51
|
+
if (!declaration) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (declaration.type === "VariableDeclaration") {
|
|
56
|
+
return getVariableDeclaratorName(declaration);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
(declaration.type === "FunctionDeclaration" ||
|
|
61
|
+
declaration.type === "ClassDeclaration") &&
|
|
62
|
+
declaration.id &&
|
|
63
|
+
declaration.id.type === "Identifier"
|
|
64
|
+
) {
|
|
65
|
+
return declaration.id.name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getSpecifierName = (node: TSESTree.ExportNamedDeclaration) => {
|
|
72
|
+
if (node.specifiers.length !== 1) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const [spec] = node.specifiers;
|
|
76
|
+
if (!spec) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (spec.exported.type === "Identifier") {
|
|
80
|
+
return spec.exported.name;
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
spec.exported.type === "Literal" &&
|
|
84
|
+
typeof spec.exported.value === "string"
|
|
85
|
+
) {
|
|
86
|
+
return spec.exported.value;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getExportName = (node: TSESTree.ExportNamedDeclaration) =>
|
|
92
|
+
getDeclarationName(node.declaration) ?? getSpecifierName(node);
|
|
93
|
+
|
|
94
|
+
const isFixableExport = (exportNode: TSESTree.ExportNamedDeclaration) => {
|
|
95
|
+
const { declaration } = exportNode;
|
|
96
|
+
|
|
97
|
+
if (!declaration) {
|
|
98
|
+
return exportNode.specifiers.length === 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
declaration.type === "VariableDeclaration" &&
|
|
103
|
+
declaration.declarations.length === 1
|
|
104
|
+
) {
|
|
105
|
+
const [firstDecl] = declaration.declarations;
|
|
106
|
+
return firstDecl !== undefined && firstDecl.id.type === "Identifier";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
(declaration.type === "FunctionDeclaration" ||
|
|
111
|
+
declaration.type === "ClassDeclaration") &&
|
|
112
|
+
declaration.id !== null &&
|
|
113
|
+
declaration.id.type === "Identifier"
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const sortExports: TSESLint.RuleModule<MessageIds, Options> = {
|
|
118
|
+
create(context) {
|
|
119
|
+
const { sourceCode } = context;
|
|
120
|
+
const [option] = context.options;
|
|
121
|
+
|
|
122
|
+
const order: "asc" | "desc" =
|
|
123
|
+
option && option.order ? option.order : "asc";
|
|
124
|
+
|
|
125
|
+
const caseSensitive =
|
|
126
|
+
option && typeof option.caseSensitive === "boolean"
|
|
127
|
+
? option.caseSensitive
|
|
128
|
+
: false;
|
|
129
|
+
|
|
130
|
+
const natural =
|
|
131
|
+
option && typeof option.natural === "boolean"
|
|
132
|
+
? option.natural
|
|
133
|
+
: false;
|
|
134
|
+
|
|
135
|
+
const minKeys =
|
|
136
|
+
option && typeof option.minKeys === "number" ? option.minKeys : 2;
|
|
137
|
+
|
|
138
|
+
const variablesBeforeFunctions =
|
|
139
|
+
option && typeof option.variablesBeforeFunctions === "boolean"
|
|
140
|
+
? option.variablesBeforeFunctions
|
|
141
|
+
: false;
|
|
142
|
+
|
|
143
|
+
const generateExportText = (node: TSESTree.ExportNamedDeclaration) =>
|
|
144
|
+
sourceCode
|
|
145
|
+
.getText(node)
|
|
146
|
+
.trim()
|
|
147
|
+
.replace(/\s*;?\s*$/, ";");
|
|
148
|
+
|
|
149
|
+
const compareStrings = (strLeft: string, strRight: string) => {
|
|
150
|
+
let left = strLeft;
|
|
151
|
+
let right = strRight;
|
|
152
|
+
|
|
153
|
+
if (!caseSensitive) {
|
|
154
|
+
left = left.toLowerCase();
|
|
155
|
+
right = right.toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cmp = natural
|
|
159
|
+
? left.localeCompare(right, undefined, { numeric: true })
|
|
160
|
+
: left.localeCompare(right);
|
|
161
|
+
|
|
162
|
+
return order === "asc" ? cmp : -cmp;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const isFunctionExport = (node: TSESTree.ExportNamedDeclaration) => {
|
|
166
|
+
const { declaration } = node;
|
|
167
|
+
|
|
168
|
+
if (!declaration) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (declaration.type === "FunctionDeclaration") {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (declaration.type !== "VariableDeclaration") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (declaration.declarations.length !== 1) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const [firstDeclarator] = declaration.declarations;
|
|
185
|
+
if (!firstDeclarator) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const { init } = firstDeclarator;
|
|
189
|
+
if (!init) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return (
|
|
193
|
+
init.type === "FunctionExpression" ||
|
|
194
|
+
init.type === "ArrowFunctionExpression"
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const sortComparator = (left: ExportItem, right: ExportItem) => {
|
|
199
|
+
const kindA = left.node.exportKind ?? "value";
|
|
200
|
+
const kindB = right.node.exportKind ?? "value";
|
|
201
|
+
|
|
202
|
+
if (kindA !== kindB) {
|
|
203
|
+
return kindA === "type" ? SORT_BEFORE : 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (
|
|
207
|
+
variablesBeforeFunctions &&
|
|
208
|
+
left.isFunction !== right.isFunction
|
|
209
|
+
) {
|
|
210
|
+
return left.isFunction ? 1 : SORT_BEFORE;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return compareStrings(left.name, right.name);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Very lightweight dependency check: look at the text of the node and see
|
|
218
|
+
* if it references any of the later export names.
|
|
219
|
+
*/
|
|
220
|
+
const hasForwardDependency = (
|
|
221
|
+
node: TSESTree.Node,
|
|
222
|
+
laterNames: Set<string>
|
|
223
|
+
) => {
|
|
224
|
+
const text = sourceCode.getText(node);
|
|
225
|
+
for (const name of laterNames) {
|
|
226
|
+
if (text.includes(name)) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const buildItems = (block: TSESTree.ExportNamedDeclaration[]) =>
|
|
234
|
+
block
|
|
235
|
+
.map((node) => {
|
|
236
|
+
const name = getExportName(node);
|
|
237
|
+
if (!name) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const item: ExportItem = {
|
|
241
|
+
isFunction: isFunctionExport(node),
|
|
242
|
+
name,
|
|
243
|
+
node,
|
|
244
|
+
text: sourceCode.getText(node)
|
|
245
|
+
};
|
|
246
|
+
return item;
|
|
247
|
+
})
|
|
248
|
+
.filter((item): item is ExportItem => item !== null);
|
|
249
|
+
|
|
250
|
+
const findFirstUnsorted = (items: ExportItem[]) => {
|
|
251
|
+
let messageId: MessageIds = "alphabetical";
|
|
252
|
+
|
|
253
|
+
const unsorted = items.some((current, idx) => {
|
|
254
|
+
if (idx === 0) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const prev = items[idx - 1];
|
|
258
|
+
if (!prev) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
if (sortComparator(prev, current) <= 0) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
variablesBeforeFunctions &&
|
|
266
|
+
prev.isFunction &&
|
|
267
|
+
!current.isFunction
|
|
268
|
+
) {
|
|
269
|
+
messageId = "variablesBeforeFunctions";
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return unsorted ? messageId : null;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const checkForwardDependencies = (items: ExportItem[]) => {
|
|
278
|
+
const exportNames = items.map((item) => item.name);
|
|
279
|
+
return items.some((item, idx) => {
|
|
280
|
+
const laterNames = new Set(exportNames.slice(idx + 1));
|
|
281
|
+
const nodeToCheck: TSESTree.Node =
|
|
282
|
+
item.node.declaration ?? item.node;
|
|
283
|
+
return hasForwardDependency(nodeToCheck, laterNames);
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const processExportBlock = (
|
|
288
|
+
block: TSESTree.ExportNamedDeclaration[]
|
|
289
|
+
) => {
|
|
290
|
+
if (block.length < minKeys) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const items = buildItems(block);
|
|
295
|
+
|
|
296
|
+
if (items.length < minKeys) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const messageId = findFirstUnsorted(items);
|
|
301
|
+
if (!messageId) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (checkForwardDependencies(items)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const sortedItems = items.slice().sort(sortComparator);
|
|
310
|
+
|
|
311
|
+
const expectedOrder = sortedItems
|
|
312
|
+
.map((item) => item.name)
|
|
313
|
+
.join(", ");
|
|
314
|
+
|
|
315
|
+
const [firstNode] = block;
|
|
316
|
+
const lastNode = block[block.length - 1];
|
|
317
|
+
|
|
318
|
+
if (!firstNode || !lastNode) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
context.report({
|
|
323
|
+
data: {
|
|
324
|
+
expectedOrder
|
|
325
|
+
},
|
|
326
|
+
fix(fixer) {
|
|
327
|
+
const fixableNodes = block.filter(isFixableExport);
|
|
328
|
+
|
|
329
|
+
if (fixableNodes.length < minKeys) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const sortedText = sortedItems
|
|
334
|
+
.map((item) => generateExportText(item.node))
|
|
335
|
+
.join("\n");
|
|
336
|
+
|
|
337
|
+
const [rangeStart] = firstNode.range;
|
|
338
|
+
const [, rangeEnd] = lastNode.range;
|
|
339
|
+
|
|
340
|
+
const fullText = sourceCode.getText();
|
|
341
|
+
const originalText = fullText.slice(rangeStart, rangeEnd);
|
|
342
|
+
|
|
343
|
+
if (originalText === sortedText) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return fixer.replaceTextRange(
|
|
348
|
+
[rangeStart, rangeEnd],
|
|
349
|
+
sortedText
|
|
350
|
+
);
|
|
351
|
+
},
|
|
352
|
+
messageId,
|
|
353
|
+
node: firstNode
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
"Program:exit"(node: TSESTree.Program) {
|
|
359
|
+
const { body } = node;
|
|
360
|
+
const block: TSESTree.ExportNamedDeclaration[] = [];
|
|
361
|
+
|
|
362
|
+
body.forEach((stmt) => {
|
|
363
|
+
if (
|
|
364
|
+
stmt.type === "ExportNamedDeclaration" &&
|
|
365
|
+
!stmt.source &&
|
|
366
|
+
getExportName(stmt) !== null
|
|
367
|
+
) {
|
|
368
|
+
block.push(stmt);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (block.length > 0) {
|
|
373
|
+
processExportBlock(block);
|
|
374
|
+
block.length = 0;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (block.length > 0) {
|
|
379
|
+
processExportBlock(block);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
defaultOptions: [{}],
|
|
385
|
+
meta: {
|
|
386
|
+
docs: {
|
|
387
|
+
description:
|
|
388
|
+
"Enforce that top-level export declarations are sorted by exported name and, optionally, that variable exports come before function exports"
|
|
389
|
+
},
|
|
390
|
+
fixable: "code",
|
|
391
|
+
messages: {
|
|
392
|
+
alphabetical:
|
|
393
|
+
"Export declarations are not sorted alphabetically. Expected order: {{expectedOrder}}.",
|
|
394
|
+
variablesBeforeFunctions:
|
|
395
|
+
"Non-function exports should come before function exports."
|
|
396
|
+
},
|
|
397
|
+
schema: [
|
|
398
|
+
{
|
|
399
|
+
additionalProperties: false,
|
|
400
|
+
properties: {
|
|
401
|
+
caseSensitive: {
|
|
402
|
+
type: "boolean"
|
|
403
|
+
},
|
|
404
|
+
minKeys: {
|
|
405
|
+
minimum: 2,
|
|
406
|
+
type: "integer"
|
|
407
|
+
},
|
|
408
|
+
natural: {
|
|
409
|
+
type: "boolean"
|
|
410
|
+
},
|
|
411
|
+
order: {
|
|
412
|
+
enum: ["asc", "desc"],
|
|
413
|
+
type: "string"
|
|
414
|
+
},
|
|
415
|
+
variablesBeforeFunctions: {
|
|
416
|
+
type: "boolean"
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
type: "object"
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
type: "suggestion"
|
|
423
|
+
}
|
|
424
|
+
};
|