@harness-engineering/eslint-plugin 0.2.3 → 0.2.4
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/dist/index.js +289 -232
- package/package.json +14 -12
package/dist/index.js
CHANGED
|
@@ -3,25 +3,25 @@ import { ESLintUtils, AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-esli
|
|
|
3
3
|
|
|
4
4
|
// src/utils/ast-helpers.ts
|
|
5
5
|
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
6
|
-
function
|
|
7
|
-
if (!node.range) return false;
|
|
8
|
-
const textBefore = sourceCode.slice(0, node.range[0]);
|
|
9
|
-
const lines = textBefore.split("\n");
|
|
10
|
-
let foundJSDoc = false;
|
|
6
|
+
function lastNonEmptyLineEndsBlockComment(lines) {
|
|
11
7
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
12
8
|
const line = lines[i]?.trim() ?? "";
|
|
13
9
|
if (line === "") continue;
|
|
14
|
-
|
|
15
|
-
const startIdx = textBefore.lastIndexOf("/**");
|
|
16
|
-
const endIdx = textBefore.lastIndexOf("*/");
|
|
17
|
-
if (startIdx !== -1 && endIdx > startIdx) {
|
|
18
|
-
foundJSDoc = true;
|
|
19
|
-
}
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
break;
|
|
10
|
+
return line.endsWith("*/");
|
|
23
11
|
}
|
|
24
|
-
return
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
function textContainsJSDoc(text) {
|
|
15
|
+
const startIdx = text.lastIndexOf("/**");
|
|
16
|
+
const endIdx = text.lastIndexOf("*/");
|
|
17
|
+
return startIdx !== -1 && endIdx > startIdx;
|
|
18
|
+
}
|
|
19
|
+
function hasJSDocComment(node, sourceCode) {
|
|
20
|
+
if (!node.range) return false;
|
|
21
|
+
const textBefore = sourceCode.slice(0, node.range[0]);
|
|
22
|
+
const lines = textBefore.split("\n");
|
|
23
|
+
if (!lastNonEmptyLineEndsBlockComment(lines)) return false;
|
|
24
|
+
return textContainsJSDoc(textBefore);
|
|
25
25
|
}
|
|
26
26
|
var SKIP_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
|
|
27
27
|
var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set(["parse", "safeParse"]);
|
|
@@ -33,17 +33,20 @@ function isZodParseCall(node) {
|
|
|
33
33
|
if (prop.type !== AST_NODE_TYPES.Identifier) return false;
|
|
34
34
|
return ZOD_PARSE_METHODS.has(prop.name);
|
|
35
35
|
}
|
|
36
|
+
function visitArrayItems(items, visitor) {
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
if (item && typeof item === "object" && "type" in item) {
|
|
39
|
+
visitor(item);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
function visitChildren(node, visitor) {
|
|
37
44
|
for (const key of Object.keys(node)) {
|
|
38
45
|
if (SKIP_KEYS.has(key)) continue;
|
|
39
46
|
const value = node[key];
|
|
40
47
|
if (!value || typeof value !== "object") continue;
|
|
41
48
|
if (Array.isArray(value)) {
|
|
42
|
-
|
|
43
|
-
if (item && typeof item === "object" && "type" in item) {
|
|
44
|
-
visitor(item);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
49
|
+
visitArrayItems(value, visitor);
|
|
47
50
|
} else if ("type" in value) {
|
|
48
51
|
visitor(value);
|
|
49
52
|
}
|
|
@@ -77,6 +80,43 @@ function isMarkedInternal(node, sourceCode) {
|
|
|
77
80
|
var createRule = ESLintUtils.RuleCreator(
|
|
78
81
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
79
82
|
);
|
|
83
|
+
function getVariableExports(decl) {
|
|
84
|
+
return decl.declarations.filter((d) => d.id.type === AST_NODE_TYPES2.Identifier).map((d) => ({ kind: "variable", name: d.id.name }));
|
|
85
|
+
}
|
|
86
|
+
function getExportInfo(decl, ignoreTypes) {
|
|
87
|
+
const declType = decl.type;
|
|
88
|
+
if (declType === AST_NODE_TYPES2.FunctionDeclaration) {
|
|
89
|
+
const fn = decl;
|
|
90
|
+
return fn.id ? [{ kind: "function", name: fn.id.name }] : [];
|
|
91
|
+
}
|
|
92
|
+
if (declType === AST_NODE_TYPES2.ClassDeclaration) {
|
|
93
|
+
const cls = decl;
|
|
94
|
+
return cls.id ? [{ kind: "class", name: cls.id.name }] : [];
|
|
95
|
+
}
|
|
96
|
+
if (declType === AST_NODE_TYPES2.VariableDeclaration) {
|
|
97
|
+
return getVariableExports(decl);
|
|
98
|
+
}
|
|
99
|
+
if (declType === AST_NODE_TYPES2.TSTypeAliasDeclaration && !ignoreTypes) {
|
|
100
|
+
return [{ kind: "type", name: decl.id.name }];
|
|
101
|
+
}
|
|
102
|
+
if (declType === AST_NODE_TYPES2.TSInterfaceDeclaration && !ignoreTypes) {
|
|
103
|
+
return [{ kind: "interface", name: decl.id.name }];
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
function checkExportNode(node, kind, name, sourceCode, ignoreInternal, report) {
|
|
108
|
+
if (ignoreInternal && isMarkedInternal(node, sourceCode)) return;
|
|
109
|
+
if (!hasJSDocComment(node, sourceCode)) {
|
|
110
|
+
report({ node, messageId: "missingJSDoc", data: { kind, name } });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function handleExportNamedDeclaration(node, ignoreTypes, sourceCode, ignoreInternal, report) {
|
|
114
|
+
const decl = node.declaration;
|
|
115
|
+
if (!decl) return;
|
|
116
|
+
for (const info of getExportInfo(decl, ignoreTypes)) {
|
|
117
|
+
checkExportNode(node, info.kind, info.name, sourceCode, ignoreInternal, report);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
80
120
|
var enforce_doc_exports_default = createRule({
|
|
81
121
|
name: "enforce-doc-exports",
|
|
82
122
|
meta: {
|
|
@@ -101,47 +141,12 @@ var enforce_doc_exports_default = createRule({
|
|
|
101
141
|
defaultOptions: [{ ignoreTypes: false, ignoreInternal: true }],
|
|
102
142
|
create(context, [options]) {
|
|
103
143
|
const sourceCode = context.sourceCode.getText();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (!hasJSDocComment(node, sourceCode)) {
|
|
109
|
-
context.report({
|
|
110
|
-
node,
|
|
111
|
-
messageId: "missingJSDoc",
|
|
112
|
-
data: { kind, name }
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function getExportInfo(decl) {
|
|
117
|
-
const declType = decl.type;
|
|
118
|
-
if (declType === AST_NODE_TYPES2.FunctionDeclaration) {
|
|
119
|
-
const fn = decl;
|
|
120
|
-
return fn.id ? [{ kind: "function", name: fn.id.name }] : [];
|
|
121
|
-
}
|
|
122
|
-
if (declType === AST_NODE_TYPES2.ClassDeclaration) {
|
|
123
|
-
const cls = decl;
|
|
124
|
-
return cls.id ? [{ kind: "class", name: cls.id.name }] : [];
|
|
125
|
-
}
|
|
126
|
-
if (declType === AST_NODE_TYPES2.VariableDeclaration) {
|
|
127
|
-
const varDecl = decl;
|
|
128
|
-
return varDecl.declarations.filter((d) => d.id.type === AST_NODE_TYPES2.Identifier).map((d) => ({ kind: "variable", name: d.id.name }));
|
|
129
|
-
}
|
|
130
|
-
if (declType === AST_NODE_TYPES2.TSTypeAliasDeclaration && !options.ignoreTypes) {
|
|
131
|
-
return [{ kind: "type", name: decl.id.name }];
|
|
132
|
-
}
|
|
133
|
-
if (declType === AST_NODE_TYPES2.TSInterfaceDeclaration && !options.ignoreTypes) {
|
|
134
|
-
return [{ kind: "interface", name: decl.id.name }];
|
|
135
|
-
}
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
144
|
+
const ignoreTypes = options.ignoreTypes ?? false;
|
|
145
|
+
const ignoreInternal = options.ignoreInternal ?? true;
|
|
146
|
+
const report = context.report.bind(context);
|
|
138
147
|
return {
|
|
139
148
|
ExportNamedDeclaration(node) {
|
|
140
|
-
|
|
141
|
-
if (!decl) return;
|
|
142
|
-
for (const info of getExportInfo(decl)) {
|
|
143
|
-
checkExport(node, info.kind, info.name);
|
|
144
|
-
}
|
|
149
|
+
handleExportNamedDeclaration(node, ignoreTypes, sourceCode, ignoreInternal, report);
|
|
145
150
|
}
|
|
146
151
|
};
|
|
147
152
|
}
|
|
@@ -196,6 +201,24 @@ function normalizePath(filePath) {
|
|
|
196
201
|
}
|
|
197
202
|
return path.basename(filePath);
|
|
198
203
|
}
|
|
204
|
+
function checkImportDeclaration(node, context, currentFile) {
|
|
205
|
+
const importPath = node.source.value;
|
|
206
|
+
if (!importPath.startsWith(".")) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const importingDir = path.dirname(context.filename);
|
|
210
|
+
const resolvedPath = path.resolve(importingDir, importPath);
|
|
211
|
+
const normalizedImport = normalizePath(resolvedPath);
|
|
212
|
+
const cycle = detectCycle(currentFile, normalizedImport);
|
|
213
|
+
if (cycle) {
|
|
214
|
+
context.report({
|
|
215
|
+
node,
|
|
216
|
+
messageId: "circularDep",
|
|
217
|
+
data: { cycle: cycle.map((f) => path.basename(f)).join(" \u2192 ") }
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
addEdge(currentFile, normalizedImport);
|
|
221
|
+
}
|
|
199
222
|
var no_circular_deps_default = createRule2({
|
|
200
223
|
name: "no-circular-deps",
|
|
201
224
|
meta: {
|
|
@@ -213,24 +236,7 @@ var no_circular_deps_default = createRule2({
|
|
|
213
236
|
const currentFile = normalizePath(context.filename);
|
|
214
237
|
return {
|
|
215
238
|
ImportDeclaration(node) {
|
|
216
|
-
|
|
217
|
-
if (!importPath.startsWith(".")) {
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const importingDir = path.dirname(context.filename);
|
|
221
|
-
const resolvedPath = path.resolve(importingDir, importPath);
|
|
222
|
-
const normalizedImport = normalizePath(resolvedPath);
|
|
223
|
-
const cycle = detectCycle(currentFile, normalizedImport);
|
|
224
|
-
if (cycle) {
|
|
225
|
-
context.report({
|
|
226
|
-
node,
|
|
227
|
-
messageId: "circularDep",
|
|
228
|
-
data: {
|
|
229
|
-
cycle: cycle.map((f) => path.basename(f)).join(" \u2192 ")
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
addEdge(currentFile, normalizedImport);
|
|
239
|
+
checkImportDeclaration(node, context, currentFile);
|
|
234
240
|
}
|
|
235
241
|
};
|
|
236
242
|
}
|
|
@@ -348,6 +354,34 @@ function normalizePath2(filePath) {
|
|
|
348
354
|
var createRule3 = ESLintUtils3.RuleCreator(
|
|
349
355
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
350
356
|
);
|
|
357
|
+
function importMatchesDisallowed(importPath, resolvedImport, disallowed) {
|
|
358
|
+
return importPath === disallowed || matchesPattern(resolvedImport, disallowed) || matchesPattern(importPath, disallowed);
|
|
359
|
+
}
|
|
360
|
+
function findViolatedRule(applicableRules, importPath, resolvedImport) {
|
|
361
|
+
for (const rule of applicableRules) {
|
|
362
|
+
for (const disallowed of rule.disallow) {
|
|
363
|
+
if (importMatchesDisallowed(importPath, resolvedImport, disallowed)) {
|
|
364
|
+
return rule;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return void 0;
|
|
369
|
+
}
|
|
370
|
+
function buildForbiddenMessage(rule, importPath) {
|
|
371
|
+
return rule.message ?? `Import "${importPath}" is forbidden in files matching "${rule.from}"`;
|
|
372
|
+
}
|
|
373
|
+
function checkImportDeclaration2(node, applicableRules, filename, report) {
|
|
374
|
+
const importPath = node.source.value;
|
|
375
|
+
const resolvedImport = resolveImportPath(importPath, filename);
|
|
376
|
+
const violatedRule = findViolatedRule(applicableRules, importPath, resolvedImport);
|
|
377
|
+
if (violatedRule) {
|
|
378
|
+
report({
|
|
379
|
+
node,
|
|
380
|
+
messageId: "forbiddenImport",
|
|
381
|
+
data: { message: buildForbiddenMessage(violatedRule, importPath) }
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
351
385
|
var no_forbidden_imports_default = createRule3({
|
|
352
386
|
name: "no-forbidden-imports",
|
|
353
387
|
meta: {
|
|
@@ -373,25 +407,10 @@ var no_forbidden_imports_default = createRule3({
|
|
|
373
407
|
if (applicableRules.length === 0) {
|
|
374
408
|
return {};
|
|
375
409
|
}
|
|
410
|
+
const report = context.report.bind(context);
|
|
376
411
|
return {
|
|
377
412
|
ImportDeclaration(node) {
|
|
378
|
-
|
|
379
|
-
const resolvedImport = resolveImportPath(importPath, context.filename);
|
|
380
|
-
for (const rule of applicableRules) {
|
|
381
|
-
for (const disallowed of rule.disallow) {
|
|
382
|
-
const isMatch = importPath === disallowed || matchesPattern(resolvedImport, disallowed) || matchesPattern(importPath, disallowed);
|
|
383
|
-
if (isMatch) {
|
|
384
|
-
context.report({
|
|
385
|
-
node,
|
|
386
|
-
messageId: "forbiddenImport",
|
|
387
|
-
data: {
|
|
388
|
-
message: rule.message || `Import "${importPath}" is forbidden in files matching "${rule.from}"`
|
|
389
|
-
}
|
|
390
|
-
});
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
413
|
+
checkImportDeclaration2(node, applicableRules, context.filename, report);
|
|
395
414
|
}
|
|
396
415
|
};
|
|
397
416
|
}
|
|
@@ -402,6 +421,25 @@ import { ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
|
|
|
402
421
|
var createRule4 = ESLintUtils4.RuleCreator(
|
|
403
422
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
404
423
|
);
|
|
424
|
+
function isLayerViolation(importLayer, currentLayer, currentLayerDef) {
|
|
425
|
+
return importLayer !== currentLayer && !currentLayerDef.allowedDependencies.includes(importLayer);
|
|
426
|
+
}
|
|
427
|
+
function resolveImportLayer(importPath, filename, layers) {
|
|
428
|
+
if (!importPath.startsWith(".")) return null;
|
|
429
|
+
const resolvedImport = resolveImportPath(importPath, filename);
|
|
430
|
+
return getLayerForFile(resolvedImport, layers) ?? null;
|
|
431
|
+
}
|
|
432
|
+
function checkLayerImport(node, context, currentLayer, currentLayerDef, layers) {
|
|
433
|
+
const importLayer = resolveImportLayer(node.source.value, context.filename, layers);
|
|
434
|
+
if (!importLayer) return;
|
|
435
|
+
if (isLayerViolation(importLayer, currentLayer, currentLayerDef)) {
|
|
436
|
+
context.report({
|
|
437
|
+
node,
|
|
438
|
+
messageId: "layerViolation",
|
|
439
|
+
data: { fromLayer: currentLayer, toLayer: importLayer }
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
405
443
|
var no_layer_violation_default = createRule4({
|
|
406
444
|
name: "no-layer-violation",
|
|
407
445
|
meta: {
|
|
@@ -429,27 +467,10 @@ var no_layer_violation_default = createRule4({
|
|
|
429
467
|
if (!currentLayerDef) {
|
|
430
468
|
return {};
|
|
431
469
|
}
|
|
470
|
+
const layers = config.layers;
|
|
432
471
|
return {
|
|
433
472
|
ImportDeclaration(node) {
|
|
434
|
-
|
|
435
|
-
if (!importPath.startsWith(".")) {
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
const resolvedImport = resolveImportPath(importPath, context.filename);
|
|
439
|
-
const importLayer = getLayerForFile(resolvedImport, config.layers);
|
|
440
|
-
if (!importLayer) {
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
if (importLayer !== currentLayer && !currentLayerDef.allowedDependencies.includes(importLayer)) {
|
|
444
|
-
context.report({
|
|
445
|
-
node,
|
|
446
|
-
messageId: "layerViolation",
|
|
447
|
-
data: {
|
|
448
|
-
fromLayer: currentLayer,
|
|
449
|
-
toLayer: importLayer
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
}
|
|
473
|
+
checkLayerImport(node, context, currentLayer, currentLayerDef, layers);
|
|
453
474
|
}
|
|
454
475
|
};
|
|
455
476
|
}
|
|
@@ -460,6 +481,49 @@ import { ESLintUtils as ESLintUtils5 } from "@typescript-eslint/utils";
|
|
|
460
481
|
var createRule5 = ESLintUtils5.RuleCreator(
|
|
461
482
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
462
483
|
);
|
|
484
|
+
function getAnnotationTarget(node) {
|
|
485
|
+
const parentType = node.parent?.type;
|
|
486
|
+
if (parentType === "ExportNamedDeclaration" || parentType === "VariableDeclaration") {
|
|
487
|
+
return node.parent;
|
|
488
|
+
}
|
|
489
|
+
return node;
|
|
490
|
+
}
|
|
491
|
+
function hasCriticalAnnotation(node, sourceText) {
|
|
492
|
+
const target = getAnnotationTarget(node);
|
|
493
|
+
const startLine = target.loc.start.line;
|
|
494
|
+
const lines = sourceText.split("\n");
|
|
495
|
+
for (let i = Math.max(0, startLine - 2); i < startLine; i++) {
|
|
496
|
+
if (lines[i]?.includes("@perf-critical")) return true;
|
|
497
|
+
}
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
function makeEnterFunction(criticalStack, loopDepthRef, sourceText) {
|
|
501
|
+
return function enterFunction(node) {
|
|
502
|
+
criticalStack.push(hasCriticalAnnotation(node, sourceText));
|
|
503
|
+
loopDepthRef.value = 0;
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function makeExitFunction(criticalStack, loopDepthRef) {
|
|
507
|
+
return function exitFunction() {
|
|
508
|
+
criticalStack.pop();
|
|
509
|
+
loopDepthRef.value = 0;
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function makeEnterLoop(criticalStack, loopDepthRef, reportFn) {
|
|
513
|
+
return function enterLoop(node) {
|
|
514
|
+
if (criticalStack.length === 0 || criticalStack[criticalStack.length - 1] !== true) return;
|
|
515
|
+
loopDepthRef.value++;
|
|
516
|
+
if (loopDepthRef.value > 1) {
|
|
517
|
+
reportFn(node);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function makeExitLoop(criticalStack, loopDepthRef) {
|
|
522
|
+
return function exitLoop() {
|
|
523
|
+
if (criticalStack.length === 0 || criticalStack[criticalStack.length - 1] !== true) return;
|
|
524
|
+
loopDepthRef.value--;
|
|
525
|
+
};
|
|
526
|
+
}
|
|
463
527
|
var no_nested_loops_in_critical_default = createRule5({
|
|
464
528
|
name: "no-nested-loops-in-critical",
|
|
465
529
|
meta: {
|
|
@@ -479,38 +543,14 @@ var no_nested_loops_in_critical_default = createRule5({
|
|
|
479
543
|
return {};
|
|
480
544
|
}
|
|
481
545
|
const criticalStack = [];
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
for (let i = Math.max(0, startLine - 2); i < startLine; i++) {
|
|
491
|
-
if (lines[i]?.includes("@perf-critical")) return true;
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
function enterFunction(node) {
|
|
496
|
-
criticalStack.push(hasCriticalAnnotation(node));
|
|
497
|
-
loopDepth = 0;
|
|
498
|
-
}
|
|
499
|
-
function exitFunction() {
|
|
500
|
-
criticalStack.pop();
|
|
501
|
-
loopDepth = 0;
|
|
502
|
-
}
|
|
503
|
-
function enterLoop(node) {
|
|
504
|
-
if (!isCritical()) return;
|
|
505
|
-
loopDepth++;
|
|
506
|
-
if (loopDepth > 1) {
|
|
507
|
-
context.report({ node, messageId: "nestedLoopInCritical" });
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
function exitLoop() {
|
|
511
|
-
if (!isCritical()) return;
|
|
512
|
-
loopDepth--;
|
|
513
|
-
}
|
|
546
|
+
const loopDepthRef = { value: 0 };
|
|
547
|
+
const reportNestedLoop = (node) => {
|
|
548
|
+
context.report({ node, messageId: "nestedLoopInCritical" });
|
|
549
|
+
};
|
|
550
|
+
const enterFunction = makeEnterFunction(criticalStack, loopDepthRef, sourceText);
|
|
551
|
+
const exitFunction = makeExitFunction(criticalStack, loopDepthRef);
|
|
552
|
+
const enterLoop = makeEnterLoop(criticalStack, loopDepthRef, reportNestedLoop);
|
|
553
|
+
const exitLoop = makeExitLoop(criticalStack, loopDepthRef);
|
|
514
554
|
return {
|
|
515
555
|
FunctionDeclaration: enterFunction,
|
|
516
556
|
"FunctionDeclaration:exit": exitFunction,
|
|
@@ -549,6 +589,32 @@ var SYNC_FS_METHODS = /* @__PURE__ */ new Set([
|
|
|
549
589
|
"renameSync",
|
|
550
590
|
"accessSync"
|
|
551
591
|
]);
|
|
592
|
+
function getSyncMethodName(node) {
|
|
593
|
+
if (node.callee.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.name)) {
|
|
594
|
+
return node.callee.name;
|
|
595
|
+
}
|
|
596
|
+
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.property.name)) {
|
|
597
|
+
return node.callee.property.name;
|
|
598
|
+
}
|
|
599
|
+
return void 0;
|
|
600
|
+
}
|
|
601
|
+
function adjustAsyncDepth(node, depthRef, delta) {
|
|
602
|
+
if (node.async) {
|
|
603
|
+
depthRef.value += delta;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function checkSyncCallInAsync(node, depthRef, reportFn) {
|
|
607
|
+
if (depthRef.value === 0) return;
|
|
608
|
+
const name = getSyncMethodName(node);
|
|
609
|
+
if (name) {
|
|
610
|
+
reportFn(name);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function reportSyncCallExpression(node, context, asyncDepthRef) {
|
|
614
|
+
checkSyncCallInAsync(node, asyncDepthRef, (name) => {
|
|
615
|
+
context.report({ node, messageId: "syncIoInAsync", data: { name } });
|
|
616
|
+
});
|
|
617
|
+
}
|
|
552
618
|
var no_sync_io_in_async_default = createRule6({
|
|
553
619
|
name: "no-sync-io-in-async",
|
|
554
620
|
meta: {
|
|
@@ -563,17 +629,9 @@ var no_sync_io_in_async_default = createRule6({
|
|
|
563
629
|
},
|
|
564
630
|
defaultOptions: [],
|
|
565
631
|
create(context) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
asyncDepth++;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
function exitFunction(node) {
|
|
573
|
-
if (node.async) {
|
|
574
|
-
asyncDepth--;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
632
|
+
const asyncDepthRef = { value: 0 };
|
|
633
|
+
const enterFunction = (node) => adjustAsyncDepth(node, asyncDepthRef, 1);
|
|
634
|
+
const exitFunction = (node) => adjustAsyncDepth(node, asyncDepthRef, -1);
|
|
577
635
|
return {
|
|
578
636
|
FunctionDeclaration: enterFunction,
|
|
579
637
|
"FunctionDeclaration:exit": exitFunction,
|
|
@@ -582,21 +640,7 @@ var no_sync_io_in_async_default = createRule6({
|
|
|
582
640
|
ArrowFunctionExpression: enterFunction,
|
|
583
641
|
"ArrowFunctionExpression:exit": exitFunction,
|
|
584
642
|
CallExpression(node) {
|
|
585
|
-
|
|
586
|
-
let name;
|
|
587
|
-
if (node.callee.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.name)) {
|
|
588
|
-
name = node.callee.name;
|
|
589
|
-
}
|
|
590
|
-
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.property.name)) {
|
|
591
|
-
name = node.callee.property.name;
|
|
592
|
-
}
|
|
593
|
-
if (name) {
|
|
594
|
-
context.report({
|
|
595
|
-
node,
|
|
596
|
-
messageId: "syncIoInAsync",
|
|
597
|
-
data: { name }
|
|
598
|
-
});
|
|
599
|
-
}
|
|
643
|
+
reportSyncCallExpression(node, context, asyncDepthRef);
|
|
600
644
|
}
|
|
601
645
|
};
|
|
602
646
|
}
|
|
@@ -618,6 +662,28 @@ var ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
|
618
662
|
"every",
|
|
619
663
|
"forEach"
|
|
620
664
|
]);
|
|
665
|
+
function isArrayMethodCall(node) {
|
|
666
|
+
return node.property.type === "Identifier" && ARRAY_METHODS.has(node.property.name);
|
|
667
|
+
}
|
|
668
|
+
function isInnerChainLink(node) {
|
|
669
|
+
const callExpr = node.parent;
|
|
670
|
+
if (!callExpr) return false;
|
|
671
|
+
const parentMember = callExpr.parent;
|
|
672
|
+
return parentMember !== null && parentMember !== void 0 && parentMember.type === "MemberExpression" && parentMember.property.type === "Identifier" && ARRAY_METHODS.has(parentMember.property.name);
|
|
673
|
+
}
|
|
674
|
+
function countChainLength(node) {
|
|
675
|
+
let length = 1;
|
|
676
|
+
let current = node;
|
|
677
|
+
while (true) {
|
|
678
|
+
const obj = current.object;
|
|
679
|
+
if (obj.type !== "CallExpression" || obj.callee.type !== "MemberExpression" || obj.callee.property.type !== "Identifier" || !ARRAY_METHODS.has(obj.callee.property.name)) {
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
length++;
|
|
683
|
+
current = obj.callee;
|
|
684
|
+
}
|
|
685
|
+
return length;
|
|
686
|
+
}
|
|
621
687
|
var no_unbounded_array_chains_default = createRule7({
|
|
622
688
|
name: "no-unbounded-array-chains",
|
|
623
689
|
meta: {
|
|
@@ -634,30 +700,11 @@ var no_unbounded_array_chains_default = createRule7({
|
|
|
634
700
|
create(context) {
|
|
635
701
|
return {
|
|
636
702
|
"CallExpression > MemberExpression.callee"(node) {
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
}
|
|
703
|
+
if (!isArrayMethodCall(node)) return;
|
|
704
|
+
if (isInnerChainLink(node)) return;
|
|
640
705
|
const callExpr = node.parent;
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
let chainLength = 1;
|
|
645
|
-
let current = node;
|
|
646
|
-
while (true) {
|
|
647
|
-
const memberExpr = current;
|
|
648
|
-
const obj = memberExpr.object;
|
|
649
|
-
if (obj.type === "CallExpression" && obj.callee.type === "MemberExpression" && obj.callee.property.type === "Identifier" && ARRAY_METHODS.has(obj.callee.property.name)) {
|
|
650
|
-
chainLength++;
|
|
651
|
-
current = obj.callee;
|
|
652
|
-
} else {
|
|
653
|
-
break;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (chainLength >= 3) {
|
|
657
|
-
context.report({
|
|
658
|
-
node: callExpr,
|
|
659
|
-
messageId: "unboundedArrayChain"
|
|
660
|
-
});
|
|
706
|
+
if (countChainLength(node) >= 3) {
|
|
707
|
+
context.report({ node: callExpr, messageId: "unboundedArrayChain" });
|
|
661
708
|
}
|
|
662
709
|
}
|
|
663
710
|
};
|
|
@@ -669,6 +716,25 @@ import { ESLintUtils as ESLintUtils8, AST_NODE_TYPES as AST_NODE_TYPES3 } from "
|
|
|
669
716
|
var createRule8 = ESLintUtils8.RuleCreator(
|
|
670
717
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
671
718
|
);
|
|
719
|
+
function extractFunctionDeclaration(node) {
|
|
720
|
+
const decl = node.declaration;
|
|
721
|
+
if (!decl || decl.type !== AST_NODE_TYPES3.FunctionDeclaration) {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
const fn = decl;
|
|
725
|
+
if (!fn.id || !fn.body) return null;
|
|
726
|
+
return fn;
|
|
727
|
+
}
|
|
728
|
+
function isFileInBoundary(filePath, patterns) {
|
|
729
|
+
return patterns.some((pattern) => matchesPattern(filePath, pattern));
|
|
730
|
+
}
|
|
731
|
+
function checkBoundaryExport(node, report) {
|
|
732
|
+
const fn = extractFunctionDeclaration(node);
|
|
733
|
+
if (!fn) return;
|
|
734
|
+
if (!hasZodValidation(fn.body)) {
|
|
735
|
+
report({ node: fn, messageId: "missingSchema", data: { name: fn.id.name } });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
672
738
|
var require_boundary_schema_default = createRule8({
|
|
673
739
|
name: "require-boundary-schema",
|
|
674
740
|
meta: {
|
|
@@ -688,27 +754,13 @@ var require_boundary_schema_default = createRule8({
|
|
|
688
754
|
return {};
|
|
689
755
|
}
|
|
690
756
|
const filePath = normalizePath2(context.filename);
|
|
691
|
-
|
|
692
|
-
(pattern) => matchesPattern(filePath, pattern)
|
|
693
|
-
);
|
|
694
|
-
if (!isBoundaryFile) {
|
|
757
|
+
if (!isFileInBoundary(filePath, config.boundaries.requireSchema)) {
|
|
695
758
|
return {};
|
|
696
759
|
}
|
|
760
|
+
const report = context.report.bind(context);
|
|
697
761
|
return {
|
|
698
762
|
ExportNamedDeclaration(node) {
|
|
699
|
-
|
|
700
|
-
if (!decl || decl.type !== AST_NODE_TYPES3.FunctionDeclaration) {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
const fn = decl;
|
|
704
|
-
if (!fn.id || !fn.body) return;
|
|
705
|
-
if (!hasZodValidation(fn.body)) {
|
|
706
|
-
context.report({
|
|
707
|
-
node: fn,
|
|
708
|
-
messageId: "missingSchema",
|
|
709
|
-
data: { name: fn.id.name }
|
|
710
|
-
});
|
|
711
|
-
}
|
|
763
|
+
checkBoundaryExport(node, report);
|
|
712
764
|
}
|
|
713
765
|
};
|
|
714
766
|
}
|
|
@@ -724,6 +776,28 @@ var UNIX_COMMAND_PATTERN = new RegExp(
|
|
|
724
776
|
`(?:^|[;&|]\\s*)(?:/(?:usr/)?(?:bin|sbin)/)?(?:${UNIX_COMMANDS.join("|")})(?:\\s|$)`
|
|
725
777
|
);
|
|
726
778
|
var FLAGGED_FUNCTIONS = /* @__PURE__ */ new Set(["exec", "execSync"]);
|
|
779
|
+
function getFlaggedFunctionName(node) {
|
|
780
|
+
if (node.callee.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.name)) {
|
|
781
|
+
return node.callee.name;
|
|
782
|
+
}
|
|
783
|
+
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.property.name)) {
|
|
784
|
+
return node.callee.property.name;
|
|
785
|
+
}
|
|
786
|
+
return void 0;
|
|
787
|
+
}
|
|
788
|
+
function isFlaggedCall(node) {
|
|
789
|
+
return getFlaggedFunctionName(node) !== void 0;
|
|
790
|
+
}
|
|
791
|
+
function extractCommandString(arg) {
|
|
792
|
+
if (!arg) return void 0;
|
|
793
|
+
if (arg.type === "Literal" && typeof arg.value === "string") {
|
|
794
|
+
return arg.value;
|
|
795
|
+
}
|
|
796
|
+
if (arg.type === "TemplateLiteral" && arg.quasis.length > 0) {
|
|
797
|
+
return arg.quasis.map((q) => q.value.raw).join(" ");
|
|
798
|
+
}
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
727
801
|
var no_unix_shell_command_default = createRule9({
|
|
728
802
|
name: "no-unix-shell-command",
|
|
729
803
|
meta: {
|
|
@@ -740,27 +814,10 @@ var no_unix_shell_command_default = createRule9({
|
|
|
740
814
|
create(context) {
|
|
741
815
|
return {
|
|
742
816
|
CallExpression(node) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
functionName = node.callee.name;
|
|
746
|
-
}
|
|
747
|
-
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.property.name)) {
|
|
748
|
-
functionName = node.callee.property.name;
|
|
749
|
-
}
|
|
750
|
-
if (!functionName) return;
|
|
751
|
-
const firstArg = node.arguments[0];
|
|
752
|
-
if (!firstArg) return;
|
|
753
|
-
let commandString;
|
|
754
|
-
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
|
|
755
|
-
commandString = firstArg.value;
|
|
756
|
-
} else if (firstArg.type === "TemplateLiteral" && firstArg.quasis.length > 0) {
|
|
757
|
-
commandString = firstArg.quasis.map((q) => q.value.raw).join(" ");
|
|
758
|
-
}
|
|
817
|
+
if (!isFlaggedCall(node)) return;
|
|
818
|
+
const commandString = extractCommandString(node.arguments[0]);
|
|
759
819
|
if (commandString && UNIX_COMMAND_PATTERN.test(commandString)) {
|
|
760
|
-
context.report({
|
|
761
|
-
node,
|
|
762
|
-
messageId: "unixShellCommand"
|
|
763
|
-
});
|
|
820
|
+
context.report({ node, messageId: "unixShellCommand" });
|
|
764
821
|
}
|
|
765
822
|
}
|
|
766
823
|
};
|
|
@@ -954,7 +1011,7 @@ var rules = {
|
|
|
954
1011
|
var plugin = {
|
|
955
1012
|
meta: {
|
|
956
1013
|
name: "@harness-engineering/eslint-plugin",
|
|
957
|
-
version: "0.
|
|
1014
|
+
version: "0.2.4"
|
|
958
1015
|
},
|
|
959
1016
|
rules,
|
|
960
1017
|
configs: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/eslint-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "ESLint plugin for harness engineering architectural constraints",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,18 +16,19 @@
|
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@typescript-eslint/utils": "^8.
|
|
20
|
-
"minimatch": "^10.2.
|
|
21
|
-
"zod": "^3.
|
|
19
|
+
"@typescript-eslint/utils": "^8.58.0",
|
|
20
|
+
"minimatch": "^10.2.5",
|
|
21
|
+
"zod": "^3.25.76"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^22.
|
|
25
|
-
"@typescript-eslint/parser": "^8.
|
|
26
|
-
"@typescript-eslint/rule-tester": "^8.
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
24
|
+
"@types/node": "^22.19.15",
|
|
25
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
26
|
+
"@typescript-eslint/rule-tester": "^8.58.0",
|
|
27
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
28
|
+
"eslint": "^10.1.0",
|
|
29
|
+
"tsup": "^8.5.1",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vitest": "^4.1.2"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
"test:watch": "vitest",
|
|
60
61
|
"lint": "eslint src",
|
|
61
62
|
"typecheck": "tsc --noEmit",
|
|
62
|
-
"clean": "node ../../scripts/clean.mjs dist"
|
|
63
|
+
"clean": "node ../../scripts/clean.mjs dist",
|
|
64
|
+
"test:coverage": "vitest run --coverage"
|
|
63
65
|
}
|
|
64
66
|
}
|