plugin-eco 0.1.0

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.
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXCLUDED_DIRS = exports.LANGUAGES = exports.RULE_CONTEXTS = void 0;
4
+ exports.ruleAppliesIn = ruleAppliesIn;
5
+ exports.specFor = specFor;
6
+ exports.isSupported = isSupported;
7
+ exports.grammarFiles = grammarFiles;
8
+ exports.excludeGlob = excludeGlob;
9
+ exports.globFor = globFor;
10
+ exports.specForPath = specForPath;
11
+ /**
12
+ * Contexte d'exécution dans lequel chaque règle a un sens.
13
+ *
14
+ * `'any'` signifie « partout, y compris quand le contexte est indéterminé ».
15
+ * Une règle restreinte ne se déclenche **pas** sur un fichier `unknown` : sans
16
+ * certitude, mieux vaut se taire que produire un faux positif.
17
+ */
18
+ exports.RULE_CONTEXTS = {
19
+ 'nested-loops': 'any',
20
+ 'string-concat-in-loop': 'any',
21
+ 'object-creation-in-loop': 'any',
22
+ 'regex-compile-in-loop': 'any',
23
+ 'blocking-io-in-loop': 'any',
24
+ 'sql-without-limit': 'any',
25
+ // Enchaîner des attentes coûte des allers-retours des deux côtés.
26
+ 'await-in-loop': 'any',
27
+ // Les API synchrones de Node n'existent que sur un serveur.
28
+ 'sync-io-in-function': ['server'],
29
+ // Un timer permanent est payé par chaque appareil qui affiche la page.
30
+ 'polling-interval': ['client'],
31
+ 'unthrottled-event-listener': ['client'],
32
+ // Le poids du bundle est transféré sur le réseau vers chaque visiteur.
33
+ 'whole-library-import': ['client'],
34
+ };
35
+ /** La règle s'applique-t-elle à un fichier dont le contexte est celui-ci ? */
36
+ function ruleAppliesIn(rule, context) {
37
+ const allowed = exports.RULE_CONTEXTS[rule];
38
+ return allowed === 'any' || allowed.includes(context);
39
+ }
40
+ // --- Noms de nœuds --------------------------------------------------------
41
+ const JAVA_NODES = {
42
+ loops: ['for_statement', 'enhanced_for_statement', 'while_statement', 'do_statement'],
43
+ objectCreation: ['object_creation_expression'],
44
+ call: ['method_invocation'],
45
+ stringLiteral: ['string_literal'],
46
+ compoundAssignment: ['assignment_expression'],
47
+ functions: ['method_declaration', 'constructor_declaration', 'lambda_expression'],
48
+ };
49
+ // Les grammaires typescript et tsx étendent toutes deux javascript :
50
+ // les noms de nœuds sont identiques pour les trois.
51
+ const JS_NODES = {
52
+ loops: ['for_statement', 'for_in_statement', 'while_statement', 'do_statement'],
53
+ objectCreation: ['new_expression'],
54
+ call: ['call_expression'],
55
+ stringLiteral: ['string', 'template_string'],
56
+ compoundAssignment: ['augmented_assignment_expression'],
57
+ functions: [
58
+ 'function_declaration', 'function_expression', 'arrow_function',
59
+ 'method_definition', 'generator_function_declaration',
60
+ ],
61
+ };
62
+ // --- Règles ---------------------------------------------------------------
63
+ const JAVA_RULES = [
64
+ 'nested-loops',
65
+ 'string-concat-in-loop',
66
+ 'object-creation-in-loop',
67
+ 'regex-compile-in-loop',
68
+ 'blocking-io-in-loop',
69
+ 'sql-without-limit',
70
+ ];
71
+ /**
72
+ * JS/TS n'hérite que des règles réellement pertinentes.
73
+ *
74
+ * `string-concat-in-loop` et `object-creation-in-loop` sont volontairement
75
+ * absentes : V8 représente les concaténations par des ropes et son GC
76
+ * générationnel rend l'allocation à courte durée de vie bon marché. Les porter
77
+ * produirait du bruit sur du code sain.
78
+ *
79
+ * S'y ajoutent les règles propres au web, dont plusieurs ne valent que d'un
80
+ * côté : voir RULE_CONTEXTS.
81
+ */
82
+ const JS_RULES = [
83
+ 'nested-loops',
84
+ 'sql-without-limit',
85
+ 'regex-compile-in-loop',
86
+ 'await-in-loop',
87
+ 'sync-io-in-function',
88
+ 'polling-interval',
89
+ 'unthrottled-event-listener',
90
+ 'whole-library-import',
91
+ ];
92
+ // --- Descripteurs ---------------------------------------------------------
93
+ exports.LANGUAGES = [
94
+ {
95
+ label: 'Java',
96
+ grammarFile: 'tree-sitter-java.wasm',
97
+ vscodeLanguageIds: ['java'],
98
+ extensions: ['.java'],
99
+ nodes: JAVA_NODES,
100
+ rules: JAVA_RULES,
101
+ fixedContext: 'server',
102
+ },
103
+ {
104
+ // tsx couvre .js et .jsx : c'est un sur-ensemble de la grammaire javascript,
105
+ // qui devient inutile à embarquer.
106
+ label: 'JavaScript',
107
+ grammarFile: 'tree-sitter-tsx.wasm',
108
+ vscodeLanguageIds: ['javascript', 'javascriptreact'],
109
+ extensions: ['.js', '.jsx', '.mjs', '.cjs'],
110
+ nodes: JS_NODES,
111
+ rules: JS_RULES,
112
+ },
113
+ {
114
+ // .ts exige la grammaire typescript : tsx lit `<T>valeur` comme une
115
+ // ouverture JSX et perd le reste du fichier (mesuré le 3 sept. 2026).
116
+ label: 'TypeScript',
117
+ grammarFile: 'tree-sitter-typescript.wasm',
118
+ vscodeLanguageIds: ['typescript'],
119
+ extensions: ['.ts'],
120
+ nodes: JS_NODES,
121
+ rules: JS_RULES,
122
+ },
123
+ {
124
+ label: 'TypeScript React',
125
+ grammarFile: 'tree-sitter-tsx.wasm',
126
+ vscodeLanguageIds: ['typescriptreact'],
127
+ extensions: ['.tsx'],
128
+ nodes: JS_NODES,
129
+ rules: JS_RULES,
130
+ },
131
+ ];
132
+ /** Descripteur correspondant à un `languageId` VSCode, ou undefined. */
133
+ function specFor(languageId) {
134
+ return exports.LANGUAGES.find(l => l.vscodeLanguageIds.includes(languageId));
135
+ }
136
+ /** Le langage est-il analysable ? */
137
+ function isSupported(languageId) {
138
+ return specFor(languageId) !== undefined;
139
+ }
140
+ /** Fichiers WASM à charger, sans doublon (tsx sert deux descripteurs). */
141
+ function grammarFiles() {
142
+ return [...new Set(exports.LANGUAGES.map(l => l.grammarFile))];
143
+ }
144
+ /**
145
+ * Dossiers exclus de toute analyse de projet.
146
+ *
147
+ * Partagé par le scan VSCode et le CLI : deux listes divergentes donneraient
148
+ * deux verdicts différents sur le même dépôt, ce qui ruinerait la confiance
149
+ * dans l'outil bien plus sûrement qu'une règle imparfaite.
150
+ */
151
+ exports.EXCLUDED_DIRS = [
152
+ 'node_modules', 'target', 'build', 'dist', 'bin', 'out',
153
+ '.git', '.gradle', '.idea', '.next', 'coverage', 'vendor',
154
+ ];
155
+ /** Motif VSCode excluant ces dossiers. */
156
+ function excludeGlob() {
157
+ return `**/{${exports.EXCLUDED_DIRS.join(',')}}/**`;
158
+ }
159
+ /** Motif de recherche VSCode pour un langage, dérivé de ses extensions. */
160
+ function globFor(spec) {
161
+ const exts = spec.extensions.map(e => e.replace(/^\./, ''));
162
+ return exts.length === 1 ? `**/*.${exts[0]}` : `**/*.{${exts.join(',')}}`;
163
+ }
164
+ /** Descripteur correspondant à un chemin de fichier, ou undefined. */
165
+ function specForPath(filePath) {
166
+ const lower = filePath.toLowerCase();
167
+ return exports.LANGUAGES.find(l => l.extensions.some(e => lower.endsWith(e)));
168
+ }
169
+ //# sourceMappingURL=languages.js.map
@@ -0,0 +1,28 @@
1
+ import Parser from 'web-tree-sitter';
2
+ import { LanguageSpec } from './languages';
3
+ /**
4
+ * Initialise web-tree-sitter et charge toutes les grammaires déclarées.
5
+ * Doit être appelé une seule fois dans activate().
6
+ *
7
+ * Les .wasm sont lus dans out/wasm/, où le script de build les a copiés :
8
+ * node_modules n'est pas présent dans un .vsix installé.
9
+ *
10
+ * Le chargement est immédiat plutôt que paresseux : une grammaire coûte
11
+ * quelques millisecondes (mesuré : 5 ms pour tsx, 2 ms pour java), ce qui ne
12
+ * justifie pas la complexité d'un chargement à la demande.
13
+ *
14
+ * @param extensionPath racine de l'extension (context.extensionUri.fsPath).
15
+ * Par défaut, la racine du paquet installé : un consommateur npm ne peut pas
16
+ * deviner ce chemin, VSCode le fournit explicitement.
17
+ */
18
+ export declare function initParser(extensionPath?: string): Promise<void>;
19
+ /**
20
+ * Parse du code source avec la grammaire du langage indiqué.
21
+ *
22
+ * @param code source à analyser
23
+ * @param languageId `languageId` VSCode ('java', 'typescriptreact'…)
24
+ * @throws si le parser n'est pas initialisé ou le langage non supporté
25
+ */
26
+ export declare function parse(code: string, languageId: string): Parser.Tree;
27
+ /** Variante prenant directement un descripteur, pour le scan workspace. */
28
+ export declare function parseWith(code: string, spec: LanguageSpec): Parser.Tree;
package/out/parser.js ADDED
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.initParser = initParser;
40
+ exports.parse = parse;
41
+ exports.parseWith = parseWith;
42
+ const web_tree_sitter_1 = __importDefault(require("web-tree-sitter"));
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const languages_1 = require("./languages");
46
+ let _parser = null;
47
+ /** Grammaires chargées, indexées par nom de fichier WASM. */
48
+ const _grammars = new Map();
49
+ /**
50
+ * Initialise web-tree-sitter et charge toutes les grammaires déclarées.
51
+ * Doit être appelé une seule fois dans activate().
52
+ *
53
+ * Les .wasm sont lus dans out/wasm/, où le script de build les a copiés :
54
+ * node_modules n'est pas présent dans un .vsix installé.
55
+ *
56
+ * Le chargement est immédiat plutôt que paresseux : une grammaire coûte
57
+ * quelques millisecondes (mesuré : 5 ms pour tsx, 2 ms pour java), ce qui ne
58
+ * justifie pas la complexité d'un chargement à la demande.
59
+ *
60
+ * @param extensionPath racine de l'extension (context.extensionUri.fsPath).
61
+ * Par défaut, la racine du paquet installé : un consommateur npm ne peut pas
62
+ * deviner ce chemin, VSCode le fournit explicitement.
63
+ */
64
+ async function initParser(extensionPath = path.join(__dirname, '..')) {
65
+ if (_parser)
66
+ return; // déjà initialisé
67
+ const wasmDir = path.join(extensionPath, 'out', 'wasm');
68
+ // Le runtime emscripten réclame tree-sitter.wasm par son nom de fichier
69
+ await web_tree_sitter_1.default.init({
70
+ locateFile(scriptName) {
71
+ return path.join(wasmDir, scriptName);
72
+ }
73
+ });
74
+ for (const file of (0, languages_1.grammarFiles)()) {
75
+ const buffer = fs.readFileSync(path.join(wasmDir, file));
76
+ _grammars.set(file, await web_tree_sitter_1.default.Language.load(buffer));
77
+ }
78
+ _parser = new web_tree_sitter_1.default();
79
+ }
80
+ /**
81
+ * Parse du code source avec la grammaire du langage indiqué.
82
+ *
83
+ * @param code source à analyser
84
+ * @param languageId `languageId` VSCode ('java', 'typescriptreact'…)
85
+ * @throws si le parser n'est pas initialisé ou le langage non supporté
86
+ */
87
+ function parse(code, languageId) {
88
+ const spec = (0, languages_1.specFor)(languageId);
89
+ if (!spec) {
90
+ throw new Error(`Langage non supporté : ${languageId}`);
91
+ }
92
+ return parseWith(code, spec);
93
+ }
94
+ /** Variante prenant directement un descripteur, pour le scan workspace. */
95
+ function parseWith(code, spec) {
96
+ if (!_parser) {
97
+ throw new Error('Parser non initialisé. Appeler initParser() d\'abord.');
98
+ }
99
+ const grammar = _grammars.get(spec.grammarFile);
100
+ if (!grammar) {
101
+ throw new Error(`Grammaire non chargée : ${spec.grammarFile}`);
102
+ }
103
+ _parser.setLanguage(grammar);
104
+ return _parser.parse(code);
105
+ }
106
+ //# sourceMappingURL=parser.js.map
package/out/rules.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import Parser from 'web-tree-sitter';
2
+ import { ExecutionContext, Finding } from './types';
3
+ import { LanguageSpec } from './languages';
4
+ /**
5
+ * Parcourt l'AST et collecte les findings énergivores applicables au langage.
6
+ *
7
+ * Les règles évaluées, comme les noms de nœuds, viennent du descripteur : ce
8
+ * module ne connaît aucun langage en particulier.
9
+ *
10
+ * Règles implémentées :
11
+ * 1. nested-loops Boucle imbriquée (haute, poids 15)
12
+ * 2. string-concat-in-loop Concaténation `+=` en boucle (moyenne, poids 7)
13
+ * 3. object-creation-in-loop Instanciation en boucle (moyenne, poids 7)
14
+ * 4. regex-compile-in-loop Pattern.compile() en boucle (haute, poids 12)
15
+ * 5. blocking-io-in-loop I/O bloquant en boucle (haute, poids 12)
16
+ * 6. sql-without-limit Requête SQL sans LIMIT (haute, poids 12)
17
+ */
18
+ export declare function collectFindings(root: Parser.SyntaxNode, spec: LanguageSpec, context?: ExecutionContext): Finding[];
package/out/rules.js ADDED
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectFindings = collectFindings;
4
+ const languages_1 = require("./languages");
5
+ /** Méthodes d'I/O bloquantes à détecter lorsqu'elles sont appelées en boucle. */
6
+ const IO_METHOD_NAMES = new Set([
7
+ 'readLine', 'readAllBytes', 'readString', 'readAllLines',
8
+ 'readNBytes', 'readFully', 'read', 'nextLine',
9
+ ]);
10
+ /** API synchrones de Node : elles bloquent la boucle d'événements. */
11
+ const SYNC_NODE_METHODS = new Set([
12
+ 'readFileSync', 'writeFileSync', 'appendFileSync', 'existsSync',
13
+ 'readdirSync', 'statSync', 'lstatSync', 'mkdirSync', 'rmSync',
14
+ 'unlinkSync', 'copyFileSync', 'readlinkSync', 'execSync', 'spawnSync',
15
+ 'execFileSync',
16
+ ]);
17
+ /** Événements tirés en rafale pendant l'interaction. */
18
+ const HIGH_FREQUENCY_EVENTS = new Set([
19
+ 'scroll', 'resize', 'mousemove', 'wheel', 'touchmove', 'pointermove', 'drag',
20
+ ]);
21
+ /** Leurs équivalents en attributs JSX. */
22
+ const HIGH_FREQUENCY_JSX_PROPS = new Set([
23
+ 'onScroll', 'onMouseMove', 'onWheel', 'onTouchMove', 'onPointerMove', 'onDrag',
24
+ ]);
25
+ /**
26
+ * Bibliothèques dont l'import par défaut embarque tout le paquet, là où un
27
+ * import nommé ou un sous-chemin ne prendrait que le nécessaire.
28
+ */
29
+ const HEAVY_MODULES = new Set([
30
+ 'lodash', 'underscore', 'moment', 'rxjs', 'jquery',
31
+ ]);
32
+ /** En dessous, un timer permanent maintient le processeur éveillé en continu. */
33
+ const FREQUENT_INTERVAL_MS = 1000;
34
+ /** Un gestionnaire passé par `debounce()` ou `throttle()` est déjà maîtrisé. */
35
+ const THROTTLE_PATTERN = /debounce|throttle|raf|requestAnimationFrame/i;
36
+ /**
37
+ * Une vraie requête, pas une chaîne qui contient les mots.
38
+ *
39
+ * Deux faux positifs successifs ont façonné ces critères :
40
+ * - `includes('SELECT')` classait `"selection"` et `"onSelect"` comme des
41
+ * requêtes — 34 alertes sur le seul compilateur TypeScript ;
42
+ * - exiger seulement `SELECT` puis `FROM` en délimiteurs de mot ne suffisait
43
+ * pas non plus : un bloc CSS-in-JS contenant `input, select, textarea {…}`
44
+ * et `@keyframes spin { from {…} }` déclenchait la règle.
45
+ *
46
+ * Une requête *commence* par `SELECT`, éventuellement précédé d'un `WITH`, et
47
+ * contient les deux mots-clés.
48
+ *
49
+ * **Trois motifs sans quantificateur imbriqué plutôt qu'un seul.** La version
50
+ * combinée `/^\s*(WITH\b[\s\S]*?)?SELECT\b[\s\S]*\bFROM\b/` était quadratique :
51
+ * sur une chaîne commençant par `WITH` et enchaînant des `SELECT` sans jamais
52
+ * de `FROM`, le temps quadruplait à chaque doublement de taille — 63 ms pour
53
+ * 36 Ko, plusieurs secondes pour quelques centaines de Ko. Un littéral forgé
54
+ * suffisait à faire traîner une analyse, ce qui compte quand la CI examine le
55
+ * code d'une contribution extérieure. Chacun de ces trois motifs est linéaire.
56
+ */
57
+ const SQL_STARTS = /^\s*(SELECT|WITH)\b/;
58
+ const HAS_SELECT = /\bSELECT\b/;
59
+ const HAS_FROM = /\bFROM\b/;
60
+ function looksLikeQuery(upper) {
61
+ return SQL_STARTS.test(upper) && HAS_SELECT.test(upper) && HAS_FROM.test(upper);
62
+ }
63
+ /**
64
+ * Parcourt l'AST et collecte les findings énergivores applicables au langage.
65
+ *
66
+ * Les règles évaluées, comme les noms de nœuds, viennent du descripteur : ce
67
+ * module ne connaît aucun langage en particulier.
68
+ *
69
+ * Règles implémentées :
70
+ * 1. nested-loops Boucle imbriquée (haute, poids 15)
71
+ * 2. string-concat-in-loop Concaténation `+=` en boucle (moyenne, poids 7)
72
+ * 3. object-creation-in-loop Instanciation en boucle (moyenne, poids 7)
73
+ * 4. regex-compile-in-loop Pattern.compile() en boucle (haute, poids 12)
74
+ * 5. blocking-io-in-loop I/O bloquant en boucle (haute, poids 12)
75
+ * 6. sql-without-limit Requête SQL sans LIMIT (haute, poids 12)
76
+ */
77
+ function collectFindings(root, spec, context = 'unknown') {
78
+ const findings = [];
79
+ const ctx = {
80
+ nodes: spec.nodes,
81
+ active: (rule) => spec.rules.includes(rule) && (0, languages_1.ruleAppliesIn)(rule, context),
82
+ };
83
+ traverse(root, { loopDepth: 0, inLoop: false, inLoopHere: false, inFunction: false }, findings, ctx);
84
+ return findings;
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Traversée récursive
88
+ // ---------------------------------------------------------------------------
89
+ function traverse(node, scope, findings, ctx) {
90
+ const { loopDepth, inLoop } = scope;
91
+ const isLoop = ctx.nodes.loops.includes(node.type);
92
+ const isFunction = ctx.nodes.functions.includes(node.type);
93
+ // Une fonction imbriquée dans une boucle s'exécute dans son propre flot :
94
+ // `items.map(async x => await f(x))` lance des attentes concurrentes, pas
95
+ // séquentielles. La profondeur de boucle « locale » repart donc à zéro.
96
+ const next = {
97
+ loopDepth: isLoop ? loopDepth + 1 : loopDepth,
98
+ inLoop: inLoop || isLoop,
99
+ inLoopHere: isFunction ? false : (scope.inLoopHere || isLoop),
100
+ inFunction: scope.inFunction || isFunction,
101
+ };
102
+ const nextInLoop = next.inLoop;
103
+ // ── Règle 1 : boucle imbriquée ───────────────────────────────────────────
104
+ if (ctx.active('nested-loops') && isLoop && loopDepth >= 1) {
105
+ findings.push({
106
+ startLine: node.startPosition.row,
107
+ startChar: node.startPosition.column,
108
+ endLine: node.startPosition.row,
109
+ endChar: 9999,
110
+ message: 'Boucle imbriquée — complexité ≥ O(n²), forte consommation énergétique. ' +
111
+ 'Envisager une restructuration algorithmique ou une mise en cache.',
112
+ severity: 'high',
113
+ weight: 15,
114
+ });
115
+ }
116
+ // ── Règle 2 : concaténation String avec += en boucle ─────────────────────
117
+ if (ctx.active('string-concat-in-loop') && nextInLoop &&
118
+ ctx.nodes.compoundAssignment.includes(node.type)) {
119
+ if (node.children.some(c => c.text === '+=')) {
120
+ findings.push({
121
+ startLine: node.startPosition.row,
122
+ startChar: node.startPosition.column,
123
+ endLine: node.endPosition.row,
124
+ endChar: node.endPosition.column,
125
+ message: 'Concaténation potentielle via `+=` en boucle — ' +
126
+ "préférer StringBuilder pour éviter la création d'objets String répétés.",
127
+ severity: 'medium',
128
+ weight: 7,
129
+ });
130
+ }
131
+ }
132
+ // ── Règle 3 : création d'objet en boucle ─────────────────────────────────
133
+ if (ctx.active('object-creation-in-loop') && nextInLoop &&
134
+ ctx.nodes.objectCreation.includes(node.type)) {
135
+ findings.push({
136
+ startLine: node.startPosition.row,
137
+ startChar: node.startPosition.column,
138
+ endLine: node.endPosition.row,
139
+ endChar: node.endPosition.column,
140
+ message: "Création d'objet (`new`) en boucle — envisager de déplacer l'instanciation " +
141
+ 'hors de la boucle ou de réutiliser l\'instance existante.',
142
+ severity: 'medium',
143
+ weight: 7,
144
+ });
145
+ }
146
+ // ── Règles 4 et 5 : appels de méthodes détectables en boucle ─────────────
147
+ // Ces deux règles lisent les champs `name` et `object`, propres à la
148
+ // structure d'appel de Java. Elles ne sont pour l'instant déclarées que
149
+ // pour ce langage ; l'équivalent JS/TS relève du jeu de règles web.
150
+ if (nextInLoop && ctx.nodes.call.includes(node.type)) {
151
+ const methodName = node.childForFieldName('name')?.text ?? '';
152
+ const objectName = node.childForFieldName('object')?.text ?? '';
153
+ // Règle 4 : Pattern.compile() en boucle
154
+ if (ctx.active('regex-compile-in-loop') && objectName === 'Pattern' && methodName === 'compile') {
155
+ findings.push({
156
+ startLine: node.startPosition.row,
157
+ startChar: node.startPosition.column,
158
+ endLine: node.endPosition.row,
159
+ endChar: node.endPosition.column,
160
+ message: 'Pattern.compile() appelé en boucle — compiler la regex une seule fois ' +
161
+ 'en dehors de la boucle (constante statique ou champ de classe).',
162
+ severity: 'high',
163
+ weight: 12,
164
+ });
165
+ }
166
+ // Règle 5 : I/O bloquant en boucle
167
+ if (ctx.active('blocking-io-in-loop') && IO_METHOD_NAMES.has(methodName)) {
168
+ findings.push({
169
+ startLine: node.startPosition.row,
170
+ startChar: node.startPosition.column,
171
+ endLine: node.endPosition.row,
172
+ endChar: node.endPosition.column,
173
+ message: `Appel I/O bloquant \`${methodName}()\` en boucle — ` +
174
+ 'charger les données hors de la boucle ou traiter en batch.',
175
+ severity: 'high',
176
+ weight: 12,
177
+ });
178
+ }
179
+ }
180
+ // ── Règle 6 : requête SQL sans pagination (partout, pas seulement en boucle) ──
181
+ if (ctx.active('sql-without-limit') && ctx.nodes.stringLiteral.includes(node.type)) {
182
+ // Sans retirer les guillemets, l'ancrage en tête ne peut pas s'appliquer.
183
+ const upper = unquote(node.text).toUpperCase();
184
+ if (looksLikeQuery(upper) && !upper.includes('LIMIT') && !upper.includes('ROWNUM')) {
185
+ findings.push({
186
+ startLine: node.startPosition.row,
187
+ startChar: node.startPosition.column,
188
+ endLine: node.endPosition.row,
189
+ endChar: node.endPosition.column,
190
+ message: 'Requête SQL sans clause LIMIT détectée — ajouter une pagination ' +
191
+ 'pour éviter de charger un volume non borné de données.',
192
+ severity: 'high',
193
+ weight: 12,
194
+ });
195
+ }
196
+ }
197
+ // ── Règles web ───────────────────────────────────────────────────────────
198
+ // Les cinq suivantes sont propres à JS/TS : elles lisent des formes d'arbre
199
+ // et des API qui n'existent que là, et ne sont déclarées que pour ces
200
+ // langages. Même convention que les règles 4 et 5 pour Java.
201
+ // Attente enchaînée dans une boucle
202
+ if (ctx.active('await-in-loop') && scope.inLoopHere && node.type === 'await_expression') {
203
+ findings.push({
204
+ ...span(node),
205
+ message: 'Attente (`await`) dans une boucle — les appels s\'enchaînent au lieu de ' +
206
+ 'se recouvrir. Collecter les promesses et les attendre ensemble ' +
207
+ '(`Promise.all`), sauf si l\'ordre est réellement nécessaire.',
208
+ severity: 'high',
209
+ weight: 12,
210
+ });
211
+ }
212
+ if (ctx.nodes.call.includes(node.type)) {
213
+ const callee = node.childForFieldName('function');
214
+ const calleeName = callee?.type === 'member_expression'
215
+ ? callee.childForFieldName('property')?.text ?? ''
216
+ : callee?.text ?? '';
217
+ const args = node.childForFieldName('arguments')?.namedChildren ?? [];
218
+ // I/O synchrone dans une fonction : au démarrage c'est acceptable, dans
219
+ // une fonction ça se répète et bloque la boucle d'événements.
220
+ if (ctx.active('sync-io-in-function') && scope.inFunction && SYNC_NODE_METHODS.has(calleeName)) {
221
+ findings.push({
222
+ ...span(node),
223
+ message: `Appel synchrone \`${calleeName}()\` dans une fonction — il bloque la ` +
224
+ 'boucle d\'événements et donc toutes les requêtes en cours. Préférer ' +
225
+ 'la variante asynchrone (`fs/promises`).',
226
+ severity: 'high',
227
+ weight: 12,
228
+ });
229
+ }
230
+ // Polling : un timer permanent empêche l'appareil de se mettre au repos.
231
+ if (ctx.active('polling-interval') && calleeName === 'setInterval') {
232
+ const delay = numericValue(args[1]);
233
+ const frequent = delay !== null && delay < FREQUENT_INTERVAL_MS;
234
+ findings.push({
235
+ ...span(node),
236
+ message: `Timer périodique${delay !== null ? ` toutes les ${delay} ms` : ''} — ` +
237
+ 'il empêche le processeur de se mettre au repos, sur chaque appareil ' +
238
+ 'qui affiche la page. Préférer un événement, une requête à la demande ' +
239
+ 'ou un intervalle plus long.',
240
+ severity: frequent ? 'high' : 'medium',
241
+ weight: frequent ? 12 : 7,
242
+ });
243
+ }
244
+ // Gestionnaire d'événement à haute fréquence sans limitation de débit
245
+ if (ctx.active('unthrottled-event-listener') && calleeName === 'addEventListener') {
246
+ const event = args[0] && isStringNode(args[0], ctx) ? unquote(args[0].text) : '';
247
+ if (HIGH_FREQUENCY_EVENTS.has(event) && !isThrottled(args[1])) {
248
+ findings.push({
249
+ ...span(node),
250
+ message: `Gestionnaire \`${event}\` sans limitation de débit — cet événement est ` +
251
+ 'tiré en rafale. Passer par `debounce()`, `throttle()` ou ' +
252
+ '`requestAnimationFrame()`.',
253
+ severity: 'medium',
254
+ weight: 7,
255
+ });
256
+ }
257
+ }
258
+ // require() d'une bibliothèque entière
259
+ if (ctx.active('whole-library-import') && calleeName === 'require') {
260
+ const source = args[0] && isStringNode(args[0], ctx) ? unquote(args[0].text) : '';
261
+ if (HEAVY_MODULES.has(source))
262
+ findings.push(heavyImportFinding(node, source));
263
+ }
264
+ }
265
+ // Version JS de la règle 4 : `new RegExp(...)` recompile à chaque tour,
266
+ // là où un littéral /.../ est compilé une seule fois.
267
+ if (ctx.active('regex-compile-in-loop') && nextInLoop &&
268
+ ctx.nodes.objectCreation.includes(node.type) &&
269
+ node.childForFieldName('constructor')?.text === 'RegExp') {
270
+ findings.push({
271
+ ...span(node),
272
+ message: '`new RegExp()` en boucle — la regex est recompilée à chaque tour. ' +
273
+ 'La construire une seule fois hors de la boucle, ou utiliser un ' +
274
+ 'littéral `/.../`.',
275
+ severity: 'high',
276
+ weight: 12,
277
+ });
278
+ }
279
+ // Import par défaut ou global d'une bibliothèque lourde
280
+ if (ctx.active('whole-library-import') && node.type === 'import_statement') {
281
+ const source = unquote(node.childForFieldName('source')?.text ?? '');
282
+ const clause = node.namedChildren.find(c => c.type === 'import_clause');
283
+ const wholePackage = clause?.namedChildren.some(c => c.type === 'identifier' || c.type === 'namespace_import');
284
+ if (HEAVY_MODULES.has(source) && wholePackage) {
285
+ findings.push(heavyImportFinding(node, source));
286
+ }
287
+ }
288
+ // Gestionnaire JSX à haute fréquence — le cas courant en React
289
+ if (ctx.active('unthrottled-event-listener') && node.type === 'jsx_attribute') {
290
+ const name = node.namedChildren[0]?.text ?? '';
291
+ if (HIGH_FREQUENCY_JSX_PROPS.has(name)) {
292
+ const value = node.namedChildren[1];
293
+ const expr = value?.type === 'jsx_expression' ? value.namedChildren[0] : value;
294
+ if (!isThrottled(expr)) {
295
+ findings.push({
296
+ ...span(node),
297
+ message: `Gestionnaire \`${name}\` sans limitation de débit — cet événement est ` +
298
+ 'tiré en rafale. Passer par `debounce()`, `throttle()` ou ' +
299
+ '`requestAnimationFrame()`.',
300
+ severity: 'medium',
301
+ weight: 7,
302
+ });
303
+ }
304
+ }
305
+ }
306
+ // Récursion sur les enfants nommés
307
+ for (const child of node.namedChildren) {
308
+ traverse(child, next, findings, ctx);
309
+ }
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // Utilitaires
313
+ // ---------------------------------------------------------------------------
314
+ function span(node) {
315
+ return {
316
+ startLine: node.startPosition.row,
317
+ startChar: node.startPosition.column,
318
+ endLine: node.endPosition.row,
319
+ endChar: node.endPosition.column,
320
+ };
321
+ }
322
+ function heavyImportFinding(node, source) {
323
+ return {
324
+ ...span(node),
325
+ message: `Import global de \`${source}\` — tout le paquet part dans le bundle, donc ` +
326
+ 'sur le réseau vers chaque visiteur. Importer seulement ce qui sert ' +
327
+ `(\`import { x } from '${source}/x'\`).`,
328
+ severity: 'medium',
329
+ weight: 7,
330
+ };
331
+ }
332
+ function isStringNode(node, ctx) {
333
+ return ctx.nodes.stringLiteral.includes(node.type);
334
+ }
335
+ /** Valeur d'un littéral numérique, ou null si l'argument n'en est pas un. */
336
+ function numericValue(node) {
337
+ if (!node || node.type !== 'number')
338
+ return null;
339
+ const value = Number(node.text);
340
+ return Number.isFinite(value) ? value : null;
341
+ }
342
+ /** Le gestionnaire passe-t-il visiblement par un limiteur de débit ? */
343
+ function isThrottled(node) {
344
+ if (!node)
345
+ return false;
346
+ return THROTTLE_PATTERN.test(node.text);
347
+ }
348
+ function unquote(text) {
349
+ return text.replace(/^['"`]|['"`]$/g, '');
350
+ }
351
+ //# sourceMappingURL=rules.js.map