@harness-engineering/eslint-plugin 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.js +354 -234
  3. package/package.json +14 -12
package/dist/index.d.ts CHANGED
@@ -22,6 +22,9 @@ declare const rules: {
22
22
  'no-nested-loops-in-critical': _typescript_eslint_utils_ts_eslint.RuleModule<"nestedLoopInCritical", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
23
23
  name: string;
24
24
  };
25
+ 'no-process-env-in-spawn': _typescript_eslint_utils_ts_eslint.RuleModule<"processEnvInSpawn", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
26
+ name: string;
27
+ };
25
28
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
26
29
  name: string;
27
30
  };
@@ -66,6 +69,9 @@ declare const plugin: {
66
69
  'no-nested-loops-in-critical': _typescript_eslint_utils_ts_eslint.RuleModule<"nestedLoopInCritical", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
67
70
  name: string;
68
71
  };
72
+ 'no-process-env-in-spawn': _typescript_eslint_utils_ts_eslint.RuleModule<"processEnvInSpawn", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
73
+ name: string;
74
+ };
69
75
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
70
76
  name: string;
71
77
  };
@@ -95,6 +101,7 @@ declare const plugin: {
95
101
  '@harness-engineering/enforce-doc-exports': string;
96
102
  '@harness-engineering/no-unix-shell-command': string;
97
103
  '@harness-engineering/no-hardcoded-path-separator': string;
104
+ '@harness-engineering/no-process-env-in-spawn': string;
98
105
  '@harness-engineering/require-path-normalization': string;
99
106
  };
100
107
  };
@@ -110,6 +117,7 @@ declare const plugin: {
110
117
  '@harness-engineering/enforce-doc-exports': string;
111
118
  '@harness-engineering/no-unix-shell-command': string;
112
119
  '@harness-engineering/no-hardcoded-path-separator': string;
120
+ '@harness-engineering/no-process-env-in-spawn': string;
113
121
  '@harness-engineering/require-path-normalization': string;
114
122
  };
115
123
  };
@@ -146,6 +154,9 @@ declare const configs: {
146
154
  'no-nested-loops-in-critical': _typescript_eslint_utils_ts_eslint.RuleModule<"nestedLoopInCritical", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
147
155
  name: string;
148
156
  };
157
+ 'no-process-env-in-spawn': _typescript_eslint_utils_ts_eslint.RuleModule<"processEnvInSpawn", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
158
+ name: string;
159
+ };
149
160
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
150
161
  name: string;
151
162
  };
@@ -173,6 +184,7 @@ declare const configs: {
173
184
  '@harness-engineering/enforce-doc-exports': string;
174
185
  '@harness-engineering/no-unix-shell-command': string;
175
186
  '@harness-engineering/no-hardcoded-path-separator': string;
187
+ '@harness-engineering/no-process-env-in-spawn': string;
176
188
  '@harness-engineering/require-path-normalization': string;
177
189
  };
178
190
  };
@@ -205,6 +217,9 @@ declare const configs: {
205
217
  'no-nested-loops-in-critical': _typescript_eslint_utils_ts_eslint.RuleModule<"nestedLoopInCritical", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
206
218
  name: string;
207
219
  };
220
+ 'no-process-env-in-spawn': _typescript_eslint_utils_ts_eslint.RuleModule<"processEnvInSpawn", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
221
+ name: string;
222
+ };
208
223
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
209
224
  name: string;
210
225
  };
@@ -232,6 +247,7 @@ declare const configs: {
232
247
  '@harness-engineering/enforce-doc-exports': string;
233
248
  '@harness-engineering/no-unix-shell-command': string;
234
249
  '@harness-engineering/no-hardcoded-path-separator': string;
250
+ '@harness-engineering/no-process-env-in-spawn': string;
235
251
  '@harness-engineering/require-path-normalization': string;
236
252
  };
237
253
  };
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
  };
@@ -878,11 +935,71 @@ var no_hardcoded_path_separator_default = createRule10({
878
935
  }
879
936
  });
880
937
 
881
- // src/rules/require-path-normalization.ts
938
+ // src/rules/no-process-env-in-spawn.ts
882
939
  import { ESLintUtils as ESLintUtils11 } from "@typescript-eslint/utils";
883
940
  var createRule11 = ESLintUtils11.RuleCreator(
884
941
  (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
885
942
  );
943
+ var SPAWN_FUNCTIONS = /* @__PURE__ */ new Set(["spawn", "spawnSync", "execFile", "execFileSync", "fork"]);
944
+ function getSpawnFunctionName(node) {
945
+ if (node.callee.type === "Identifier" && SPAWN_FUNCTIONS.has(node.callee.name)) {
946
+ return node.callee.name;
947
+ }
948
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && SPAWN_FUNCTIONS.has(node.callee.property.name)) {
949
+ return node.callee.property.name;
950
+ }
951
+ return void 0;
952
+ }
953
+ function isProcessEnv(node) {
954
+ return node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env";
955
+ }
956
+ function hasProcessEnvProperty(node) {
957
+ for (const prop of node.properties) {
958
+ if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "env" && isProcessEnv(prop.value)) {
959
+ return prop;
960
+ }
961
+ if (prop.type === "SpreadElement" && isProcessEnv(prop.argument)) {
962
+ return prop;
963
+ }
964
+ }
965
+ return void 0;
966
+ }
967
+ var no_process_env_in_spawn_default = createRule11({
968
+ name: "no-process-env-in-spawn",
969
+ meta: {
970
+ type: "problem",
971
+ docs: {
972
+ description: "Disallow passing process.env directly to spawn/execFile/fork, which leaks all server-side secrets to child processes"
973
+ },
974
+ messages: {
975
+ processEnvInSpawn: "Do not pass process.env directly to '{{name}}'. Build an explicit env object with only the variables the child process needs."
976
+ },
977
+ schema: []
978
+ },
979
+ defaultOptions: [],
980
+ create(context) {
981
+ return {
982
+ CallExpression(node) {
983
+ const name = getSpawnFunctionName(node);
984
+ if (!name) return;
985
+ for (const arg of node.arguments) {
986
+ if (arg.type === "ObjectExpression") {
987
+ const envProp = hasProcessEnvProperty(arg);
988
+ if (envProp) {
989
+ context.report({ node: envProp, messageId: "processEnvInSpawn", data: { name } });
990
+ }
991
+ }
992
+ }
993
+ }
994
+ };
995
+ }
996
+ });
997
+
998
+ // src/rules/require-path-normalization.ts
999
+ import { ESLintUtils as ESLintUtils12 } from "@typescript-eslint/utils";
1000
+ var createRule12 = ESLintUtils12.RuleCreator(
1001
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
1002
+ );
886
1003
  function isRelativeCall(node) {
887
1004
  const callee = node.callee;
888
1005
  if (callee.type === "Identifier" && callee.name === "relative") {
@@ -908,7 +1025,7 @@ function hasNormalization(node) {
908
1025
  if (grandparent?.type !== "CallExpression" || grandparent.arguments.length < 1) return false;
909
1026
  return isBackslashNormalizationArg(grandparent.arguments[0]);
910
1027
  }
911
- var require_path_normalization_default = createRule11({
1028
+ var require_path_normalization_default = createRule12({
912
1029
  name: "require-path-normalization",
913
1030
  meta: {
914
1031
  type: "problem",
@@ -943,6 +1060,7 @@ var rules = {
943
1060
  "no-hardcoded-path-separator": no_hardcoded_path_separator_default,
944
1061
  "no-layer-violation": no_layer_violation_default,
945
1062
  "no-nested-loops-in-critical": no_nested_loops_in_critical_default,
1063
+ "no-process-env-in-spawn": no_process_env_in_spawn_default,
946
1064
  "no-sync-io-in-async": no_sync_io_in_async_default,
947
1065
  "no-unbounded-array-chains": no_unbounded_array_chains_default,
948
1066
  "no-unix-shell-command": no_unix_shell_command_default,
@@ -954,7 +1072,7 @@ var rules = {
954
1072
  var plugin = {
955
1073
  meta: {
956
1074
  name: "@harness-engineering/eslint-plugin",
957
- version: "0.1.0"
1075
+ version: "0.2.4"
958
1076
  },
959
1077
  rules,
960
1078
  configs: {
@@ -972,6 +1090,7 @@ var plugin = {
972
1090
  "@harness-engineering/enforce-doc-exports": "warn",
973
1091
  "@harness-engineering/no-unix-shell-command": "warn",
974
1092
  "@harness-engineering/no-hardcoded-path-separator": "warn",
1093
+ "@harness-engineering/no-process-env-in-spawn": "error",
975
1094
  "@harness-engineering/require-path-normalization": "warn"
976
1095
  }
977
1096
  },
@@ -989,6 +1108,7 @@ var plugin = {
989
1108
  "@harness-engineering/enforce-doc-exports": "error",
990
1109
  "@harness-engineering/no-unix-shell-command": "error",
991
1110
  "@harness-engineering/no-hardcoded-path-separator": "error",
1111
+ "@harness-engineering/no-process-env-in-spawn": "error",
992
1112
  "@harness-engineering/require-path-normalization": "error"
993
1113
  }
994
1114
  }
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.3.0",
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.59.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.59.0",
26
+ "@typescript-eslint/rule-tester": "^8.59.0",
27
+ "@vitest/coverage-v8": "^4.1.5",
28
+ "eslint": "^10.2.1",
29
+ "tsup": "^8.5.1",
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^4.1.5"
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
  }