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.
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/out/cli.d.ts +28 -0
- package/out/cli.js +254 -0
- package/out/context.d.ts +31 -0
- package/out/context.js +173 -0
- package/out/extension.d.ts +3 -0
- package/out/extension.js +269 -0
- package/out/index.d.ts +28 -0
- package/out/index.js +44 -0
- package/out/languages.d.ts +76 -0
- package/out/languages.js +169 -0
- package/out/parser.d.ts +28 -0
- package/out/parser.js +106 -0
- package/out/rules.d.ts +18 -0
- package/out/rules.js +351 -0
- package/out/scoring.d.ts +26 -0
- package/out/scoring.js +71 -0
- package/out/types.d.ts +47 -0
- package/out/types.js +3 -0
- package/out/wasm/tree-sitter-java.wasm +0 -0
- package/out/wasm/tree-sitter-tsx.wasm +0 -0
- package/out/wasm/tree-sitter-typescript.wasm +0 -0
- package/out/wasm/tree-sitter.wasm +0 -0
- package/out/webview.d.ts +5 -0
- package/out/webview.js +231 -0
- package/out/workspace.d.ts +10 -0
- package/out/workspace.js +122 -0
- package/package.json +67 -0
package/out/languages.js
ADDED
|
@@ -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
|
package/out/parser.d.ts
ADDED
|
@@ -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
|