@craft-ng/dev-tools 0.5.0-beta.1 → 0.5.0-beta.2
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/package.json +1 -1
- package/src/eslint-rules/craft-computed-name-match.cjs +125 -0
- package/src/eslint-rules/craft-method-name-match.cjs +166 -0
- package/src/eslint-rules/index.cjs +10 -0
- package/src/eslint-rules/prefer-craft-computed.cjs +105 -0
- package/src/eslint-rules/provide-host-name-match-component.cjs +455 -0
- package/src/eslint-rules/require-component-monitoring.cjs +212 -0
package/package.json
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
"Ensure craftComputed(name, ...) is called with a string literal first argument that matches the declared variable or class property name.",
|
|
7
|
+
},
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
missingName:
|
|
12
|
+
"craftComputed must be called with a string literal name matching '{{declaredName}}' as the first argument.",
|
|
13
|
+
mismatchedName:
|
|
14
|
+
"craftComputed first argument '{{actual}}' must match the declared name '{{declaredName}}'.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (
|
|
23
|
+
node.callee.type !== 'Identifier' ||
|
|
24
|
+
node.callee.name !== 'craftComputed'
|
|
25
|
+
) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const declaredName = getDeclaredName(node);
|
|
30
|
+
if (!declaredName) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const firstArg = node.arguments[0];
|
|
35
|
+
|
|
36
|
+
if (!firstArg) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: 'missingName',
|
|
40
|
+
data: { declaredName },
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
const openParen = sourceCode.getTokenAfter(
|
|
43
|
+
node.callee,
|
|
44
|
+
(token) => token.type === 'Punctuator' && token.value === '(',
|
|
45
|
+
);
|
|
46
|
+
if (!openParen) return null;
|
|
47
|
+
return fixer.insertTextAfter(openParen, `'${declaredName}'`);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isStringLiteral(firstArg)) {
|
|
54
|
+
const actual = getStringLiteralValue(firstArg);
|
|
55
|
+
if (actual === declaredName) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
context.report({
|
|
59
|
+
node: firstArg,
|
|
60
|
+
messageId: 'mismatchedName',
|
|
61
|
+
data: { declaredName, actual },
|
|
62
|
+
fix(fixer) {
|
|
63
|
+
return fixer.replaceText(firstArg, `'${declaredName}'`);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
context.report({
|
|
70
|
+
node: firstArg,
|
|
71
|
+
messageId: 'missingName',
|
|
72
|
+
data: { declaredName },
|
|
73
|
+
fix(fixer) {
|
|
74
|
+
return fixer.insertTextBefore(firstArg, `'${declaredName}', `);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function getDeclaredName(callNode) {
|
|
83
|
+
const parent = callNode.parent;
|
|
84
|
+
if (!parent) return undefined;
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
parent.type === 'VariableDeclarator' &&
|
|
88
|
+
parent.init === callNode &&
|
|
89
|
+
parent.id.type === 'Identifier'
|
|
90
|
+
) {
|
|
91
|
+
return parent.id.name;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
parent.type === 'PropertyDefinition' &&
|
|
96
|
+
parent.value === callNode &&
|
|
97
|
+
!parent.computed &&
|
|
98
|
+
parent.key.type === 'Identifier'
|
|
99
|
+
) {
|
|
100
|
+
return parent.key.name;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isStringLiteral(node) {
|
|
107
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
node.type === 'TemplateLiteral' &&
|
|
112
|
+
node.expressions.length === 0 &&
|
|
113
|
+
node.quasis.length === 1
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getStringLiteralValue(node) {
|
|
121
|
+
if (node.type === 'Literal') {
|
|
122
|
+
return node.value;
|
|
123
|
+
}
|
|
124
|
+
return node.quasis[0].value.cooked;
|
|
125
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
"Ensure craftMethod(name, ...) is called with a string literal first argument (or { name } object) that matches the declared variable or class property name.",
|
|
7
|
+
},
|
|
8
|
+
fixable: 'code',
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
missingName:
|
|
12
|
+
"craftMethod must be called with a string literal name matching '{{declaredName}}' as the first argument.",
|
|
13
|
+
mismatchedName:
|
|
14
|
+
"craftMethod first argument '{{actual}}' must match the declared name '{{declaredName}}'.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (
|
|
23
|
+
node.callee.type !== 'Identifier' ||
|
|
24
|
+
node.callee.name !== 'craftMethod'
|
|
25
|
+
) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const declaredName = getDeclaredName(node);
|
|
30
|
+
if (!declaredName) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const firstArg = node.arguments[0];
|
|
35
|
+
|
|
36
|
+
if (!firstArg) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: 'missingName',
|
|
40
|
+
data: { declaredName },
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
const openParen = sourceCode.getTokenAfter(
|
|
43
|
+
node.callee,
|
|
44
|
+
(token) => token.type === 'Punctuator' && token.value === '(',
|
|
45
|
+
);
|
|
46
|
+
if (!openParen) return null;
|
|
47
|
+
return fixer.insertTextAfter(openParen, `'${declaredName}'`);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle object config form: { name: 'methodName', providers: [...] }
|
|
54
|
+
if (firstArg.type === 'ObjectExpression') {
|
|
55
|
+
const nameProp = firstArg.properties.find(
|
|
56
|
+
(p) =>
|
|
57
|
+
p.type === 'Property' &&
|
|
58
|
+
!p.computed &&
|
|
59
|
+
p.key.type === 'Identifier' &&
|
|
60
|
+
p.key.name === 'name',
|
|
61
|
+
);
|
|
62
|
+
if (!nameProp) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: firstArg,
|
|
65
|
+
messageId: 'missingName',
|
|
66
|
+
data: { declaredName },
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const nameValue = nameProp.value;
|
|
71
|
+
if (isStringLiteral(nameValue)) {
|
|
72
|
+
const actual = getStringLiteralValue(nameValue);
|
|
73
|
+
if (actual === declaredName) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
context.report({
|
|
77
|
+
node: nameValue,
|
|
78
|
+
messageId: 'mismatchedName',
|
|
79
|
+
data: { declaredName, actual },
|
|
80
|
+
fix(fixer) {
|
|
81
|
+
return fixer.replaceText(nameValue, `'${declaredName}'`);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
context.report({
|
|
86
|
+
node: nameValue,
|
|
87
|
+
messageId: 'mismatchedName',
|
|
88
|
+
data: { declaredName, actual: sourceCode.getText(nameValue) },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isStringLiteral(firstArg)) {
|
|
95
|
+
const actual = getStringLiteralValue(firstArg);
|
|
96
|
+
if (actual === declaredName) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
context.report({
|
|
100
|
+
node: firstArg,
|
|
101
|
+
messageId: 'mismatchedName',
|
|
102
|
+
data: { declaredName, actual },
|
|
103
|
+
fix(fixer) {
|
|
104
|
+
return fixer.replaceText(firstArg, `'${declaredName}'`);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
context.report({
|
|
111
|
+
node: firstArg,
|
|
112
|
+
messageId: 'missingName',
|
|
113
|
+
data: { declaredName },
|
|
114
|
+
fix(fixer) {
|
|
115
|
+
return fixer.insertTextBefore(firstArg, `'${declaredName}', `);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function getDeclaredName(callNode) {
|
|
124
|
+
const parent = callNode.parent;
|
|
125
|
+
if (!parent) return undefined;
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
parent.type === 'VariableDeclarator' &&
|
|
129
|
+
parent.init === callNode &&
|
|
130
|
+
parent.id.type === 'Identifier'
|
|
131
|
+
) {
|
|
132
|
+
return parent.id.name;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
parent.type === 'PropertyDefinition' &&
|
|
137
|
+
parent.value === callNode &&
|
|
138
|
+
!parent.computed &&
|
|
139
|
+
parent.key.type === 'Identifier'
|
|
140
|
+
) {
|
|
141
|
+
return parent.key.name;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isStringLiteral(node) {
|
|
148
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (
|
|
152
|
+
node.type === 'TemplateLiteral' &&
|
|
153
|
+
node.expressions.length === 0 &&
|
|
154
|
+
node.quasis.length === 1
|
|
155
|
+
) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getStringLiteralValue(node) {
|
|
162
|
+
if (node.type === 'Literal') {
|
|
163
|
+
return node.value;
|
|
164
|
+
}
|
|
165
|
+
return node.quasis[0].value.cooked;
|
|
166
|
+
}
|
|
@@ -2,11 +2,16 @@ const brandAngularGenDepsRequired = require('./brand-angular-gen-deps-required.c
|
|
|
2
2
|
const brandAngularDepsMatch = require('./brand-angular-deps-match.cjs');
|
|
3
3
|
const appStartRegistryMatch = require('./app-start-registry-match.cjs');
|
|
4
4
|
const componentTestGenDepsMatch = require('./component-test-gen-deps-match.cjs');
|
|
5
|
+
const craftMethodNameMatch = require('./craft-method-name-match.cjs');
|
|
6
|
+
const craftComputedNameMatch = require('./craft-computed-name-match.cjs');
|
|
7
|
+
const preferCraftComputed = require('./prefer-craft-computed.cjs');
|
|
5
8
|
const noAngularInject = require('./no-angular-inject.cjs');
|
|
6
9
|
const noAngularSignalForms = require('./no-angular-signal-forms.cjs');
|
|
10
|
+
const provideHostNameMatchComponent = require('./provide-host-name-match-component.cjs');
|
|
7
11
|
const preferCraftHttpClient = require('./prefer-craft-http-client.cjs');
|
|
8
12
|
const preferCraftService = require('./prefer-craft-service.cjs');
|
|
9
13
|
const preferBrowserBoundaries = require('./prefer-browser-boundaries.cjs');
|
|
14
|
+
const requireComponentMonitoring = require('./require-component-monitoring.cjs');
|
|
10
15
|
|
|
11
16
|
module.exports = {
|
|
12
17
|
rules: {
|
|
@@ -14,10 +19,15 @@ module.exports = {
|
|
|
14
19
|
'brand-angular-gen-deps-required': brandAngularGenDepsRequired,
|
|
15
20
|
'brand-angular-deps-match': brandAngularDepsMatch,
|
|
16
21
|
'component-test-gen-deps-match': componentTestGenDepsMatch,
|
|
22
|
+
'craft-method-name-match': craftMethodNameMatch,
|
|
23
|
+
'craft-computed-name-match': craftComputedNameMatch,
|
|
24
|
+
'prefer-craft-computed': preferCraftComputed,
|
|
17
25
|
'no-angular-inject': noAngularInject,
|
|
18
26
|
'no-angular-signal-forms': noAngularSignalForms,
|
|
27
|
+
'provide-host-name-match-component': provideHostNameMatchComponent,
|
|
19
28
|
'prefer-craft-http-client': preferCraftHttpClient,
|
|
20
29
|
'prefer-craft-service': preferCraftService,
|
|
21
30
|
'prefer-browser-boundaries': preferBrowserBoundaries,
|
|
31
|
+
'require-component-monitoring': requireComponentMonitoring,
|
|
22
32
|
},
|
|
23
33
|
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'suggestion',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
"Prefer craftComputed() over computed() from @angular/core for better observability and host name tracking.",
|
|
7
|
+
},
|
|
8
|
+
hasSuggestions: true,
|
|
9
|
+
schema: [],
|
|
10
|
+
messages: {
|
|
11
|
+
preferCraftComputed:
|
|
12
|
+
"Use craftComputed('{{name}}', ...) instead of computed() for better observability. craftComputed adds HostName tracking to the computed signal.",
|
|
13
|
+
preferCraftComputedUnnamed:
|
|
14
|
+
"Use craftComputed('name', ...) instead of computed() for better observability. craftComputed adds HostName tracking to the computed signal.",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
CallExpression(node) {
|
|
20
|
+
if (
|
|
21
|
+
node.callee.type !== 'Identifier' ||
|
|
22
|
+
node.callee.name !== 'computed'
|
|
23
|
+
) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!isAngularComputedImport(node, context)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const declaredName = getDeclaredName(node);
|
|
32
|
+
|
|
33
|
+
if (declaredName) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: 'preferCraftComputed',
|
|
37
|
+
data: { name: declaredName },
|
|
38
|
+
suggest: [
|
|
39
|
+
{
|
|
40
|
+
desc: `Replace with craftComputed('${declaredName}', ...)`,
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
return fixer.replaceText(node.callee, 'craftComputed');
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: 'preferCraftComputedUnnamed',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function getDeclaredName(callNode) {
|
|
59
|
+
const parent = callNode.parent;
|
|
60
|
+
if (!parent) return undefined;
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
parent.type === 'VariableDeclarator' &&
|
|
64
|
+
parent.init === callNode &&
|
|
65
|
+
parent.id.type === 'Identifier'
|
|
66
|
+
) {
|
|
67
|
+
return parent.id.name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
parent.type === 'PropertyDefinition' &&
|
|
72
|
+
parent.value === callNode &&
|
|
73
|
+
!parent.computed &&
|
|
74
|
+
parent.key.type === 'Identifier'
|
|
75
|
+
) {
|
|
76
|
+
return parent.key.name;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isAngularComputedImport(callNode, context) {
|
|
83
|
+
const scope = context.getScope ? context.getScope() : context.sourceCode.getScope(callNode);
|
|
84
|
+
|
|
85
|
+
let currentScope = scope;
|
|
86
|
+
while (currentScope) {
|
|
87
|
+
for (const variable of currentScope.variables) {
|
|
88
|
+
if (variable.name === 'computed') {
|
|
89
|
+
for (const def of variable.defs) {
|
|
90
|
+
if (
|
|
91
|
+
def.type === 'ImportBinding' &&
|
|
92
|
+
def.parent &&
|
|
93
|
+
def.parent.source &&
|
|
94
|
+
def.parent.source.value === '@angular/core'
|
|
95
|
+
) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
currentScope = currentScope.upper;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const {
|
|
4
|
+
IndentationText,
|
|
5
|
+
Node,
|
|
6
|
+
Project,
|
|
7
|
+
QuoteKind,
|
|
8
|
+
SyntaxKind,
|
|
9
|
+
} = require('ts-morph');
|
|
10
|
+
|
|
11
|
+
const projectCache = new Map();
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
"Ensure Angular @Component and @Directive classes provide provideHostName('ClassName') in providers.",
|
|
19
|
+
},
|
|
20
|
+
fixable: 'code',
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
'Program:exit'() {
|
|
26
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
27
|
+
const filePath = getFilePath(context);
|
|
28
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const text = sourceCode.getText();
|
|
33
|
+
if (!text.includes('@Component') && !text.includes('@Directive')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sourceFile = getProjectSourceFile(
|
|
38
|
+
getProject(getCwd(context)),
|
|
39
|
+
filePath,
|
|
40
|
+
text,
|
|
41
|
+
);
|
|
42
|
+
const issues = collectHostNameIssues(sourceFile);
|
|
43
|
+
if (issues.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reportLoc = getNodeLoc(sourceCode, issues[0].reportNode);
|
|
48
|
+
|
|
49
|
+
for (const issue of issues) {
|
|
50
|
+
applyIssueFix(issue);
|
|
51
|
+
}
|
|
52
|
+
ensureProvideHostNameImport(sourceFile);
|
|
53
|
+
|
|
54
|
+
const fixedText = sourceFile.getFullText();
|
|
55
|
+
if (fixedText === text) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
context.report({
|
|
60
|
+
loc: reportLoc,
|
|
61
|
+
message: formatIssueMessage(issues),
|
|
62
|
+
fix(fixer) {
|
|
63
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function getProject(cwd) {
|
|
72
|
+
let project = projectCache.get(cwd);
|
|
73
|
+
if (project) {
|
|
74
|
+
return project;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const manipulationSettings = {
|
|
78
|
+
indentationText: IndentationText.TwoSpaces,
|
|
79
|
+
quoteKind: QuoteKind.Single,
|
|
80
|
+
};
|
|
81
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
82
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
83
|
+
? new Project({
|
|
84
|
+
tsConfigFilePath,
|
|
85
|
+
manipulationSettings,
|
|
86
|
+
})
|
|
87
|
+
: new Project({
|
|
88
|
+
compilerOptions: {
|
|
89
|
+
experimentalDecorators: true,
|
|
90
|
+
target: 9,
|
|
91
|
+
},
|
|
92
|
+
manipulationSettings,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
projectCache.set(cwd, project);
|
|
96
|
+
return project;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
100
|
+
const normalizedPath = path.resolve(filePath);
|
|
101
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
102
|
+
if (existingSourceFile) {
|
|
103
|
+
existingSourceFile.replaceWithText(text);
|
|
104
|
+
return existingSourceFile;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
108
|
+
if (sourceFile) {
|
|
109
|
+
sourceFile.replaceWithText(text);
|
|
110
|
+
return sourceFile;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getFilePath(context) {
|
|
117
|
+
const filePath = context.filename ?? context.getFilename();
|
|
118
|
+
if (!filePath || filePath === '<input>') {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getCwd(context) {
|
|
126
|
+
return context.cwd ?? process.cwd();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getNodeLoc(sourceCode, node) {
|
|
130
|
+
return {
|
|
131
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
132
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectHostNameIssues(sourceFile) {
|
|
137
|
+
const angularDecorators = collectAngularDecoratorNames(sourceFile);
|
|
138
|
+
if (
|
|
139
|
+
angularDecorators.componentNames.size === 0 &&
|
|
140
|
+
angularDecorators.directiveNames.size === 0 &&
|
|
141
|
+
angularDecorators.namespaceNames.size === 0
|
|
142
|
+
) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const issues = [];
|
|
147
|
+
|
|
148
|
+
for (const classDeclaration of sourceFile.getClasses()) {
|
|
149
|
+
const className = classDeclaration.getName();
|
|
150
|
+
if (!className) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const decoratorMatch = findAngularDecorator(
|
|
155
|
+
classDeclaration,
|
|
156
|
+
angularDecorators,
|
|
157
|
+
);
|
|
158
|
+
if (!decoratorMatch) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const metadata = getDecoratorMetadata(decoratorMatch.decorator);
|
|
163
|
+
if (!metadata) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const providersProperty = getObjectProperty(metadata, 'providers');
|
|
168
|
+
if (!providersProperty) {
|
|
169
|
+
issues.push({
|
|
170
|
+
kind: 'missing-providers',
|
|
171
|
+
decoratorKind: decoratorMatch.kind,
|
|
172
|
+
className,
|
|
173
|
+
metadata,
|
|
174
|
+
reportNode: classDeclaration,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const providersArray = getArrayInitializer(providersProperty);
|
|
180
|
+
if (!providersArray) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const hostNameCalls = findProvideHostNameCalls(providersArray);
|
|
185
|
+
if (hostNameCalls.length === 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
kind: 'missing-call',
|
|
188
|
+
decoratorKind: decoratorMatch.kind,
|
|
189
|
+
className,
|
|
190
|
+
providersArray,
|
|
191
|
+
reportNode: classDeclaration,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const expectedHostName = `${getHostNamePrefix(decoratorMatch.kind)}${className}`;
|
|
197
|
+
const matchingCall = hostNameCalls.find((callExpression) =>
|
|
198
|
+
isMatchingHostNameCall(callExpression, expectedHostName),
|
|
199
|
+
);
|
|
200
|
+
if (matchingCall) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
issues.push({
|
|
205
|
+
kind: 'mismatched-call',
|
|
206
|
+
decoratorKind: decoratorMatch.kind,
|
|
207
|
+
className,
|
|
208
|
+
callExpression: hostNameCalls[0],
|
|
209
|
+
reportNode: classDeclaration,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return issues;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function collectAngularDecoratorNames(sourceFile) {
|
|
217
|
+
const componentNames = new Set();
|
|
218
|
+
const directiveNames = new Set();
|
|
219
|
+
const namespaceNames = new Set();
|
|
220
|
+
|
|
221
|
+
for (const importDeclaration of sourceFile.getImportDeclarations()) {
|
|
222
|
+
if (importDeclaration.getModuleSpecifierValue() !== '@angular/core') {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const namedImport of importDeclaration.getNamedImports()) {
|
|
227
|
+
const importedName = namedImport.getName();
|
|
228
|
+
const localName = namedImport.getAliasNode()?.getText() ?? importedName;
|
|
229
|
+
|
|
230
|
+
if (importedName === 'Component') {
|
|
231
|
+
componentNames.add(localName);
|
|
232
|
+
}
|
|
233
|
+
if (importedName === 'Directive') {
|
|
234
|
+
directiveNames.add(localName);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const namespaceImport = importDeclaration.getNamespaceImport();
|
|
239
|
+
if (namespaceImport) {
|
|
240
|
+
namespaceNames.add(namespaceImport.getText());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
componentNames,
|
|
246
|
+
directiveNames,
|
|
247
|
+
namespaceNames,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findAngularDecorator(classDeclaration, angularDecorators) {
|
|
252
|
+
for (const decorator of classDeclaration.getDecorators()) {
|
|
253
|
+
const expression = decorator.getExpression();
|
|
254
|
+
if (!Node.isCallExpression(expression)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const callee = expression.getExpression();
|
|
259
|
+
if (Node.isIdentifier(callee)) {
|
|
260
|
+
const decoratorName = callee.getText();
|
|
261
|
+
if (angularDecorators.componentNames.has(decoratorName)) {
|
|
262
|
+
return { decorator, kind: 'component' };
|
|
263
|
+
}
|
|
264
|
+
if (angularDecorators.directiveNames.has(decoratorName)) {
|
|
265
|
+
return { decorator, kind: 'directive' };
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!Node.isPropertyAccessExpression(callee)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const objectExpression = callee.getExpression();
|
|
275
|
+
const propertyName = callee.getName();
|
|
276
|
+
if (
|
|
277
|
+
Node.isIdentifier(objectExpression) &&
|
|
278
|
+
angularDecorators.namespaceNames.has(objectExpression.getText())
|
|
279
|
+
) {
|
|
280
|
+
if (propertyName === 'Component') {
|
|
281
|
+
return { decorator, kind: 'component' };
|
|
282
|
+
}
|
|
283
|
+
if (propertyName === 'Directive') {
|
|
284
|
+
return { decorator, kind: 'directive' };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getHostNamePrefix(kind) {
|
|
293
|
+
return kind === 'directive' ? 'directive:' : 'component:';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getDecoratorMetadata(decorator) {
|
|
297
|
+
const expression = decorator.getExpression();
|
|
298
|
+
if (!Node.isCallExpression(expression)) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const [metadataArgument] = expression.getArguments();
|
|
303
|
+
if (!metadataArgument || !Node.isObjectLiteralExpression(metadataArgument)) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return metadataArgument;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getObjectProperty(objectLiteralExpression, name) {
|
|
311
|
+
const property = objectLiteralExpression
|
|
312
|
+
.getProperties()
|
|
313
|
+
.find(
|
|
314
|
+
(candidate) =>
|
|
315
|
+
Node.isPropertyAssignment(candidate) && candidate.getName() === name,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return Node.isPropertyAssignment(property) ? property : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getArrayInitializer(propertyAssignment) {
|
|
322
|
+
const initializer = propertyAssignment.getInitializer();
|
|
323
|
+
if (!initializer || !Node.isArrayLiteralExpression(initializer)) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return initializer;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function findProvideHostNameCalls(arrayLiteralExpression) {
|
|
331
|
+
return arrayLiteralExpression
|
|
332
|
+
.getElements()
|
|
333
|
+
.filter((element) => Node.isCallExpression(element))
|
|
334
|
+
.filter((callExpression) => isProvideHostNameCall(callExpression));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isProvideHostNameCall(callExpression) {
|
|
338
|
+
const expression = callExpression.getExpression();
|
|
339
|
+
if (Node.isIdentifier(expression)) {
|
|
340
|
+
return expression.getText() === 'provideHostName';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
344
|
+
return expression.getName() === 'provideHostName';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isMatchingHostNameCall(callExpression, expectedHostName) {
|
|
351
|
+
const [nameArgument] = callExpression.getArguments();
|
|
352
|
+
if (
|
|
353
|
+
!nameArgument ||
|
|
354
|
+
(!Node.isStringLiteral(nameArgument) &&
|
|
355
|
+
!Node.isNoSubstitutionTemplateLiteral(nameArgument))
|
|
356
|
+
) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return nameArgument.getLiteralText() === expectedHostName;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function applyIssueFix(issue) {
|
|
364
|
+
const expectedHostName = `${getHostNamePrefix(issue.decoratorKind)}${issue.className}`;
|
|
365
|
+
const expectedCall = `provideHostName('${expectedHostName}')`;
|
|
366
|
+
|
|
367
|
+
if (issue.kind === 'missing-providers') {
|
|
368
|
+
issue.metadata.addPropertyAssignment({
|
|
369
|
+
name: 'providers',
|
|
370
|
+
initializer: `[${expectedCall}]`,
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (issue.kind === 'missing-call') {
|
|
376
|
+
issue.providersArray.addElement(expectedCall);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (issue.kind === 'mismatched-call') {
|
|
381
|
+
issue.callExpression.replaceWithText(expectedCall);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ensureProvideHostNameImport(sourceFile) {
|
|
386
|
+
const coreImports = sourceFile
|
|
387
|
+
.getImportDeclarations()
|
|
388
|
+
.filter(
|
|
389
|
+
(importDeclaration) =>
|
|
390
|
+
importDeclaration.getModuleSpecifierValue() === '@craft-ng/core',
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
for (const importDeclaration of coreImports) {
|
|
394
|
+
const namedImports = importDeclaration.getNamedImports();
|
|
395
|
+
const directImport = namedImports.find(
|
|
396
|
+
(namedImport) =>
|
|
397
|
+
namedImport.getName() === 'provideHostName' &&
|
|
398
|
+
!namedImport.getAliasNode(),
|
|
399
|
+
);
|
|
400
|
+
if (directImport) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const importDeclaration of coreImports) {
|
|
406
|
+
for (const namedImport of importDeclaration.getNamedImports()) {
|
|
407
|
+
if (
|
|
408
|
+
namedImport.getName() === 'provideHostName' &&
|
|
409
|
+
namedImport.getAliasNode()
|
|
410
|
+
) {
|
|
411
|
+
namedImport.remove();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const targetImport = coreImports.find(
|
|
417
|
+
(importDeclaration) => !importDeclaration.getNamespaceImport(),
|
|
418
|
+
);
|
|
419
|
+
if (targetImport) {
|
|
420
|
+
const hasNamedImport = targetImport
|
|
421
|
+
.getNamedImports()
|
|
422
|
+
.some((namedImport) => namedImport.getName() === 'provideHostName');
|
|
423
|
+
if (!hasNamedImport) {
|
|
424
|
+
targetImport.addNamedImport('provideHostName');
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const insertIndex = sourceFile
|
|
430
|
+
.getStatements()
|
|
431
|
+
.findIndex(
|
|
432
|
+
(statement) => statement.getKind() !== SyntaxKind.ImportDeclaration,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
sourceFile.insertImportDeclaration(
|
|
436
|
+
insertIndex < 0 ? sourceFile.getImportDeclarations().length : insertIndex,
|
|
437
|
+
{
|
|
438
|
+
moduleSpecifier: '@craft-ng/core',
|
|
439
|
+
namedImports: ['provideHostName'],
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatIssueMessage(issues) {
|
|
445
|
+
if (issues.length === 1) {
|
|
446
|
+
const issue = issues[0];
|
|
447
|
+
const expectedHostName = `${getHostNamePrefix(issue.decoratorKind)}${issue.className}`;
|
|
448
|
+
return `${issue.className} must include provideHostName('${expectedHostName}') in providers.`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const classNames = [...new Set(issues.map((issue) => issue.className))].join(
|
|
452
|
+
', ',
|
|
453
|
+
);
|
|
454
|
+
return `Classes must include provideHostName('<kind>:<ClassName>') in providers: ${classNames}.`;
|
|
455
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const {
|
|
4
|
+
IndentationText,
|
|
5
|
+
Project,
|
|
6
|
+
QuoteKind,
|
|
7
|
+
Scope,
|
|
8
|
+
SyntaxKind,
|
|
9
|
+
} = require('ts-morph');
|
|
10
|
+
|
|
11
|
+
const projectCache = new Map();
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
"Ensure Angular @Component and @Directive classes declare 'private readonly _monitoring = componentMonitoring()'.",
|
|
19
|
+
},
|
|
20
|
+
fixable: 'code',
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
missingMonitoring:
|
|
24
|
+
"Component/Directive '{{className}}' must declare 'private readonly _monitoring = componentMonitoring()'.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
'Program:exit'() {
|
|
30
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
31
|
+
const filePath = getFilePath(context);
|
|
32
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const text = sourceCode.getText();
|
|
37
|
+
if (!text.includes('@Component') && !text.includes('@Directive')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sourceFile = getProjectSourceFile(
|
|
42
|
+
getProject(getCwd(context)),
|
|
43
|
+
filePath,
|
|
44
|
+
text,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const issues = collectMonitoringIssues(sourceFile);
|
|
48
|
+
if (issues.length === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const reportLoc = getNodeLoc(sourceCode, issues[0].classDeclaration);
|
|
53
|
+
|
|
54
|
+
for (const issue of issues) {
|
|
55
|
+
applyMonitoringFix(issue);
|
|
56
|
+
}
|
|
57
|
+
ensureComponentMonitoringImport(sourceFile);
|
|
58
|
+
|
|
59
|
+
const fixedText = sourceFile.getFullText();
|
|
60
|
+
if (fixedText === text) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const classNames = issues.map((i) => i.className).join(', ');
|
|
65
|
+
|
|
66
|
+
context.report({
|
|
67
|
+
loc: reportLoc,
|
|
68
|
+
message: `Component(s) missing componentMonitoring(): ${classNames}`,
|
|
69
|
+
fix(fixer) {
|
|
70
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function getProject(cwd) {
|
|
79
|
+
let project = projectCache.get(cwd);
|
|
80
|
+
if (project) {
|
|
81
|
+
return project;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const manipulationSettings = {
|
|
85
|
+
indentationText: IndentationText.TwoSpaces,
|
|
86
|
+
quoteKind: QuoteKind.Single,
|
|
87
|
+
};
|
|
88
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
89
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
90
|
+
? new Project({ tsConfigFilePath, manipulationSettings })
|
|
91
|
+
: new Project({
|
|
92
|
+
compilerOptions: { experimentalDecorators: true, target: 9 },
|
|
93
|
+
manipulationSettings,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
projectCache.set(cwd, project);
|
|
97
|
+
return project;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
101
|
+
const normalizedPath = path.resolve(filePath);
|
|
102
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
103
|
+
if (existingSourceFile) {
|
|
104
|
+
existingSourceFile.replaceWithText(text);
|
|
105
|
+
return existingSourceFile;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
109
|
+
if (sourceFile) {
|
|
110
|
+
sourceFile.replaceWithText(text);
|
|
111
|
+
return sourceFile;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getFilePath(context) {
|
|
118
|
+
const filePath = context.filename ?? context.getFilename();
|
|
119
|
+
if (!filePath || filePath === '<input>') {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getCwd(context) {
|
|
126
|
+
return context.cwd ?? process.cwd();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getNodeLoc(sourceCode, node) {
|
|
130
|
+
return {
|
|
131
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
132
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isAngularComponentOrDirective(classDeclaration) {
|
|
137
|
+
for (const decorator of classDeclaration.getDecorators()) {
|
|
138
|
+
const name = decorator.getName();
|
|
139
|
+
if (name === 'Component' || name === 'Directive') {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hasMonitoringProperty(classDeclaration) {
|
|
147
|
+
for (const prop of classDeclaration.getProperties()) {
|
|
148
|
+
if (
|
|
149
|
+
prop.getName() === '_monitoring' &&
|
|
150
|
+
prop.hasModifier(SyntaxKind.PrivateKeyword) &&
|
|
151
|
+
prop.isReadonly()
|
|
152
|
+
) {
|
|
153
|
+
const initializer = prop.getInitializer();
|
|
154
|
+
if (
|
|
155
|
+
initializer &&
|
|
156
|
+
initializer.getKindName() === 'CallExpression' &&
|
|
157
|
+
initializer.getText().startsWith('componentMonitoring(')
|
|
158
|
+
) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectMonitoringIssues(sourceFile) {
|
|
167
|
+
const issues = [];
|
|
168
|
+
|
|
169
|
+
for (const classDeclaration of sourceFile.getClasses()) {
|
|
170
|
+
const className = classDeclaration.getName();
|
|
171
|
+
if (!className) continue;
|
|
172
|
+
if (!isAngularComponentOrDirective(classDeclaration)) continue;
|
|
173
|
+
if (hasMonitoringProperty(classDeclaration)) continue;
|
|
174
|
+
|
|
175
|
+
issues.push({ className, classDeclaration });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return issues;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyMonitoringFix(issue) {
|
|
182
|
+
const { classDeclaration } = issue;
|
|
183
|
+
|
|
184
|
+
// Insert as first property of the class
|
|
185
|
+
classDeclaration.insertProperty(0, {
|
|
186
|
+
name: '_monitoring',
|
|
187
|
+
scope: Scope.Private,
|
|
188
|
+
isReadonly: true,
|
|
189
|
+
initializer: 'componentMonitoring()',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ensureComponentMonitoringImport(sourceFile) {
|
|
194
|
+
const craftNgCoreImport = sourceFile
|
|
195
|
+
.getImportDeclarations()
|
|
196
|
+
.find((imp) => imp.getModuleSpecifierValue() === '@craft-ng/core');
|
|
197
|
+
|
|
198
|
+
if (craftNgCoreImport) {
|
|
199
|
+
const namedImports = craftNgCoreImport.getNamedImports();
|
|
200
|
+
const alreadyImported = namedImports.some(
|
|
201
|
+
(ni) => ni.getName() === 'componentMonitoring',
|
|
202
|
+
);
|
|
203
|
+
if (!alreadyImported) {
|
|
204
|
+
craftNgCoreImport.addNamedImport('componentMonitoring');
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
sourceFile.addImportDeclaration({
|
|
208
|
+
moduleSpecifier: '@craft-ng/core',
|
|
209
|
+
namedImports: ['componentMonitoring'],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|