@ethlete/eslint-plugin 0.1.0-next.1 → 0.1.0-next.3
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 +12 -0
- package/package.json +3 -2
- package/src/configs/recommended.js +51 -10
- package/src/index.js +22 -2
- package/src/rules/angular-decorator-property-order.js +3 -15
- package/src/rules/class-constant-property.js +344 -0
- package/src/rules/class-member-order.js +431 -0
- package/src/rules/inject-member-accessibility.js +5 -198
- package/src/rules/internals/angular-member-visibility.js +233 -0
- package/src/rules/internals/implemented-contract-members.js +294 -0
- package/src/rules/no-empty-angular-metadata-arrays.js +104 -0
- package/src/rules/no-empty-newlines-between-imports.js +45 -0
- package/src/rules/no-leading-underscore-class-member.js +120 -0
- package/src/rules/no-legacy-angular-decorators.js +196 -1
- package/src/rules/no-public-property.js +47 -1
- package/src/rules/no-redundant-internal.js +94 -0
- package/src/rules/no-standalone-flag.js +95 -0
- package/src/rules/no-typed-injected-element-ref.js +88 -0
- package/src/rules/require-on-push-change-detection.js +271 -0
- package/src/rules/require-view-encapsulation-none.js +215 -52
- package/src/rules/template-member-accessibility.js +190 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} value
|
|
9
|
+
*/
|
|
10
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {any} decorator
|
|
14
|
+
*/
|
|
15
|
+
const getDecoratorCall = (decorator) => {
|
|
16
|
+
if (!decorator || decorator.type !== 'Decorator') return null;
|
|
17
|
+
|
|
18
|
+
const expression = decorator.expression;
|
|
19
|
+
if (expression.type !== 'CallExpression') return null;
|
|
20
|
+
if (expression.callee.type !== 'Identifier') return null;
|
|
21
|
+
if (expression.arguments.length === 0) return null;
|
|
22
|
+
|
|
23
|
+
const metadata = expression.arguments[0];
|
|
24
|
+
if (!metadata || metadata.type !== 'ObjectExpression') return null;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
name: expression.callee.name,
|
|
28
|
+
metadata,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {any} property
|
|
34
|
+
*/
|
|
35
|
+
const getPropertyKeyName = (property) => {
|
|
36
|
+
if (!property || property.computed) return null;
|
|
37
|
+
|
|
38
|
+
const key = property.key;
|
|
39
|
+
if (key.type === 'Identifier') return key.name;
|
|
40
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {any} metadata
|
|
47
|
+
* @param {string} propertyName
|
|
48
|
+
*/
|
|
49
|
+
const getMetadataProperty = (metadata, propertyName) => {
|
|
50
|
+
for (const property of metadata.properties) {
|
|
51
|
+
if (!property || property.type !== 'Property' || property.computed) continue;
|
|
52
|
+
|
|
53
|
+
const keyName = getPropertyKeyName(property);
|
|
54
|
+
|
|
55
|
+
if (keyName === propertyName) {
|
|
56
|
+
return property;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {any} node
|
|
65
|
+
*/
|
|
66
|
+
const getStringValue = (node) => {
|
|
67
|
+
if (!node) return null;
|
|
68
|
+
if (node.type === 'Literal' && typeof node.value === 'string') return node.value;
|
|
69
|
+
|
|
70
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length === 1) {
|
|
71
|
+
return node.quasis[0].value.cooked || '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} filePath
|
|
79
|
+
*/
|
|
80
|
+
const readFileIfExists = (filePath) => {
|
|
81
|
+
try {
|
|
82
|
+
if (fs.existsSync(filePath)) {
|
|
83
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
94
|
+
*/
|
|
95
|
+
const getContextFilename = (context) => {
|
|
96
|
+
const filename = context.physicalFilename || context.filename;
|
|
97
|
+
|
|
98
|
+
if (!filename || filename === '<input>' || filename === '<text>') {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return filename;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} memberName
|
|
107
|
+
* @param {string | null} template
|
|
108
|
+
*/
|
|
109
|
+
const templateReferencesMember = (memberName, template) => {
|
|
110
|
+
if (!template) return false;
|
|
111
|
+
|
|
112
|
+
const escapedName = escapeRegExp(memberName);
|
|
113
|
+
const patterns = [
|
|
114
|
+
new RegExp(`\\{\\{[\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\}\\}`, 'u'),
|
|
115
|
+
new RegExp(`@[a-zA-Z]+\\s*\\([\\s\\S]*?\\b${escapedName}\\b[\\s\\S]*?\\)`, 'u'),
|
|
116
|
+
new RegExp(`@let\\s+[a-zA-Z_$][\\w$]*\\s*=\\s*[\\s\\S]*?\\b${escapedName}\\b`, 'u'),
|
|
117
|
+
new RegExp(`\\*[a-zA-Z0-9_\\-]+\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
118
|
+
new RegExp(`\\*[a-zA-Z0-9_\\-]+\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
119
|
+
new RegExp(`\\[[^\\]]+\\]\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
120
|
+
new RegExp(`\\[[^\\]]+\\]\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
121
|
+
new RegExp(`\\([^\\)]+\\)\\s*=\\s*"[^"]*\\b${escapedName}\\b[^"]*"`, 'u'),
|
|
122
|
+
new RegExp(`\\([^\\)]+\\)\\s*=\\s*'[^']*\\b${escapedName}\\b[^']*'`, 'u'),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
return patterns.some((pattern) => pattern.test(template));
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {string | null} keyName
|
|
130
|
+
*/
|
|
131
|
+
const isDynamicHostBindingKey = (keyName) => {
|
|
132
|
+
if (!keyName) return false;
|
|
133
|
+
|
|
134
|
+
return keyName.startsWith('[') || keyName.startsWith('(') || keyName.startsWith('@');
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {any} classNode
|
|
139
|
+
*/
|
|
140
|
+
const getAngularMetadata = (classNode) => {
|
|
141
|
+
for (const decorator of classNode.decorators || []) {
|
|
142
|
+
const call = getDecoratorCall(decorator);
|
|
143
|
+
if (!call) continue;
|
|
144
|
+
|
|
145
|
+
if (call.name === 'Component' || call.name === 'Directive') {
|
|
146
|
+
return call;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {string} memberName
|
|
155
|
+
* @param {string | null} text
|
|
156
|
+
*/
|
|
157
|
+
const textReferencesMember = (memberName, text) => {
|
|
158
|
+
if (!text) return false;
|
|
159
|
+
|
|
160
|
+
const pattern = new RegExp(`\\b${escapeRegExp(memberName)}\\b`, 'u');
|
|
161
|
+
return pattern.test(text);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} memberName
|
|
166
|
+
* @param {any} metadata
|
|
167
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
168
|
+
*/
|
|
169
|
+
const isReferencedFromTemplateOrHostMetadata = (memberName, metadata, context) => {
|
|
170
|
+
const templateProperty = getMetadataProperty(metadata, 'template');
|
|
171
|
+
if (templateProperty && templateReferencesMember(memberName, getStringValue(templateProperty.value))) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const templateUrlProperty = getMetadataProperty(metadata, 'templateUrl');
|
|
176
|
+
if (templateUrlProperty) {
|
|
177
|
+
const templateUrl = getStringValue(templateUrlProperty.value);
|
|
178
|
+
const filename = getContextFilename(context);
|
|
179
|
+
|
|
180
|
+
if (templateUrl && filename) {
|
|
181
|
+
const templatePath = path.resolve(path.dirname(filename), templateUrl);
|
|
182
|
+
const templateText = readFileIfExists(templatePath);
|
|
183
|
+
if (templateReferencesMember(memberName, templateText)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const hostProperty = getMetadataProperty(metadata, 'host');
|
|
190
|
+
if (hostProperty && hostProperty.value.type === 'ObjectExpression') {
|
|
191
|
+
for (const property of hostProperty.value.properties) {
|
|
192
|
+
if (!property || property.type !== 'Property') continue;
|
|
193
|
+
if (!isDynamicHostBindingKey(getPropertyKeyName(property))) continue;
|
|
194
|
+
|
|
195
|
+
if (textReferencesMember(memberName, getStringValue(property.value))) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return false;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {any} node
|
|
206
|
+
*/
|
|
207
|
+
const getMemberName = (node) => {
|
|
208
|
+
if (node.key?.type === 'Identifier') return node.key.name;
|
|
209
|
+
if (node.key?.type === 'Literal' && typeof node.key.value === 'string') return node.key.value;
|
|
210
|
+
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {any} classNode
|
|
216
|
+
* @param {string} memberName
|
|
217
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
218
|
+
*/
|
|
219
|
+
const isReferencedFromTemplateOrHost = (classNode, memberName, context) => {
|
|
220
|
+
const angularMetadata = getAngularMetadata(classNode);
|
|
221
|
+
|
|
222
|
+
if (!angularMetadata) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return isReferencedFromTemplateOrHostMetadata(memberName, angularMetadata.metadata, context);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
getAngularMetadata,
|
|
231
|
+
getMemberName,
|
|
232
|
+
isReferencedFromTemplateOrHost,
|
|
233
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const ts = require('typescript');
|
|
7
|
+
|
|
8
|
+
/** @type {Map<string, import('typescript').SourceFile | null>} */
|
|
9
|
+
const SOURCE_FILE_CACHE = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} filePath
|
|
13
|
+
*/
|
|
14
|
+
const readFileIfExists = (filePath) => {
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(filePath)) {
|
|
17
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {{ filePath: string, text?: string | null }} config
|
|
28
|
+
*/
|
|
29
|
+
const getSourceFile = ({ filePath, text = null }) => {
|
|
30
|
+
if (text !== null) {
|
|
31
|
+
return ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (SOURCE_FILE_CACHE.has(filePath)) {
|
|
35
|
+
return SOURCE_FILE_CACHE.get(filePath) ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fileText = readFileIfExists(filePath);
|
|
39
|
+
|
|
40
|
+
if (fileText === null) {
|
|
41
|
+
SOURCE_FILE_CACHE.set(filePath, null);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sourceFile = ts.createSourceFile(filePath, fileText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
46
|
+
SOURCE_FILE_CACHE.set(filePath, sourceFile);
|
|
47
|
+
return sourceFile;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} importingFilePath
|
|
52
|
+
* @param {string} specifier
|
|
53
|
+
*/
|
|
54
|
+
const resolveRelativeModule = (importingFilePath, specifier) => {
|
|
55
|
+
if (!specifier.startsWith('.')) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const basePath = path.resolve(path.dirname(importingFilePath), specifier);
|
|
60
|
+
const candidates = [
|
|
61
|
+
`${basePath}.ts`,
|
|
62
|
+
`${basePath}.tsx`,
|
|
63
|
+
`${basePath}.d.ts`,
|
|
64
|
+
path.join(basePath, 'index.ts'),
|
|
65
|
+
path.join(basePath, 'index.d.ts'),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (fs.existsSync(candidate)) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {import('typescript').TypeElement | import('typescript').ClassElement} member
|
|
79
|
+
*/
|
|
80
|
+
const getMemberName = (member) => {
|
|
81
|
+
if (!member.name) return null;
|
|
82
|
+
|
|
83
|
+
if (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) {
|
|
84
|
+
return member.name.text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)) {
|
|
88
|
+
return String(member.name.text);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {{ sourceFile: import('typescript').SourceFile, typeName: string, currentFilePath: string, visited: Set<string>, currentFileText?: string | null }} config
|
|
96
|
+
*/
|
|
97
|
+
const collectMemberNamesForType = ({ sourceFile, typeName, currentFilePath, visited, currentFileText = null }) => {
|
|
98
|
+
const visitKey = `${sourceFile.fileName}::${typeName}`;
|
|
99
|
+
if (visited.has(visitKey)) {
|
|
100
|
+
return new Set();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
visited.add(visitKey);
|
|
104
|
+
|
|
105
|
+
/** @type {Set<string>} */
|
|
106
|
+
const memberNames = new Set();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {import('typescript').TypeNode | undefined} typeNode
|
|
110
|
+
* @param {string} filePath
|
|
111
|
+
* @param {string | null} fileText
|
|
112
|
+
*/
|
|
113
|
+
const collectFromTypeNode = (typeNode, filePath, fileText) => {
|
|
114
|
+
if (!typeNode) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ts.isTypeLiteralNode(typeNode)) {
|
|
119
|
+
for (const member of typeNode.members) {
|
|
120
|
+
const memberName = getMemberName(member);
|
|
121
|
+
if (memberName) memberNames.add(memberName);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (ts.isIntersectionTypeNode(typeNode)) {
|
|
128
|
+
for (const part of typeNode.types) {
|
|
129
|
+
collectFromTypeNode(part, filePath, fileText);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (ts.isTypeReferenceNode(typeNode) && ts.isIdentifier(typeNode.typeName)) {
|
|
136
|
+
const referencedMembers = collectMemberNamesForTypeByName({
|
|
137
|
+
sourceFile: getSourceFile({ filePath, text: fileText }),
|
|
138
|
+
typeName: typeNode.typeName.text,
|
|
139
|
+
currentFilePath: filePath,
|
|
140
|
+
visited,
|
|
141
|
+
currentFileText: fileText,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
for (const referencedMember of referencedMembers) {
|
|
145
|
+
memberNames.add(referencedMember);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const statements = sourceFile.statements;
|
|
151
|
+
|
|
152
|
+
for (const statement of statements) {
|
|
153
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name.text === typeName) {
|
|
154
|
+
for (const member of statement.members) {
|
|
155
|
+
const memberName = getMemberName(member);
|
|
156
|
+
if (memberName) memberNames.add(memberName);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (statement.heritageClauses) {
|
|
160
|
+
for (const clause of statement.heritageClauses) {
|
|
161
|
+
if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
162
|
+
|
|
163
|
+
for (const heritageType of clause.types) {
|
|
164
|
+
if (!ts.isIdentifier(heritageType.expression)) continue;
|
|
165
|
+
|
|
166
|
+
const inheritedMembers = collectMemberNamesForTypeByName({
|
|
167
|
+
sourceFile,
|
|
168
|
+
typeName: heritageType.expression.text,
|
|
169
|
+
currentFilePath,
|
|
170
|
+
visited,
|
|
171
|
+
currentFileText,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
for (const inheritedMember of inheritedMembers) {
|
|
175
|
+
memberNames.add(inheritedMember);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return memberNames;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (ts.isTypeAliasDeclaration(statement) && statement.name.text === typeName) {
|
|
185
|
+
collectFromTypeNode(
|
|
186
|
+
statement.type,
|
|
187
|
+
sourceFile.fileName,
|
|
188
|
+
sourceFile.fileName === currentFilePath ? currentFileText : null,
|
|
189
|
+
);
|
|
190
|
+
return memberNames;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const statement of statements) {
|
|
195
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
196
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
197
|
+
if (!statement.importClause || !statement.importClause.namedBindings) continue;
|
|
198
|
+
if (!ts.isNamedImports(statement.importClause.namedBindings)) continue;
|
|
199
|
+
|
|
200
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
201
|
+
const localName = element.name.text;
|
|
202
|
+
if (localName !== typeName) continue;
|
|
203
|
+
|
|
204
|
+
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
205
|
+
const resolvedModulePath = resolveRelativeModule(sourceFile.fileName, statement.moduleSpecifier.text);
|
|
206
|
+
if (!resolvedModulePath) {
|
|
207
|
+
return memberNames;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const importedSourceFile = getSourceFile({ filePath: resolvedModulePath });
|
|
211
|
+
if (!importedSourceFile) {
|
|
212
|
+
return memberNames;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const importedMembers = collectMemberNamesForTypeByName({
|
|
216
|
+
sourceFile: importedSourceFile,
|
|
217
|
+
typeName: importedName,
|
|
218
|
+
currentFilePath: resolvedModulePath,
|
|
219
|
+
visited,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
for (const importedMember of importedMembers) {
|
|
223
|
+
memberNames.add(importedMember);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return memberNames;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return memberNames;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {{ sourceFile: import('typescript').SourceFile | null, typeName: string, currentFilePath: string, visited: Set<string>, currentFileText?: string | null }} config
|
|
235
|
+
*/
|
|
236
|
+
const collectMemberNamesForTypeByName = ({
|
|
237
|
+
sourceFile,
|
|
238
|
+
typeName,
|
|
239
|
+
currentFilePath,
|
|
240
|
+
visited,
|
|
241
|
+
currentFileText = null,
|
|
242
|
+
}) => {
|
|
243
|
+
if (!sourceFile) {
|
|
244
|
+
return new Set();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return collectMemberNamesForType({
|
|
248
|
+
sourceFile,
|
|
249
|
+
typeName,
|
|
250
|
+
currentFilePath,
|
|
251
|
+
visited,
|
|
252
|
+
currentFileText,
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @param {{ classNode: any, context: import('eslint').Rule.RuleContext }} config
|
|
258
|
+
*/
|
|
259
|
+
const getImplementedContractMemberNames = ({ classNode, context }) => {
|
|
260
|
+
const filename = context.physicalFilename || context.filename;
|
|
261
|
+
if (!filename || filename === '<input>' || filename === '<text>') {
|
|
262
|
+
return new Set();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const currentSourceFile = getSourceFile({ filePath: filename, text: context.sourceCode.text });
|
|
266
|
+
if (!currentSourceFile) {
|
|
267
|
+
return new Set();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** @type {Set<string>} */
|
|
271
|
+
const memberNames = new Set();
|
|
272
|
+
|
|
273
|
+
for (const implementedType of classNode.implements || []) {
|
|
274
|
+
if (!implementedType.expression || implementedType.expression.type !== 'Identifier') continue;
|
|
275
|
+
|
|
276
|
+
const typeMemberNames = collectMemberNamesForTypeByName({
|
|
277
|
+
sourceFile: currentSourceFile,
|
|
278
|
+
typeName: implementedType.expression.name,
|
|
279
|
+
currentFilePath: filename,
|
|
280
|
+
visited: new Set(),
|
|
281
|
+
currentFileText: context.sourceCode.text,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
for (const memberName of typeMemberNames) {
|
|
285
|
+
memberNames.add(memberName);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return memberNames;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
getImplementedContractMemberNames,
|
|
294
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/** @typedef {'Component' | 'Directive'} TDecoratorName */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('estree').Property['key']} key
|
|
8
|
+
*/
|
|
9
|
+
const getPropertyName = (key) => {
|
|
10
|
+
if (key.type === 'Identifier') return key.name;
|
|
11
|
+
if (key.type === 'Literal' && typeof key.value === 'string') return key.value;
|
|
12
|
+
return null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('eslint').Rule.Node} node
|
|
17
|
+
* @returns {TDecoratorName | null}
|
|
18
|
+
*/
|
|
19
|
+
const getDecoratorName = (node) => {
|
|
20
|
+
const decorator = /** @type {any} */ (node);
|
|
21
|
+
if (decorator.type !== 'Decorator') return null;
|
|
22
|
+
|
|
23
|
+
const expression = decorator.expression;
|
|
24
|
+
if (expression.type === 'CallExpression') {
|
|
25
|
+
return expression.callee.type === 'Identifier' ? expression.callee.name : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return expression.type === 'Identifier' ? expression.name : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
33
|
+
* @param {any} metadata
|
|
34
|
+
* @param {any} property
|
|
35
|
+
*/
|
|
36
|
+
const getRemovalRange = (sourceCode, metadata, property) => {
|
|
37
|
+
const properties = metadata.properties.filter((entry) => entry.type === 'Property');
|
|
38
|
+
const propertyIndex = properties.indexOf(property);
|
|
39
|
+
const openingBrace = sourceCode.getFirstToken(metadata);
|
|
40
|
+
const closingBrace = sourceCode.getLastToken(metadata);
|
|
41
|
+
|
|
42
|
+
if (!openingBrace || !closingBrace) return property.range;
|
|
43
|
+
if (properties.length === 1) return [openingBrace.range[1], closingBrace.range[0]];
|
|
44
|
+
if (propertyIndex < properties.length - 1) return [property.range[0], properties[propertyIndex + 1].range[0]];
|
|
45
|
+
|
|
46
|
+
return [properties[propertyIndex - 1].range[1], property.range[1]];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const MESSAGE_IDS = {
|
|
50
|
+
hostDirectives: 'noEmptyHostDirectives',
|
|
51
|
+
imports: 'noEmptyImports',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
55
|
+
const noEmptyAngularMetadataArrays = {
|
|
56
|
+
meta: {
|
|
57
|
+
type: 'suggestion',
|
|
58
|
+
docs: {
|
|
59
|
+
description: 'Disallow empty Angular metadata arrays such as imports: [] and hostDirectives: [].',
|
|
60
|
+
},
|
|
61
|
+
fixable: 'code',
|
|
62
|
+
schema: [],
|
|
63
|
+
messages: {
|
|
64
|
+
noEmptyImports: 'Remove empty imports: [] from Angular metadata.',
|
|
65
|
+
noEmptyHostDirectives: 'Remove empty hostDirectives: [] from Angular metadata.',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
create(context) {
|
|
69
|
+
const sourceCode = context.sourceCode;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
/** @param {import('eslint').Rule.Node} node */
|
|
73
|
+
Decorator(node) {
|
|
74
|
+
const decoratorName = getDecoratorName(node);
|
|
75
|
+
if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
|
|
76
|
+
|
|
77
|
+
const decorator = /** @type {any} */ (node);
|
|
78
|
+
const expression = decorator.expression;
|
|
79
|
+
if (expression.type !== 'CallExpression' || expression.arguments.length === 0) return;
|
|
80
|
+
|
|
81
|
+
const metadata = expression.arguments[0];
|
|
82
|
+
if (!metadata || metadata.type !== 'ObjectExpression') return;
|
|
83
|
+
|
|
84
|
+
for (const property of metadata.properties) {
|
|
85
|
+
if (property.type !== 'Property') continue;
|
|
86
|
+
|
|
87
|
+
const propertyName = getPropertyName(property.key);
|
|
88
|
+
if (propertyName !== 'imports' && propertyName !== 'hostDirectives') continue;
|
|
89
|
+
if (property.value.type !== 'ArrayExpression' || property.value.elements.length > 0) continue;
|
|
90
|
+
|
|
91
|
+
context.report({
|
|
92
|
+
node: property,
|
|
93
|
+
messageId: MESSAGE_IDS[propertyName],
|
|
94
|
+
fix(fixer) {
|
|
95
|
+
return fixer.replaceTextRange(getRemovalRange(sourceCode, metadata, property), '');
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = noEmptyAngularMetadataArrays;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
5
|
+
const noEmptyNewlinesBetweenImports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'layout',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Disallow empty blank lines between consecutive import declarations.',
|
|
10
|
+
},
|
|
11
|
+
fixable: 'code',
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
noEmptyLine: 'Remove empty blank lines between import declarations.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const sourceCode = context.sourceCode;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
Program(node) {
|
|
22
|
+
const importDeclarations = node.body.filter((statement) => statement.type === 'ImportDeclaration');
|
|
23
|
+
|
|
24
|
+
for (let index = 1; index < importDeclarations.length; index += 1) {
|
|
25
|
+
const previousImport = importDeclarations[index - 1];
|
|
26
|
+
const currentImport = importDeclarations[index];
|
|
27
|
+
const textBetween = sourceCode.text.slice(previousImport.range[1], currentImport.range[0]);
|
|
28
|
+
|
|
29
|
+
if (/\S/.test(textBetween)) continue;
|
|
30
|
+
if (!/\n\s*\n/.test(textBetween)) continue;
|
|
31
|
+
|
|
32
|
+
context.report({
|
|
33
|
+
node: currentImport,
|
|
34
|
+
messageId: 'noEmptyLine',
|
|
35
|
+
fix(fixer) {
|
|
36
|
+
return fixer.replaceTextRange([previousImport.range[1], currentImport.range[0]], '\n');
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
module.exports = noEmptyNewlinesBetweenImports;
|