@simplysm/lint 13.0.69 → 13.0.71
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/README.md +12 -490
- package/dist/eslint-recommended.js +41 -41
- package/dist/rules/no-hard-private.d.ts +6 -6
- package/dist/rules/no-hard-private.js +6 -6
- package/dist/rules/no-subpath-imports-from-simplysm.d.ts +5 -5
- package/dist/rules/no-subpath-imports-from-simplysm.js +6 -6
- package/dist/rules/ts-no-throw-not-implemented-error.d.ts +5 -5
- package/dist/rules/ts-no-throw-not-implemented-error.js +2 -2
- package/dist/stylelint-recommended.js +3 -3
- package/dist/utils/create-rule.d.ts +3 -3
- package/package.json +5 -4
- package/src/eslint-recommended.ts +54 -54
- package/src/rules/no-hard-private.ts +23 -23
- package/src/rules/no-subpath-imports-from-simplysm.ts +13 -13
- package/src/rules/ts-no-throw-not-implemented-error.ts +13 -13
- package/src/stylelint-recommended.ts +3 -3
- package/src/utils/create-rule.ts +3 -3
- package/tests/no-hard-private.spec.ts +961 -0
- package/tests/no-subpath-imports-from-simplysm.spec.ts +352 -0
- package/tests/recommended.spec.ts +155 -0
- package/tests/ts-no-throw-not-implemented-error.spec.ts +386 -0
- package/tests/vitest.setup.ts +10 -0
|
@@ -18,27 +18,27 @@ function isClassMemberWithAccessibility(
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* ECMAScript private
|
|
21
|
+
* ESLint rule that restricts ECMAScript private fields (`#field`) and enforces TypeScript `private` keyword usage.
|
|
22
22
|
*
|
|
23
23
|
* @remarks
|
|
24
|
-
*
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
28
|
-
* -
|
|
24
|
+
* This rule checks:
|
|
25
|
+
* - Class field declarations: `#field`
|
|
26
|
+
* - Class method declarations: `#method()`
|
|
27
|
+
* - Class accessor declarations: `accessor #field`
|
|
28
|
+
* - Member access expressions: `this.#field`
|
|
29
29
|
*/
|
|
30
30
|
export default createRule({
|
|
31
31
|
name: "no-hard-private",
|
|
32
32
|
meta: {
|
|
33
33
|
type: "problem",
|
|
34
34
|
docs: {
|
|
35
|
-
description: '
|
|
35
|
+
description: 'Enforces TypeScript "private _" style instead of hard private fields (#).',
|
|
36
36
|
},
|
|
37
37
|
messages: {
|
|
38
38
|
preferSoftPrivate:
|
|
39
|
-
'
|
|
39
|
+
'Hard private fields (#) are not allowed. Use the "private _" style instead.',
|
|
40
40
|
nameConflict:
|
|
41
|
-
'
|
|
41
|
+
'Cannot convert hard private field "#{{name}}" to "_{{name}}". A member with the same name already exists.',
|
|
42
42
|
},
|
|
43
43
|
fixable: "code",
|
|
44
44
|
schema: [],
|
|
@@ -46,11 +46,11 @@ export default createRule({
|
|
|
46
46
|
defaultOptions: [],
|
|
47
47
|
create(context) {
|
|
48
48
|
const sourceCode = context.sourceCode;
|
|
49
|
-
//
|
|
49
|
+
// Stack structure for supporting nested classes
|
|
50
50
|
const classStack: Set<string>[] = [];
|
|
51
51
|
|
|
52
52
|
return {
|
|
53
|
-
// 0.
|
|
53
|
+
// 0. Collect member names when entering a class
|
|
54
54
|
"ClassBody"(node: TSESTree.ClassBody) {
|
|
55
55
|
const memberNames = new Set<string>();
|
|
56
56
|
for (const member of node.body) {
|
|
@@ -67,7 +67,7 @@ export default createRule({
|
|
|
67
67
|
classStack.pop();
|
|
68
68
|
},
|
|
69
69
|
|
|
70
|
-
// 1.
|
|
70
|
+
// 1. Detect declarations (PropertyDefinition, MethodDefinition, AccessorProperty)
|
|
71
71
|
"PropertyDefinition > PrivateIdentifier, MethodDefinition > PrivateIdentifier, AccessorProperty > PrivateIdentifier"(
|
|
72
72
|
node: TSESTree.PrivateIdentifier,
|
|
73
73
|
) {
|
|
@@ -76,11 +76,11 @@ export default createRule({
|
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
const identifierName = node.name; // '#'
|
|
79
|
+
const identifierName = node.name; // Name without the '#' character
|
|
80
80
|
const targetName = `_${identifierName}`;
|
|
81
81
|
const currentClassMembers = classStack.at(-1);
|
|
82
82
|
|
|
83
|
-
//
|
|
83
|
+
// Check for name conflicts
|
|
84
84
|
if (currentClassMembers?.has(targetName)) {
|
|
85
85
|
context.report({
|
|
86
86
|
node,
|
|
@@ -96,25 +96,25 @@ export default createRule({
|
|
|
96
96
|
fix(fixer) {
|
|
97
97
|
const fixes: RuleFix[] = [];
|
|
98
98
|
|
|
99
|
-
// 1-1.
|
|
99
|
+
// 1-1. Rename (#a -> _a)
|
|
100
100
|
fixes.push(fixer.replaceText(node, targetName));
|
|
101
101
|
|
|
102
|
-
// 1-2. 'private'
|
|
102
|
+
// 1-2. Calculate the position to add the 'private' access modifier
|
|
103
103
|
if (parent.accessibility == null) {
|
|
104
|
-
//
|
|
104
|
+
// Default insertion position: beginning of parent node (including static, async, etc)
|
|
105
105
|
let tokenToInsertBefore = sourceCode.getFirstToken(parent);
|
|
106
106
|
|
|
107
|
-
//
|
|
107
|
+
// If decorators exist, insert before the token after the last decorator
|
|
108
108
|
// (@Deco private static _foo)
|
|
109
109
|
if (parent.decorators.length > 0) {
|
|
110
110
|
const lastDecorator = parent.decorators.at(-1)!;
|
|
111
111
|
tokenToInsertBefore = sourceCode.getTokenAfter(lastDecorator);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// tokenToInsertBefore
|
|
115
|
-
//
|
|
116
|
-
// tokenToInsertBefore
|
|
117
|
-
//
|
|
114
|
+
// tokenToInsertBefore is now 'static', 'async', 'readonly', or a variable name ('_foo').
|
|
115
|
+
// Inserting 'private ' before it naturally results in the correct order 'private static ...'.
|
|
116
|
+
// If tokenToInsertBefore is null, it indicates an exceptional situation such as an AST parsing error.
|
|
117
|
+
// In such cases, skip the entire fix to prevent an incomplete fix that only renames.
|
|
118
118
|
if (tokenToInsertBefore == null) {
|
|
119
119
|
return [];
|
|
120
120
|
}
|
|
@@ -126,7 +126,7 @@ export default createRule({
|
|
|
126
126
|
});
|
|
127
127
|
},
|
|
128
128
|
|
|
129
|
-
// 2.
|
|
129
|
+
// 2. Detect usage (this.#field)
|
|
130
130
|
"MemberExpression > PrivateIdentifier"(node: TSESTree.PrivateIdentifier) {
|
|
131
131
|
const identifierName = node.name;
|
|
132
132
|
context.report({
|
|
@@ -2,13 +2,13 @@ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
|
|
|
2
2
|
import { createRule } from "../utils/create-rule";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* ESLint rule that prohibits 'src' subpath imports from `@simplysm/*` packages.
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
8
|
+
* This rule checks:
|
|
9
|
+
* - Static import statements: `import ... from '...'`
|
|
10
|
+
* - Dynamic imports: `import('...')`
|
|
11
|
+
* - Re-export statements: `export { ... } from '...'`, `export * from '...'`
|
|
12
12
|
*/
|
|
13
13
|
export default createRule({
|
|
14
14
|
name: "no-subpath-imports-from-simplysm",
|
|
@@ -16,13 +16,13 @@ export default createRule({
|
|
|
16
16
|
type: "problem",
|
|
17
17
|
docs: {
|
|
18
18
|
description:
|
|
19
|
-
"
|
|
19
|
+
"Prohibits 'src' subpath imports from @simplysm packages. (e.g., @simplysm/pkg/src/x → prohibited)",
|
|
20
20
|
},
|
|
21
21
|
fixable: "code",
|
|
22
22
|
schema: [],
|
|
23
23
|
messages: {
|
|
24
24
|
noSubpathImport:
|
|
25
|
-
"'@simplysm/{{pkg}}'
|
|
25
|
+
"Cannot import 'src' subpath from '@simplysm/{{pkg}}' package: '{{importPath}}'",
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
28
|
defaultOptions: [],
|
|
@@ -32,8 +32,8 @@ export default createRule({
|
|
|
32
32
|
|
|
33
33
|
const parts = importPath.split("/");
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
35
|
+
// Allowed: @simplysm/pkg, @simplysm/pkg/xxx, @simplysm/pkg/xxx/yyy
|
|
36
|
+
// Prohibited: @simplysm/pkg/src, @simplysm/pkg/src/xxx
|
|
37
37
|
if (parts.length >= 3 && parts[2] === "src") {
|
|
38
38
|
const fixedPath = `@simplysm/${parts[1]}`;
|
|
39
39
|
context.report({
|
|
@@ -52,12 +52,12 @@ export default createRule({
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return {
|
|
55
|
-
//
|
|
55
|
+
// Static import: import { x } from '...'
|
|
56
56
|
ImportDeclaration(node) {
|
|
57
57
|
checkAndReport(node.source, node.source.value);
|
|
58
58
|
},
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// Dynamic import: import('...')
|
|
61
61
|
ImportExpression(node) {
|
|
62
62
|
if (node.source.type !== AST_NODE_TYPES.Literal) return;
|
|
63
63
|
const importPath = node.source.value;
|
|
@@ -65,13 +65,13 @@ export default createRule({
|
|
|
65
65
|
checkAndReport(node.source, importPath);
|
|
66
66
|
},
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// Re-export: export { x } from '...'
|
|
69
69
|
ExportNamedDeclaration(node) {
|
|
70
70
|
if (!node.source) return;
|
|
71
71
|
checkAndReport(node.source, node.source.value);
|
|
72
72
|
},
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// Re-export all: export * from '...'
|
|
75
75
|
ExportAllDeclaration(node) {
|
|
76
76
|
checkAndReport(node.source, node.source.value);
|
|
77
77
|
},
|
|
@@ -2,25 +2,25 @@ import { AST_NODE_TYPES, ASTUtils, type TSESTree } from "@typescript-eslint/util
|
|
|
2
2
|
import { createRule } from "../utils/create-rule";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* `@simplysm/core-common
|
|
5
|
+
* ESLint rule that detects and warns about the use of `NotImplementedError` from `@simplysm/core-common`.
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* This rule detects code that instantiates `NotImplementedError` imported from `@simplysm/core-common` using `new`.
|
|
9
|
+
* It prevents unimplemented code from being included in production.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Supported import forms:
|
|
12
12
|
* - named import: `import { NotImplementedError } from "@simplysm/core-common"`
|
|
13
13
|
* - aliased import: `import { NotImplementedError as NIE } from "@simplysm/core-common"`
|
|
14
14
|
* - namespace import: `import * as CC from "@simplysm/core-common"` → `new CC.NotImplementedError()`
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* Dynamic imports (`await import(...)`) are not detected.
|
|
17
17
|
*/
|
|
18
18
|
export default createRule({
|
|
19
19
|
name: "ts-no-throw-not-implemented-error",
|
|
20
20
|
meta: {
|
|
21
21
|
type: "suggestion",
|
|
22
22
|
docs: {
|
|
23
|
-
description: "'NotImplementedError'
|
|
23
|
+
description: "Warns about 'NotImplementedError' usage",
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
messages: {
|
|
@@ -30,10 +30,10 @@ export default createRule({
|
|
|
30
30
|
defaultOptions: [],
|
|
31
31
|
create(context) {
|
|
32
32
|
/**
|
|
33
|
-
* identifier
|
|
34
|
-
* @param identifier -
|
|
35
|
-
* @param expectedImportedName -
|
|
36
|
-
* @returns import
|
|
33
|
+
* Checks if an identifier is imported from @simplysm/core-common.
|
|
34
|
+
* @param identifier - The identifier to check
|
|
35
|
+
* @param expectedImportedName - The original name to check for named imports (undefined for namespace imports)
|
|
36
|
+
* @returns true if the import source is @simplysm/core-common, false otherwise
|
|
37
37
|
*/
|
|
38
38
|
function isImportedFromSimplysm(
|
|
39
39
|
identifier: TSESTree.Identifier,
|
|
@@ -48,7 +48,7 @@ export default createRule({
|
|
|
48
48
|
if (def.parent.type !== AST_NODE_TYPES.ImportDeclaration) continue;
|
|
49
49
|
if (def.parent.source.value !== "@simplysm/core-common") continue;
|
|
50
50
|
|
|
51
|
-
// named/aliased import: import { NotImplementedError }
|
|
51
|
+
// named/aliased import: import { NotImplementedError } or import { NotImplementedError as NIE }
|
|
52
52
|
if (def.node.type === AST_NODE_TYPES.ImportSpecifier && expectedImportedName != null) {
|
|
53
53
|
const imported = def.node.imported;
|
|
54
54
|
if (
|
|
@@ -75,7 +75,7 @@ export default createRule({
|
|
|
75
75
|
NewExpression(node: TSESTree.NewExpression) {
|
|
76
76
|
let shouldReport = false;
|
|
77
77
|
|
|
78
|
-
// Case 1: new NotImplementedError()
|
|
78
|
+
// Case 1: new NotImplementedError() or new NIE() (named/aliased import)
|
|
79
79
|
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
|
80
80
|
shouldReport = isImportedFromSimplysm(node.callee, "NotImplementedError");
|
|
81
81
|
}
|
|
@@ -92,7 +92,7 @@ export default createRule({
|
|
|
92
92
|
|
|
93
93
|
if (!shouldReport) return;
|
|
94
94
|
|
|
95
|
-
let msg = "
|
|
95
|
+
let msg = "Not implemented";
|
|
96
96
|
const firstArg = node.arguments.at(0);
|
|
97
97
|
if (
|
|
98
98
|
firstArg?.type === AST_NODE_TYPES.Literal &&
|
|
@@ -2,7 +2,7 @@ export default {
|
|
|
2
2
|
extends: ["stylelint-config-standard", "stylelint-config-tailwindcss"],
|
|
3
3
|
plugins: ["stylelint-no-unsupported-browser-features", "stylelint-no-unresolved-module"],
|
|
4
4
|
rules: {
|
|
5
|
-
// Chrome 84+
|
|
5
|
+
// Chrome 84+ compatibility check
|
|
6
6
|
"plugin/no-unsupported-browser-features": [
|
|
7
7
|
true,
|
|
8
8
|
{
|
|
@@ -11,9 +11,9 @@ export default {
|
|
|
11
11
|
ignore: ["css-cascade-layers", "css-nesting", "css-overflow"],
|
|
12
12
|
},
|
|
13
13
|
],
|
|
14
|
-
// inset
|
|
14
|
+
// inset requires Chrome 87+, so enforce shorthand is disabled
|
|
15
15
|
"declaration-block-no-redundant-longhand-properties": [true, { ignoreShorthands: ["inset"] }],
|
|
16
|
-
// @import
|
|
16
|
+
// Check for file existence in @import and url()
|
|
17
17
|
"plugin/no-unresolved-module": true,
|
|
18
18
|
},
|
|
19
19
|
};
|
package/src/utils/create-rule.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Factory function to create ESLint rules.
|
|
5
5
|
*
|
|
6
6
|
* @remarks
|
|
7
|
-
* `@typescript-eslint/utils
|
|
8
|
-
*
|
|
7
|
+
* Wraps `RuleCreator` from `@typescript-eslint/utils` and
|
|
8
|
+
* automatically generates rule documentation URLs.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
11
11
|
* ```typescript
|