@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.
Files changed (2) hide show
  1. package/dist/index.js +289 -232
  2. 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 hasJSDocComment(node, sourceCode) {
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
- 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;
21
- }
22
- break;
10
+ return line.endsWith("*/");
23
11
  }
24
- return foundJSDoc;
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
- for (const item of value) {
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
- function checkExport(node, kind, name) {
105
- if (options.ignoreInternal && isMarkedInternal(node, sourceCode)) {
106
- return;
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
- const decl = node.declaration;
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
- const importPath = node.source.value;
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
- const importPath = node.source.value;
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
- const importPath = node.source.value;
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
- let loopDepth = 0;
483
- function isCritical() {
484
- return criticalStack.length > 0 && criticalStack[criticalStack.length - 1] === true;
485
- }
486
- function hasCriticalAnnotation(node) {
487
- const target = node.parent?.type === "ExportNamedDeclaration" || node.parent?.type === "VariableDeclaration" ? node.parent : node;
488
- const startLine = target.loc.start.line;
489
- const lines = sourceText.split("\n");
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
- let asyncDepth = 0;
567
- function enterFunction(node) {
568
- if (node.async) {
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
- if (asyncDepth === 0) return;
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 (node.property.type !== "Identifier" || !ARRAY_METHODS.has(node.property.name)) {
638
- return;
639
- }
703
+ if (!isArrayMethodCall(node)) return;
704
+ if (isInnerChainLink(node)) return;
640
705
  const callExpr = node.parent;
641
- if (callExpr.parent && callExpr.parent.type === "MemberExpression" && callExpr.parent.property.type === "Identifier" && ARRAY_METHODS.has(callExpr.parent.property.name)) {
642
- return;
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
- const isBoundaryFile = config.boundaries.requireSchema.some(
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
- const decl = node.declaration;
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
- let functionName;
744
- if (node.callee.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.name)) {
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.1.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",
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
  }