@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.
Files changed (2) hide show
  1. package/dist/index.js +324 -256
  2. 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
- let foundJSDoc = false;
11
- for (let i = lines.length - 1; i >= 0; i--) {
12
- const line = lines[i]?.trim() ?? "";
13
- if (line === "") continue;
14
- if (line.endsWith("*/")) {
15
- const startIdx = textBefore.lastIndexOf("/**");
16
- const endIdx = textBefore.lastIndexOf("*/");
17
- if (startIdx !== -1 && endIdx > startIdx) {
18
- foundJSDoc = true;
19
- }
20
- break;
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.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression) {
32
- const prop = node.callee.property;
33
- const propType = prop.type;
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
- function checkExport(node, kind, name) {
98
- if (options.ignoreInternal && isMarkedInternal(node, sourceCode)) {
99
- return;
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
- const decl = node.declaration;
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
- 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: {
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
- const importPath = node.source.value;
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
- const importPath = node.source.value;
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
- let loopDepth = 0;
472
- function isCritical() {
473
- return criticalStack.length > 0 && criticalStack[criticalStack.length - 1] === true;
474
- }
475
- function hasCriticalAnnotation(node) {
476
- const target = node.parent?.type === "ExportNamedDeclaration" || node.parent?.type === "VariableDeclaration" ? node.parent : node;
477
- const startLine = target.loc.start.line;
478
- const lines = sourceText.split("\n");
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
- let asyncDepth = 0;
556
- function enterFunction(node) {
557
- if (node.async) {
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
- if (asyncDepth === 0) return;
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 (node.property.type !== "Identifier" || !ARRAY_METHODS.has(node.property.name)) {
627
- return;
628
- }
703
+ if (!isArrayMethodCall(node)) return;
704
+ if (isInnerChainLink(node)) return;
629
705
  const callExpr = node.parent;
630
- if (callExpr.parent && callExpr.parent.type === "MemberExpression" && callExpr.parent.property.type === "Identifier" && ARRAY_METHODS.has(callExpr.parent.property.name)) {
631
- return;
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
- const isBoundaryFile = config.boundaries.requireSchema.some(
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
- const decl = node.declaration;
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
- let functionName;
733
- if (node.callee.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.name)) {
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 === "CallExpression") {
826
- const callee = parent.callee;
827
- if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && PATH_METHODS.has(callee.property.name)) {
828
- return true;
829
- }
830
- if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && (callee.object.name === "fs" || callee.object.name === "fsp") && callee.property.type === "Identifier" && FS_METHODS.has(callee.property.name)) {
831
- return true;
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.1.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.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.0.0",
20
- "minimatch": "^10.2.4",
21
- "zod": "^3.22.0"
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.0.0",
25
- "@typescript-eslint/parser": "^8.57.0",
26
- "@typescript-eslint/rule-tester": "^8.0.0",
27
- "eslint": "^10.0.0",
28
- "tsup": "^8.0.0",
29
- "typescript": "^5.3.3",
30
- "vitest": "^4.0.18"
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
  }