@silvestv/migration-planificator 6.0.2 → 6.0.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.
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.HtmlMatcher = exports.SymbolMatcher = exports.FileMatcher = exports.CollectionMatcher = exports.TypeMatcher = exports.ExpressionMatcher = exports.HierarchyMatcher = exports.DecoratorMatcher = exports.ImportMatcher = exports.ContextMatcher = exports.NodeMatcher = exports.clearTemplateCache = void 0;
|
|
37
|
+
exports.setOverrideNode = setOverrideNode;
|
|
38
|
+
exports.getOverrideNode = getOverrideNode;
|
|
39
|
+
exports.clearOverrideNode = clearOverrideNode;
|
|
37
40
|
exports.matchesAstPattern = matchesAstPattern;
|
|
38
41
|
const ts_morph_1 = require("ts-morph");
|
|
39
42
|
// Import matchers TypeScript (depuis ts/)
|
|
@@ -63,6 +66,24 @@ exports.HtmlMatcher = HtmlMatcher;
|
|
|
63
66
|
const html_pipe_variable_matcher_1 = require("./html/html-pipe-variable-matcher");
|
|
64
67
|
const host_binding_property_matcher_1 = require("./ts/host-binding-property-matcher");
|
|
65
68
|
const mutation_matcher_1 = require("./ts/mutation-matcher");
|
|
69
|
+
/**
|
|
70
|
+
* Mécanisme "Override Node" pour localisation précise
|
|
71
|
+
* Permet aux matchers de retourner un nœud différent pour l'affichage
|
|
72
|
+
* Exemple: hostBindingProperty retourne le PropertyAssignment au lieu du @Component
|
|
73
|
+
*/
|
|
74
|
+
let currentOverrideNode = null;
|
|
75
|
+
/** Définit le nœud à utiliser pour la localisation (ligne, texte) */
|
|
76
|
+
function setOverrideNode(node) {
|
|
77
|
+
currentOverrideNode = node;
|
|
78
|
+
}
|
|
79
|
+
/** Récupère le nœud override ou null si non défini */
|
|
80
|
+
function getOverrideNode() {
|
|
81
|
+
return currentOverrideNode;
|
|
82
|
+
}
|
|
83
|
+
/** Nettoie le nœud override (à appeler après utilisation) */
|
|
84
|
+
function clearOverrideNode() {
|
|
85
|
+
currentOverrideNode = null;
|
|
86
|
+
}
|
|
66
87
|
/**
|
|
67
88
|
* Fonction principale : vérifie si un nœud correspond à un pattern AST
|
|
68
89
|
* OPTIMISÉ : excludeContext en premier (88% des règles l'utilisent)
|
|
@@ -393,10 +414,13 @@ function matchesAstPattern(node, pattern) {
|
|
|
393
414
|
}
|
|
394
415
|
}
|
|
395
416
|
// Vérifier hostBindingProperty (résolution host binding → TS property → vérification wrappers)
|
|
417
|
+
// Note: matchesHostBindingProperty retourne Node | null pour localisation précise
|
|
396
418
|
if (pattern.hostBindingProperty !== undefined) {
|
|
397
|
-
|
|
419
|
+
const problematicNode = (0, host_binding_property_matcher_1.matchesHostBindingProperty)(node, pattern.hostBindingProperty);
|
|
420
|
+
if (!problematicNode) {
|
|
398
421
|
return false;
|
|
399
422
|
}
|
|
423
|
+
setOverrideNode(problematicNode);
|
|
400
424
|
}
|
|
401
425
|
// Vérifier fileMissing (identifiant absent de tout le fichier)
|
|
402
426
|
if (pattern.fileMissing !== undefined) {
|
|
@@ -38,7 +38,7 @@ function getHostObject(componentNode) {
|
|
|
38
38
|
/**
|
|
39
39
|
* Trouve les property bindings dans l'objet host
|
|
40
40
|
* Exemples: '[class.active]', '[attr.disabled]', '[style.color]'
|
|
41
|
-
* Retourne array de {key, value}
|
|
41
|
+
* Retourne array de {key, value, node} pour localisation précise
|
|
42
42
|
*/
|
|
43
43
|
function findPropertyBindings(hostObject) {
|
|
44
44
|
const bindings = [];
|
|
@@ -60,7 +60,7 @@ function findPropertyBindings(hostObject) {
|
|
|
60
60
|
const value = ts_morph_1.Node.isStringLiteral(initializer)
|
|
61
61
|
? initializer.getLiteralValue()
|
|
62
62
|
: initializer.getText();
|
|
63
|
-
bindings.push({ key, value });
|
|
63
|
+
bindings.push({ key, value, node: property });
|
|
64
64
|
}
|
|
65
65
|
return bindings;
|
|
66
66
|
}
|
|
@@ -79,6 +79,156 @@ function isWrappedInWrappers(propertyDecl, wrappers) {
|
|
|
79
79
|
}
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
|
+
/** Opérateurs d'assignation (réutilisé de mutation-matcher.ts) */
|
|
83
|
+
const ASSIGNMENT_OPERATORS = new Set([
|
|
84
|
+
ts_morph_1.SyntaxKind.EqualsToken,
|
|
85
|
+
ts_morph_1.SyntaxKind.PlusEqualsToken,
|
|
86
|
+
ts_morph_1.SyntaxKind.MinusEqualsToken,
|
|
87
|
+
ts_morph_1.SyntaxKind.AsteriskEqualsToken,
|
|
88
|
+
ts_morph_1.SyntaxKind.SlashEqualsToken
|
|
89
|
+
]);
|
|
90
|
+
/**
|
|
91
|
+
* Vérifie si un BinaryExpression est une assignation à this.propertyName
|
|
92
|
+
*/
|
|
93
|
+
function isAssignmentToThisProperty(expr, propertyName) {
|
|
94
|
+
if (!ts_morph_1.Node.isBinaryExpression(expr))
|
|
95
|
+
return false;
|
|
96
|
+
const operatorKind = expr.getOperatorToken().getKind();
|
|
97
|
+
if (!ASSIGNMENT_OPERATORS.has(operatorKind))
|
|
98
|
+
return false;
|
|
99
|
+
const left = expr.getLeft();
|
|
100
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(left))
|
|
101
|
+
return false;
|
|
102
|
+
const expression = left.getExpression();
|
|
103
|
+
const name = left.getName();
|
|
104
|
+
return expression.getText() === 'this' && name === propertyName;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Trouve la MethodDeclaration parente d'un nœud
|
|
108
|
+
*/
|
|
109
|
+
function findParentMethod(node) {
|
|
110
|
+
let current = node.getParent();
|
|
111
|
+
while (current) {
|
|
112
|
+
if (ts_morph_1.Node.isMethodDeclaration(current)) {
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
if (ts_morph_1.Node.isClassDeclaration(current)) {
|
|
116
|
+
return null; // Arrêter si on atteint la classe (initializer de propriété)
|
|
117
|
+
}
|
|
118
|
+
current = current.getParent();
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Trouve toutes les mutations d'une propriété dans une classe
|
|
124
|
+
* Détecte : this.prop = value, this.prop += value, etc.
|
|
125
|
+
*/
|
|
126
|
+
function findPropertyMutationsInClass(classDecl, propertyName) {
|
|
127
|
+
const mutations = [];
|
|
128
|
+
classDecl.forEachDescendant((descendant) => {
|
|
129
|
+
if (isAssignmentToThisProperty(descendant, propertyName)) {
|
|
130
|
+
mutations.push({
|
|
131
|
+
mutationNode: descendant,
|
|
132
|
+
containingMethod: findParentMethod(descendant)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return mutations;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Vérifie si un nœud contient un appel à markForCheck()
|
|
140
|
+
*/
|
|
141
|
+
function containsMarkForCheckCall(node) {
|
|
142
|
+
let found = false;
|
|
143
|
+
node.forEachDescendant((child) => {
|
|
144
|
+
if (found)
|
|
145
|
+
return;
|
|
146
|
+
if (!ts_morph_1.Node.isCallExpression(child))
|
|
147
|
+
return;
|
|
148
|
+
const expr = child.getExpression();
|
|
149
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(expr))
|
|
150
|
+
return;
|
|
151
|
+
if (expr.getName() === 'markForCheck') {
|
|
152
|
+
found = true;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Trouve la position du premier appel markForCheck() dans un nœud
|
|
159
|
+
* Retourne -1 si non trouvé
|
|
160
|
+
*/
|
|
161
|
+
function findMarkForCheckPosition(node) {
|
|
162
|
+
let position = -1;
|
|
163
|
+
node.forEachDescendant((child) => {
|
|
164
|
+
if (position !== -1)
|
|
165
|
+
return;
|
|
166
|
+
if (!ts_morph_1.Node.isCallExpression(child))
|
|
167
|
+
return;
|
|
168
|
+
const expr = child.getExpression();
|
|
169
|
+
const isMarkForCheck = ts_morph_1.Node.isPropertyAccessExpression(expr) && expr.getName() === 'markForCheck';
|
|
170
|
+
if (isMarkForCheck) {
|
|
171
|
+
position = child.getStart();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return position;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Vérifie si markForCheck() est appelé APRÈS une mutation dans la même méthode
|
|
178
|
+
*/
|
|
179
|
+
function hasMarkForCheckAfterMutation(method, mutationNode) {
|
|
180
|
+
const body = method.getBody();
|
|
181
|
+
if (!body || !ts_morph_1.Node.isBlock(body))
|
|
182
|
+
return false;
|
|
183
|
+
const statements = body.getStatements();
|
|
184
|
+
const mutationPos = mutationNode.getStart();
|
|
185
|
+
// Trouver l'index du statement contenant la mutation
|
|
186
|
+
let mutationIndex = -1;
|
|
187
|
+
for (let i = 0; i < statements.length; i++) {
|
|
188
|
+
const stmtStart = statements[i].getStart();
|
|
189
|
+
const stmtEnd = statements[i].getEnd();
|
|
190
|
+
if (mutationPos >= stmtStart && mutationPos <= stmtEnd) {
|
|
191
|
+
mutationIndex = i;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (mutationIndex === -1)
|
|
196
|
+
return false;
|
|
197
|
+
// Chercher markForCheck() APRÈS la mutation (même statement ou suivants)
|
|
198
|
+
for (let i = mutationIndex; i < statements.length; i++) {
|
|
199
|
+
if (containsMarkForCheckCall(statements[i])) {
|
|
200
|
+
if (i === mutationIndex) {
|
|
201
|
+
// Même statement : vérifier que markForCheck est APRÈS mutation
|
|
202
|
+
const markForCheckPos = findMarkForCheckPosition(statements[i]);
|
|
203
|
+
if (markForCheckPos > mutationPos)
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
return true; // Statement suivant
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Vérifie si TOUTES les mutations d'une propriété ont markForCheck() appelé après
|
|
215
|
+
* Retourne true si :
|
|
216
|
+
* - La propriété n'est jamais mutée (seulement initialisée)
|
|
217
|
+
* - Toutes les mutations sont suivies de markForCheck()
|
|
218
|
+
*/
|
|
219
|
+
function allMutationsHaveMarkForCheck(classDecl, propertyName) {
|
|
220
|
+
const mutations = findPropertyMutationsInClass(classDecl, propertyName);
|
|
221
|
+
// Si aucune mutation → propriété jamais changée → PAS problématique
|
|
222
|
+
if (mutations.length === 0) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
// Vérifier que TOUTES les mutations ont markForCheck() après
|
|
226
|
+
return mutations.every(({ mutationNode, containingMethod }) => {
|
|
227
|
+
if (!containingMethod)
|
|
228
|
+
return false; // Mutation hors méthode = non protégée
|
|
229
|
+
return hasMarkForCheckAfterMutation(containingMethod, mutationNode);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
82
232
|
/**
|
|
83
233
|
* Vérifie si un getter utilise uniquement des propriétés signal/computed dans son body
|
|
84
234
|
* Retourne true si le getter est "safe" (ne nécessite pas markForCheck)
|
|
@@ -188,6 +338,31 @@ function findHostBindingDecorators(componentNode) {
|
|
|
188
338
|
extractHostBindings(parent.getGetAccessors());
|
|
189
339
|
return results;
|
|
190
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Vérifie si un membre résolu (PropertyDeclaration ou GetAccessor) est problématique
|
|
343
|
+
* Factorise la logique commune entre isProblematicBinding() et matchesHostBindingProperty()
|
|
344
|
+
* @returns true si problématique, false sinon
|
|
345
|
+
*/
|
|
346
|
+
function isResolvedMemberProblematic(resolved, propertyName, classDecl, notWrappedIn) {
|
|
347
|
+
// Si c'est un getter → Vérifier si son body utilise uniquement des signals
|
|
348
|
+
if (ts_morph_1.Node.isGetAccessorDeclaration(resolved)) {
|
|
349
|
+
if (classDecl && isGetterUsingOnlySignals(resolved, classDecl, notWrappedIn)) {
|
|
350
|
+
return false; // Getter safe (utilise uniquement des signals)
|
|
351
|
+
}
|
|
352
|
+
return true; // Getter problématique
|
|
353
|
+
}
|
|
354
|
+
// Si PropertyDeclaration, vérifier wrapper
|
|
355
|
+
if (ts_morph_1.Node.isPropertyDeclaration(resolved)) {
|
|
356
|
+
if (!isWrappedInWrappers(resolved, notWrappedIn)) {
|
|
357
|
+
// Propriété simple - vérifier si markForCheck() est appelé après chaque mutation
|
|
358
|
+
if (classDecl && allMutationsHaveMarkForCheck(classDecl, propertyName)) {
|
|
359
|
+
return false; // Toutes mutations protégées par markForCheck() → PAS problématique
|
|
360
|
+
}
|
|
361
|
+
return true; // Propriété simple sans markForCheck, PROBLÉMATIQUE
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
191
366
|
/**
|
|
192
367
|
* Vérifie si un binding est problématique (nécessite markForCheck)
|
|
193
368
|
* Vérifie TOUTES les variables dans l'expression
|
|
@@ -211,18 +386,8 @@ function isProblematicBinding(componentNode, bindingValue, notWrappedIn) {
|
|
|
211
386
|
if (!resolved) {
|
|
212
387
|
continue; // Variable non trouvée, skip
|
|
213
388
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (classDecl && isGetterUsingOnlySignals(resolved, classDecl, notWrappedIn)) {
|
|
217
|
-
continue; // Getter safe (utilise uniquement des signals)
|
|
218
|
-
}
|
|
219
|
-
return true; // Getter problématique
|
|
220
|
-
}
|
|
221
|
-
// Si PropertyDeclaration, vérifier wrapper
|
|
222
|
-
if (ts_morph_1.Node.isPropertyDeclaration(resolved)) {
|
|
223
|
-
if (!isWrappedInWrappers(resolved, notWrappedIn)) {
|
|
224
|
-
return true; // Propriété simple, PROBLÉMATIQUE
|
|
225
|
-
}
|
|
389
|
+
if (isResolvedMemberProblematic(resolved, variableName, classDecl, notWrappedIn)) {
|
|
390
|
+
return true;
|
|
226
391
|
}
|
|
227
392
|
}
|
|
228
393
|
return false;
|
|
@@ -234,19 +399,21 @@ function isProblematicBinding(componentNode, bindingValue, notWrappedIn) {
|
|
|
234
399
|
* Détecte 2 sources de host bindings:
|
|
235
400
|
* 1. Objet host dans @Component: host: { '[class.active]': 'isActive' }
|
|
236
401
|
* 2. @HostBinding() decorators sur properties/getters
|
|
402
|
+
*
|
|
403
|
+
* @returns Le nœud problématique (PropertyAssignment ou Decorator @HostBinding) ou null si pas de match
|
|
237
404
|
*/
|
|
238
405
|
function matchesHostBindingProperty(componentNode, pattern) {
|
|
239
406
|
if (!ts_morph_1.Node.isDecorator(componentNode)) {
|
|
240
|
-
return
|
|
407
|
+
return null;
|
|
241
408
|
}
|
|
242
409
|
const notWrappedIn = pattern.resolvedProperty.notWrappedIn || [];
|
|
243
410
|
// Source 1: Objet host dans @Component
|
|
244
411
|
const hostObject = getHostObject(componentNode);
|
|
245
412
|
if (hostObject) {
|
|
246
413
|
const bindings = findPropertyBindings(hostObject);
|
|
247
|
-
for (const { value } of bindings) {
|
|
414
|
+
for (const { value, node } of bindings) {
|
|
248
415
|
if (isProblematicBinding(componentNode, value, notWrappedIn)) {
|
|
249
|
-
return
|
|
416
|
+
return node; // Retourner le PropertyAssignment problématique
|
|
250
417
|
}
|
|
251
418
|
}
|
|
252
419
|
}
|
|
@@ -254,26 +421,17 @@ function matchesHostBindingProperty(componentNode, pattern) {
|
|
|
254
421
|
const parent = componentNode.getParent();
|
|
255
422
|
const classDecl = parent && ts_morph_1.Node.isClassDeclaration(parent) ? parent : null;
|
|
256
423
|
const hostBindingDecorators = findHostBindingDecorators(componentNode);
|
|
257
|
-
for (const { propertyName } of hostBindingDecorators) {
|
|
424
|
+
for (const { decorator, propertyName } of hostBindingDecorators) {
|
|
258
425
|
const resolved = resolveBindingVariable(componentNode, propertyName);
|
|
259
426
|
if (!resolved) {
|
|
260
427
|
continue;
|
|
261
428
|
}
|
|
262
|
-
//
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
continue; // Getter safe (utilise uniquement des signals)
|
|
266
|
-
}
|
|
267
|
-
return true; // Getter problématique
|
|
268
|
-
}
|
|
269
|
-
// Si PropertyDeclaration, vérifier wrapper
|
|
270
|
-
if (ts_morph_1.Node.isPropertyDeclaration(resolved)) {
|
|
271
|
-
if (!isWrappedInWrappers(resolved, notWrappedIn)) {
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
429
|
+
// Utiliser la fonction factorisée (DRY)
|
|
430
|
+
if (isResolvedMemberProblematic(resolved, propertyName, classDecl, notWrappedIn)) {
|
|
431
|
+
return decorator; // Retourner le @HostBinding decorator problématique
|
|
274
432
|
}
|
|
275
433
|
}
|
|
276
|
-
return
|
|
434
|
+
return null;
|
|
277
435
|
}
|
|
278
436
|
/**
|
|
279
437
|
* Vérifie si un composant a des @HostListener sur ses méthodes
|
|
@@ -56,13 +56,19 @@ function stripQuotes(str) {
|
|
|
56
56
|
* 'isActive' → ['isActive']
|
|
57
57
|
* 'count > 0' → ['count']
|
|
58
58
|
* 'count1 + count2' → ['count1', 'count2']
|
|
59
|
-
* 'sig() ? "a" : "b"' → ['sig']
|
|
59
|
+
* 'sig() ? "a" : "b"' → ['sig'] (strings littérales ignorées)
|
|
60
60
|
* 'obj.prop' → ['obj']
|
|
61
|
+
* '"search"' → [] (string littérale pure = pas de variable)
|
|
61
62
|
* @param expression Expression de binding
|
|
62
63
|
* @returns Array de noms de variables
|
|
63
64
|
*/
|
|
64
65
|
function extractVariableNames(expression) {
|
|
65
|
-
|
|
66
|
+
// Supprimer les strings littérales pour éviter les faux positifs
|
|
67
|
+
// '"search"' → '', 'sig() ? "a" : "b"' → 'sig() ? : '
|
|
68
|
+
const withoutStrings = expression
|
|
69
|
+
.replace(/"[^"]*"/g, '') // Supprimer strings double quotes
|
|
70
|
+
.replace(/'[^']*'/g, ''); // Supprimer strings single quotes
|
|
71
|
+
const cleaned = withoutStrings.trim();
|
|
66
72
|
const variables = new Set();
|
|
67
73
|
// Pattern pour extraire tous les identifiants valides
|
|
68
74
|
const identifierPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
|
|
@@ -131,18 +131,22 @@ function processNodeWithRules(node, rules, sourceFile, relativeFilePath, matches
|
|
|
131
131
|
// Vérifier testSetup si présent
|
|
132
132
|
if (rule.astPattern.testSetup &&
|
|
133
133
|
!matchers_1.FileMatcher.matchesTestSetup(sourceFile, rule.astPattern.testSetup)) {
|
|
134
|
+
(0, matchers_1.clearOverrideNode)(); // Nettoyer même si on skip
|
|
134
135
|
continue;
|
|
135
136
|
}
|
|
137
|
+
// Utiliser le nœud override si disponible (localisation précise)
|
|
138
|
+
const reportNode = (0, matchers_1.getOverrideNode)() || node;
|
|
139
|
+
(0, matchers_1.clearOverrideNode)(); // Toujours nettoyer après utilisation
|
|
136
140
|
// Optimisation getText() : vérifier width d'abord
|
|
137
|
-
const width =
|
|
141
|
+
const width = reportNode.getWidth();
|
|
138
142
|
const matchedText = width > 100
|
|
139
|
-
?
|
|
140
|
-
:
|
|
143
|
+
? reportNode.getText().substring(0, 100).trim()
|
|
144
|
+
: reportNode.getText().trim();
|
|
141
145
|
matches.push({
|
|
142
146
|
ruleKey: rule.key,
|
|
143
147
|
ruleSummary: rule.summary,
|
|
144
148
|
filePath: relativeFilePath,
|
|
145
|
-
lineNumber:
|
|
149
|
+
lineNumber: reportNode.getStartLineNumber(),
|
|
146
150
|
matchedText
|
|
147
151
|
});
|
|
148
152
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silvestv/migration-planificator",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.3",
|
|
4
4
|
"description": "Professional Angular migration analysis tool with AST precision for version upgrades (17→18, 18→19, 19→20, 20→21), Nx monorepo refactoring, workload estimation, and technical debt assessment. Interactive HTML reports with Gantt timeline and real-time editing.",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"publishConfig": {
|