@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
- if (!(0, host_binding_property_matcher_1.matchesHostBindingProperty)(node, pattern.hostBindingProperty)) {
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
- // Si c'est un getter → Vérifier si son body utilise uniquement des signals
215
- if (ts_morph_1.Node.isGetAccessorDeclaration(resolved)) {
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 false;
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 true;
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
- // Si c'est un getter → Vérifier si son body utilise uniquement des signals
263
- if (ts_morph_1.Node.isGetAccessorDeclaration(resolved)) {
264
- if (classDecl && isGetterUsingOnlySignals(resolved, classDecl, notWrappedIn)) {
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 false;
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
- const cleaned = expression.trim();
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 = node.getWidth();
141
+ const width = reportNode.getWidth();
138
142
  const matchedText = width > 100
139
- ? node.getText().substring(0, 100).trim()
140
- : node.getText().trim();
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: node.getStartLineNumber(),
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.2",
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": {