@harness-engineering/eslint-plugin 0.2.2 → 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 +324 -256
- package/package.json +14 -12
package/dist/index.js
CHANGED
|
@@ -3,54 +3,64 @@ 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 lastNonEmptyLineEndsBlockComment(lines) {
|
|
7
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
8
|
+
const line = lines[i]?.trim() ?? "";
|
|
9
|
+
if (line === "") continue;
|
|
10
|
+
return line.endsWith("*/");
|
|
11
|
+
}
|
|
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
|
+
}
|
|
6
19
|
function hasJSDocComment(node, sourceCode) {
|
|
7
20
|
if (!node.range) return false;
|
|
8
21
|
const textBefore = sourceCode.slice(0, node.range[0]);
|
|
9
22
|
const lines = textBefore.split("\n");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
if (!lastNonEmptyLineEndsBlockComment(lines)) return false;
|
|
24
|
+
return textContainsJSDoc(textBefore);
|
|
25
|
+
}
|
|
26
|
+
var SKIP_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
|
|
27
|
+
var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set(["parse", "safeParse"]);
|
|
28
|
+
function isZodParseCall(node) {
|
|
29
|
+
if (node.type !== AST_NODE_TYPES.CallExpression) return false;
|
|
30
|
+
const callee = node.callee;
|
|
31
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
|
|
32
|
+
const prop = callee.property;
|
|
33
|
+
if (prop.type !== AST_NODE_TYPES.Identifier) return false;
|
|
34
|
+
return ZOD_PARSE_METHODS.has(prop.name);
|
|
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
|
+
}
|
|
43
|
+
function visitChildren(node, visitor) {
|
|
44
|
+
for (const key of Object.keys(node)) {
|
|
45
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
46
|
+
const value = node[key];
|
|
47
|
+
if (!value || typeof value !== "object") continue;
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
visitArrayItems(value, visitor);
|
|
50
|
+
} else if ("type" in value) {
|
|
51
|
+
visitor(value);
|
|
21
52
|
}
|
|
22
|
-
break;
|
|
23
53
|
}
|
|
24
|
-
return foundJSDoc;
|
|
25
54
|
}
|
|
26
55
|
function hasZodValidation(body) {
|
|
27
56
|
let found = false;
|
|
28
|
-
const skipKeys = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
|
|
29
57
|
function visit(node) {
|
|
30
58
|
if (found) return;
|
|
31
|
-
if (node
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (propType === AST_NODE_TYPES.Identifier && (prop.name === "parse" || prop.name === "safeParse")) {
|
|
35
|
-
found = true;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
for (const key of Object.keys(node)) {
|
|
40
|
-
if (skipKeys.has(key)) continue;
|
|
41
|
-
const value = node[key];
|
|
42
|
-
if (value && typeof value === "object") {
|
|
43
|
-
if (Array.isArray(value)) {
|
|
44
|
-
for (const item of value) {
|
|
45
|
-
if (item && typeof item === "object" && "type" in item) {
|
|
46
|
-
visit(item);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
} else if ("type" in value) {
|
|
50
|
-
visit(value);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
59
|
+
if (isZodParseCall(node)) {
|
|
60
|
+
found = true;
|
|
61
|
+
return;
|
|
53
62
|
}
|
|
63
|
+
visitChildren(node, visit);
|
|
54
64
|
}
|
|
55
65
|
visit(body);
|
|
56
66
|
return found;
|
|
@@ -70,6 +80,43 @@ function isMarkedInternal(node, sourceCode) {
|
|
|
70
80
|
var createRule = ESLintUtils.RuleCreator(
|
|
71
81
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
72
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
|
+
}
|
|
73
120
|
var enforce_doc_exports_default = createRule({
|
|
74
121
|
name: "enforce-doc-exports",
|
|
75
122
|
meta: {
|
|
@@ -94,43 +141,12 @@ var enforce_doc_exports_default = createRule({
|
|
|
94
141
|
defaultOptions: [{ ignoreTypes: false, ignoreInternal: true }],
|
|
95
142
|
create(context, [options]) {
|
|
96
143
|
const sourceCode = context.sourceCode.getText();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (!hasJSDocComment(node, sourceCode)) {
|
|
102
|
-
context.report({
|
|
103
|
-
node,
|
|
104
|
-
messageId: "missingJSDoc",
|
|
105
|
-
data: { kind, name }
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
144
|
+
const ignoreTypes = options.ignoreTypes ?? false;
|
|
145
|
+
const ignoreInternal = options.ignoreInternal ?? true;
|
|
146
|
+
const report = context.report.bind(context);
|
|
109
147
|
return {
|
|
110
148
|
ExportNamedDeclaration(node) {
|
|
111
|
-
|
|
112
|
-
if (!decl) return;
|
|
113
|
-
const declType = decl.type;
|
|
114
|
-
if (declType === AST_NODE_TYPES2.FunctionDeclaration) {
|
|
115
|
-
const fn = decl;
|
|
116
|
-
if (fn.id) checkExport(node, "function", fn.id.name);
|
|
117
|
-
} else if (declType === AST_NODE_TYPES2.ClassDeclaration) {
|
|
118
|
-
const cls = decl;
|
|
119
|
-
if (cls.id) checkExport(node, "class", cls.id.name);
|
|
120
|
-
} else if (declType === AST_NODE_TYPES2.VariableDeclaration) {
|
|
121
|
-
const varDecl = decl;
|
|
122
|
-
for (const declarator of varDecl.declarations) {
|
|
123
|
-
if (declarator.id.type === AST_NODE_TYPES2.Identifier) {
|
|
124
|
-
checkExport(node, "variable", declarator.id.name);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
} else if (declType === AST_NODE_TYPES2.TSTypeAliasDeclaration && !options.ignoreTypes) {
|
|
128
|
-
const typeAlias = decl;
|
|
129
|
-
checkExport(node, "type", typeAlias.id.name);
|
|
130
|
-
} else if (declType === AST_NODE_TYPES2.TSInterfaceDeclaration && !options.ignoreTypes) {
|
|
131
|
-
const iface = decl;
|
|
132
|
-
checkExport(node, "interface", iface.id.name);
|
|
133
|
-
}
|
|
149
|
+
handleExportNamedDeclaration(node, ignoreTypes, sourceCode, ignoreInternal, report);
|
|
134
150
|
}
|
|
135
151
|
};
|
|
136
152
|
}
|
|
@@ -185,6 +201,24 @@ function normalizePath(filePath) {
|
|
|
185
201
|
}
|
|
186
202
|
return path.basename(filePath);
|
|
187
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
|
+
}
|
|
188
222
|
var no_circular_deps_default = createRule2({
|
|
189
223
|
name: "no-circular-deps",
|
|
190
224
|
meta: {
|
|
@@ -202,24 +236,7 @@ var no_circular_deps_default = createRule2({
|
|
|
202
236
|
const currentFile = normalizePath(context.filename);
|
|
203
237
|
return {
|
|
204
238
|
ImportDeclaration(node) {
|
|
205
|
-
|
|
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: {
|
|
218
|
-
cycle: cycle.map((f) => path.basename(f)).join(" \u2192 ")
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
addEdge(currentFile, normalizedImport);
|
|
239
|
+
checkImportDeclaration(node, context, currentFile);
|
|
223
240
|
}
|
|
224
241
|
};
|
|
225
242
|
}
|
|
@@ -337,6 +354,34 @@ function normalizePath2(filePath) {
|
|
|
337
354
|
var createRule3 = ESLintUtils3.RuleCreator(
|
|
338
355
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
339
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
|
+
}
|
|
340
385
|
var no_forbidden_imports_default = createRule3({
|
|
341
386
|
name: "no-forbidden-imports",
|
|
342
387
|
meta: {
|
|
@@ -362,25 +407,10 @@ var no_forbidden_imports_default = createRule3({
|
|
|
362
407
|
if (applicableRules.length === 0) {
|
|
363
408
|
return {};
|
|
364
409
|
}
|
|
410
|
+
const report = context.report.bind(context);
|
|
365
411
|
return {
|
|
366
412
|
ImportDeclaration(node) {
|
|
367
|
-
|
|
368
|
-
const resolvedImport = resolveImportPath(importPath, context.filename);
|
|
369
|
-
for (const rule of applicableRules) {
|
|
370
|
-
for (const disallowed of rule.disallow) {
|
|
371
|
-
const isMatch = importPath === disallowed || matchesPattern(resolvedImport, disallowed) || matchesPattern(importPath, disallowed);
|
|
372
|
-
if (isMatch) {
|
|
373
|
-
context.report({
|
|
374
|
-
node,
|
|
375
|
-
messageId: "forbiddenImport",
|
|
376
|
-
data: {
|
|
377
|
-
message: rule.message || `Import "${importPath}" is forbidden in files matching "${rule.from}"`
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
413
|
+
checkImportDeclaration2(node, applicableRules, context.filename, report);
|
|
384
414
|
}
|
|
385
415
|
};
|
|
386
416
|
}
|
|
@@ -391,6 +421,25 @@ import { ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
|
|
|
391
421
|
var createRule4 = ESLintUtils4.RuleCreator(
|
|
392
422
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
393
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
|
+
}
|
|
394
443
|
var no_layer_violation_default = createRule4({
|
|
395
444
|
name: "no-layer-violation",
|
|
396
445
|
meta: {
|
|
@@ -418,27 +467,10 @@ var no_layer_violation_default = createRule4({
|
|
|
418
467
|
if (!currentLayerDef) {
|
|
419
468
|
return {};
|
|
420
469
|
}
|
|
470
|
+
const layers = config.layers;
|
|
421
471
|
return {
|
|
422
472
|
ImportDeclaration(node) {
|
|
423
|
-
|
|
424
|
-
if (!importPath.startsWith(".")) {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
const resolvedImport = resolveImportPath(importPath, context.filename);
|
|
428
|
-
const importLayer = getLayerForFile(resolvedImport, config.layers);
|
|
429
|
-
if (!importLayer) {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (importLayer !== currentLayer && !currentLayerDef.allowedDependencies.includes(importLayer)) {
|
|
433
|
-
context.report({
|
|
434
|
-
node,
|
|
435
|
-
messageId: "layerViolation",
|
|
436
|
-
data: {
|
|
437
|
-
fromLayer: currentLayer,
|
|
438
|
-
toLayer: importLayer
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
}
|
|
473
|
+
checkLayerImport(node, context, currentLayer, currentLayerDef, layers);
|
|
442
474
|
}
|
|
443
475
|
};
|
|
444
476
|
}
|
|
@@ -449,6 +481,49 @@ import { ESLintUtils as ESLintUtils5 } from "@typescript-eslint/utils";
|
|
|
449
481
|
var createRule5 = ESLintUtils5.RuleCreator(
|
|
450
482
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
451
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
|
+
}
|
|
452
527
|
var no_nested_loops_in_critical_default = createRule5({
|
|
453
528
|
name: "no-nested-loops-in-critical",
|
|
454
529
|
meta: {
|
|
@@ -468,38 +543,14 @@ var no_nested_loops_in_critical_default = createRule5({
|
|
|
468
543
|
return {};
|
|
469
544
|
}
|
|
470
545
|
const criticalStack = [];
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
for (let i = Math.max(0, startLine - 2); i < startLine; i++) {
|
|
480
|
-
if (lines[i]?.includes("@perf-critical")) return true;
|
|
481
|
-
}
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
function enterFunction(node) {
|
|
485
|
-
criticalStack.push(hasCriticalAnnotation(node));
|
|
486
|
-
loopDepth = 0;
|
|
487
|
-
}
|
|
488
|
-
function exitFunction() {
|
|
489
|
-
criticalStack.pop();
|
|
490
|
-
loopDepth = 0;
|
|
491
|
-
}
|
|
492
|
-
function enterLoop(node) {
|
|
493
|
-
if (!isCritical()) return;
|
|
494
|
-
loopDepth++;
|
|
495
|
-
if (loopDepth > 1) {
|
|
496
|
-
context.report({ node, messageId: "nestedLoopInCritical" });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
function exitLoop() {
|
|
500
|
-
if (!isCritical()) return;
|
|
501
|
-
loopDepth--;
|
|
502
|
-
}
|
|
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);
|
|
503
554
|
return {
|
|
504
555
|
FunctionDeclaration: enterFunction,
|
|
505
556
|
"FunctionDeclaration:exit": exitFunction,
|
|
@@ -538,6 +589,32 @@ var SYNC_FS_METHODS = /* @__PURE__ */ new Set([
|
|
|
538
589
|
"renameSync",
|
|
539
590
|
"accessSync"
|
|
540
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
|
+
}
|
|
541
618
|
var no_sync_io_in_async_default = createRule6({
|
|
542
619
|
name: "no-sync-io-in-async",
|
|
543
620
|
meta: {
|
|
@@ -552,17 +629,9 @@ var no_sync_io_in_async_default = createRule6({
|
|
|
552
629
|
},
|
|
553
630
|
defaultOptions: [],
|
|
554
631
|
create(context) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
asyncDepth++;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
function exitFunction(node) {
|
|
562
|
-
if (node.async) {
|
|
563
|
-
asyncDepth--;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
632
|
+
const asyncDepthRef = { value: 0 };
|
|
633
|
+
const enterFunction = (node) => adjustAsyncDepth(node, asyncDepthRef, 1);
|
|
634
|
+
const exitFunction = (node) => adjustAsyncDepth(node, asyncDepthRef, -1);
|
|
566
635
|
return {
|
|
567
636
|
FunctionDeclaration: enterFunction,
|
|
568
637
|
"FunctionDeclaration:exit": exitFunction,
|
|
@@ -571,21 +640,7 @@ var no_sync_io_in_async_default = createRule6({
|
|
|
571
640
|
ArrowFunctionExpression: enterFunction,
|
|
572
641
|
"ArrowFunctionExpression:exit": exitFunction,
|
|
573
642
|
CallExpression(node) {
|
|
574
|
-
|
|
575
|
-
let name;
|
|
576
|
-
if (node.callee.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.name)) {
|
|
577
|
-
name = node.callee.name;
|
|
578
|
-
}
|
|
579
|
-
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.property.name)) {
|
|
580
|
-
name = node.callee.property.name;
|
|
581
|
-
}
|
|
582
|
-
if (name) {
|
|
583
|
-
context.report({
|
|
584
|
-
node,
|
|
585
|
-
messageId: "syncIoInAsync",
|
|
586
|
-
data: { name }
|
|
587
|
-
});
|
|
588
|
-
}
|
|
643
|
+
reportSyncCallExpression(node, context, asyncDepthRef);
|
|
589
644
|
}
|
|
590
645
|
};
|
|
591
646
|
}
|
|
@@ -607,6 +662,28 @@ var ARRAY_METHODS = /* @__PURE__ */ new Set([
|
|
|
607
662
|
"every",
|
|
608
663
|
"forEach"
|
|
609
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
|
+
}
|
|
610
687
|
var no_unbounded_array_chains_default = createRule7({
|
|
611
688
|
name: "no-unbounded-array-chains",
|
|
612
689
|
meta: {
|
|
@@ -623,30 +700,11 @@ var no_unbounded_array_chains_default = createRule7({
|
|
|
623
700
|
create(context) {
|
|
624
701
|
return {
|
|
625
702
|
"CallExpression > MemberExpression.callee"(node) {
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
}
|
|
703
|
+
if (!isArrayMethodCall(node)) return;
|
|
704
|
+
if (isInnerChainLink(node)) return;
|
|
629
705
|
const callExpr = node.parent;
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
let chainLength = 1;
|
|
634
|
-
let current = node;
|
|
635
|
-
while (true) {
|
|
636
|
-
const memberExpr = current;
|
|
637
|
-
const obj = memberExpr.object;
|
|
638
|
-
if (obj.type === "CallExpression" && obj.callee.type === "MemberExpression" && obj.callee.property.type === "Identifier" && ARRAY_METHODS.has(obj.callee.property.name)) {
|
|
639
|
-
chainLength++;
|
|
640
|
-
current = obj.callee;
|
|
641
|
-
} else {
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if (chainLength >= 3) {
|
|
646
|
-
context.report({
|
|
647
|
-
node: callExpr,
|
|
648
|
-
messageId: "unboundedArrayChain"
|
|
649
|
-
});
|
|
706
|
+
if (countChainLength(node) >= 3) {
|
|
707
|
+
context.report({ node: callExpr, messageId: "unboundedArrayChain" });
|
|
650
708
|
}
|
|
651
709
|
}
|
|
652
710
|
};
|
|
@@ -658,6 +716,25 @@ import { ESLintUtils as ESLintUtils8, AST_NODE_TYPES as AST_NODE_TYPES3 } from "
|
|
|
658
716
|
var createRule8 = ESLintUtils8.RuleCreator(
|
|
659
717
|
(name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
|
|
660
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
|
+
}
|
|
661
738
|
var require_boundary_schema_default = createRule8({
|
|
662
739
|
name: "require-boundary-schema",
|
|
663
740
|
meta: {
|
|
@@ -677,27 +754,13 @@ var require_boundary_schema_default = createRule8({
|
|
|
677
754
|
return {};
|
|
678
755
|
}
|
|
679
756
|
const filePath = normalizePath2(context.filename);
|
|
680
|
-
|
|
681
|
-
(pattern) => matchesPattern(filePath, pattern)
|
|
682
|
-
);
|
|
683
|
-
if (!isBoundaryFile) {
|
|
757
|
+
if (!isFileInBoundary(filePath, config.boundaries.requireSchema)) {
|
|
684
758
|
return {};
|
|
685
759
|
}
|
|
760
|
+
const report = context.report.bind(context);
|
|
686
761
|
return {
|
|
687
762
|
ExportNamedDeclaration(node) {
|
|
688
|
-
|
|
689
|
-
if (!decl || decl.type !== AST_NODE_TYPES3.FunctionDeclaration) {
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
const fn = decl;
|
|
693
|
-
if (!fn.id || !fn.body) return;
|
|
694
|
-
if (!hasZodValidation(fn.body)) {
|
|
695
|
-
context.report({
|
|
696
|
-
node: fn,
|
|
697
|
-
messageId: "missingSchema",
|
|
698
|
-
data: { name: fn.id.name }
|
|
699
|
-
});
|
|
700
|
-
}
|
|
763
|
+
checkBoundaryExport(node, report);
|
|
701
764
|
}
|
|
702
765
|
};
|
|
703
766
|
}
|
|
@@ -713,6 +776,28 @@ var UNIX_COMMAND_PATTERN = new RegExp(
|
|
|
713
776
|
`(?:^|[;&|]\\s*)(?:/(?:usr/)?(?:bin|sbin)/)?(?:${UNIX_COMMANDS.join("|")})(?:\\s|$)`
|
|
714
777
|
);
|
|
715
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
|
+
}
|
|
716
801
|
var no_unix_shell_command_default = createRule9({
|
|
717
802
|
name: "no-unix-shell-command",
|
|
718
803
|
meta: {
|
|
@@ -729,27 +814,10 @@ var no_unix_shell_command_default = createRule9({
|
|
|
729
814
|
create(context) {
|
|
730
815
|
return {
|
|
731
816
|
CallExpression(node) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
functionName = node.callee.name;
|
|
735
|
-
}
|
|
736
|
-
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.property.name)) {
|
|
737
|
-
functionName = node.callee.property.name;
|
|
738
|
-
}
|
|
739
|
-
if (!functionName) return;
|
|
740
|
-
const firstArg = node.arguments[0];
|
|
741
|
-
if (!firstArg) return;
|
|
742
|
-
let commandString;
|
|
743
|
-
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
|
|
744
|
-
commandString = firstArg.value;
|
|
745
|
-
} else if (firstArg.type === "TemplateLiteral" && firstArg.quasis.length > 0) {
|
|
746
|
-
commandString = firstArg.quasis.map((q) => q.value.raw).join(" ");
|
|
747
|
-
}
|
|
817
|
+
if (!isFlaggedCall(node)) return;
|
|
818
|
+
const commandString = extractCommandString(node.arguments[0]);
|
|
748
819
|
if (commandString && UNIX_COMMAND_PATTERN.test(commandString)) {
|
|
749
|
-
context.report({
|
|
750
|
-
node,
|
|
751
|
-
messageId: "unixShellCommand"
|
|
752
|
-
});
|
|
820
|
+
context.report({ node, messageId: "unixShellCommand" });
|
|
753
821
|
}
|
|
754
822
|
}
|
|
755
823
|
};
|
|
@@ -819,21 +887,21 @@ function isImportOrRequire(node) {
|
|
|
819
887
|
}
|
|
820
888
|
return false;
|
|
821
889
|
}
|
|
890
|
+
function isMemberCall(callee, objectNames, methods) {
|
|
891
|
+
return callee.object.type === "Identifier" && objectNames.has(callee.object.name) && callee.property.type === "Identifier" && methods.has(callee.property.name);
|
|
892
|
+
}
|
|
893
|
+
var PATH_OBJECTS = /* @__PURE__ */ new Set(["path"]);
|
|
894
|
+
var FS_OBJECTS = /* @__PURE__ */ new Set(["fs", "fsp"]);
|
|
822
895
|
function isInFlaggedContext(node) {
|
|
823
896
|
const parent = node.parent;
|
|
824
897
|
if (!parent) return false;
|
|
825
|
-
if (parent.type
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && STRING_METHODS.has(callee.property.name)) {
|
|
834
|
-
return true;
|
|
835
|
-
}
|
|
836
|
-
return false;
|
|
898
|
+
if (parent.type !== "CallExpression") return false;
|
|
899
|
+
const callee = parent.callee;
|
|
900
|
+
if (callee.type !== "MemberExpression") return false;
|
|
901
|
+
if (isMemberCall(callee, PATH_OBJECTS, PATH_METHODS)) return true;
|
|
902
|
+
if (isMemberCall(callee, FS_OBJECTS, FS_METHODS)) return true;
|
|
903
|
+
if (callee.property.type === "Identifier" && STRING_METHODS.has(callee.property.name)) {
|
|
904
|
+
return true;
|
|
837
905
|
}
|
|
838
906
|
return false;
|
|
839
907
|
}
|
|
@@ -943,7 +1011,7 @@ var rules = {
|
|
|
943
1011
|
var plugin = {
|
|
944
1012
|
meta: {
|
|
945
1013
|
name: "@harness-engineering/eslint-plugin",
|
|
946
|
-
version: "0.
|
|
1014
|
+
version: "0.2.4"
|
|
947
1015
|
},
|
|
948
1016
|
rules,
|
|
949
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
|
}
|