@harness-engineering/eslint-plugin 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
  };
@@ -93,8 +99,12 @@ declare const plugin: {
93
99
  '@harness-engineering/no-forbidden-imports': string;
94
100
  '@harness-engineering/require-boundary-schema': string;
95
101
  '@harness-engineering/enforce-doc-exports': string;
102
+ '@harness-engineering/no-nested-loops-in-critical': string;
103
+ '@harness-engineering/no-sync-io-in-async': string;
104
+ '@harness-engineering/no-unbounded-array-chains': string;
96
105
  '@harness-engineering/no-unix-shell-command': string;
97
106
  '@harness-engineering/no-hardcoded-path-separator': string;
107
+ '@harness-engineering/no-process-env-in-spawn': string;
98
108
  '@harness-engineering/require-path-normalization': string;
99
109
  };
100
110
  };
@@ -108,8 +118,12 @@ declare const plugin: {
108
118
  '@harness-engineering/no-forbidden-imports': string;
109
119
  '@harness-engineering/require-boundary-schema': string;
110
120
  '@harness-engineering/enforce-doc-exports': string;
121
+ '@harness-engineering/no-nested-loops-in-critical': string;
122
+ '@harness-engineering/no-sync-io-in-async': string;
123
+ '@harness-engineering/no-unbounded-array-chains': string;
111
124
  '@harness-engineering/no-unix-shell-command': string;
112
125
  '@harness-engineering/no-hardcoded-path-separator': string;
126
+ '@harness-engineering/no-process-env-in-spawn': string;
113
127
  '@harness-engineering/require-path-normalization': string;
114
128
  };
115
129
  };
@@ -146,6 +160,9 @@ declare const configs: {
146
160
  'no-nested-loops-in-critical': _typescript_eslint_utils_ts_eslint.RuleModule<"nestedLoopInCritical", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
147
161
  name: string;
148
162
  };
163
+ 'no-process-env-in-spawn': _typescript_eslint_utils_ts_eslint.RuleModule<"processEnvInSpawn", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
164
+ name: string;
165
+ };
149
166
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
150
167
  name: string;
151
168
  };
@@ -171,8 +188,12 @@ declare const configs: {
171
188
  '@harness-engineering/no-forbidden-imports': string;
172
189
  '@harness-engineering/require-boundary-schema': string;
173
190
  '@harness-engineering/enforce-doc-exports': string;
191
+ '@harness-engineering/no-nested-loops-in-critical': string;
192
+ '@harness-engineering/no-sync-io-in-async': string;
193
+ '@harness-engineering/no-unbounded-array-chains': string;
174
194
  '@harness-engineering/no-unix-shell-command': string;
175
195
  '@harness-engineering/no-hardcoded-path-separator': string;
196
+ '@harness-engineering/no-process-env-in-spawn': string;
176
197
  '@harness-engineering/require-path-normalization': string;
177
198
  };
178
199
  };
@@ -205,6 +226,9 @@ declare const configs: {
205
226
  'no-nested-loops-in-critical': _typescript_eslint_utils_ts_eslint.RuleModule<"nestedLoopInCritical", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
206
227
  name: string;
207
228
  };
229
+ 'no-process-env-in-spawn': _typescript_eslint_utils_ts_eslint.RuleModule<"processEnvInSpawn", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
230
+ name: string;
231
+ };
208
232
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
209
233
  name: string;
210
234
  };
@@ -230,8 +254,12 @@ declare const configs: {
230
254
  '@harness-engineering/no-forbidden-imports': string;
231
255
  '@harness-engineering/require-boundary-schema': string;
232
256
  '@harness-engineering/enforce-doc-exports': string;
257
+ '@harness-engineering/no-nested-loops-in-critical': string;
258
+ '@harness-engineering/no-sync-io-in-async': string;
259
+ '@harness-engineering/no-unbounded-array-chains': string;
233
260
  '@harness-engineering/no-unix-shell-command': string;
234
261
  '@harness-engineering/no-hardcoded-path-separator': string;
262
+ '@harness-engineering/no-process-env-in-spawn': string;
235
263
  '@harness-engineering/require-path-normalization': string;
236
264
  };
237
265
  };
package/dist/index.js CHANGED
@@ -308,17 +308,27 @@ function getConfig(filePath) {
308
308
  return null;
309
309
  }
310
310
  }
311
+ function getConfigRoot(filePath) {
312
+ const configPath = findConfigFile(path2.dirname(filePath));
313
+ return configPath ? path2.dirname(configPath) : null;
314
+ }
311
315
 
312
316
  // src/utils/path-utils.ts
313
317
  import * as path3 from "path";
314
318
  import { minimatch } from "minimatch";
315
- function resolveImportPath(importPath, importingFile) {
319
+ function resolveImportPath(importPath, importingFile, projectRoot) {
316
320
  if (!importPath.startsWith(".")) {
317
321
  return importPath;
318
322
  }
319
323
  const importingDir = path3.dirname(importingFile);
320
324
  const resolved = path3.resolve(importingDir, importPath);
321
325
  const normalized = resolved.replace(/\\/g, "/");
326
+ if (projectRoot) {
327
+ const root = projectRoot.replace(/\\/g, "/").replace(/\/$/, "");
328
+ if (normalized.startsWith(root + "/")) {
329
+ return normalized.slice(root.length + 1);
330
+ }
331
+ }
322
332
  const srcIndex = normalized.indexOf("/src/");
323
333
  if (srcIndex !== -1) {
324
334
  return normalized.slice(srcIndex + 1);
@@ -341,8 +351,14 @@ function getLayerForFile(filePath, layers) {
341
351
  function getLayerByName(name, layers) {
342
352
  return layers.find((l) => l.name === name);
343
353
  }
344
- function normalizePath2(filePath) {
354
+ function normalizePath2(filePath, projectRoot) {
345
355
  const normalized = filePath.replace(/\\/g, "/");
356
+ if (projectRoot) {
357
+ const root = projectRoot.replace(/\\/g, "/").replace(/\/$/, "");
358
+ if (normalized.startsWith(root + "/")) {
359
+ return normalized.slice(root.length + 1);
360
+ }
361
+ }
346
362
  const srcIndex = normalized.indexOf("/src/");
347
363
  if (srcIndex !== -1) {
348
364
  return normalized.slice(srcIndex + 1);
@@ -370,9 +386,9 @@ function findViolatedRule(applicableRules, importPath, resolvedImport) {
370
386
  function buildForbiddenMessage(rule, importPath) {
371
387
  return rule.message ?? `Import "${importPath}" is forbidden in files matching "${rule.from}"`;
372
388
  }
373
- function checkImportDeclaration2(node, applicableRules, filename, report) {
389
+ function checkImportDeclaration2(node, applicableRules, filename, projectRoot, report) {
374
390
  const importPath = node.source.value;
375
- const resolvedImport = resolveImportPath(importPath, filename);
391
+ const resolvedImport = resolveImportPath(importPath, filename, projectRoot ?? void 0);
376
392
  const violatedRule = findViolatedRule(applicableRules, importPath, resolvedImport);
377
393
  if (violatedRule) {
378
394
  report({
@@ -400,7 +416,8 @@ var no_forbidden_imports_default = createRule3({
400
416
  if (!config?.forbiddenImports?.length) {
401
417
  return {};
402
418
  }
403
- const filePath = normalizePath2(context.filename);
419
+ const projectRoot = getConfigRoot(context.filename);
420
+ const filePath = normalizePath2(context.filename, projectRoot ?? void 0);
404
421
  const applicableRules = config.forbiddenImports.filter(
405
422
  (rule) => matchesPattern(filePath, rule.from)
406
423
  );
@@ -410,7 +427,7 @@ var no_forbidden_imports_default = createRule3({
410
427
  const report = context.report.bind(context);
411
428
  return {
412
429
  ImportDeclaration(node) {
413
- checkImportDeclaration2(node, applicableRules, context.filename, report);
430
+ checkImportDeclaration2(node, applicableRules, context.filename, projectRoot, report);
414
431
  }
415
432
  };
416
433
  }
@@ -424,13 +441,13 @@ var createRule4 = ESLintUtils4.RuleCreator(
424
441
  function isLayerViolation(importLayer, currentLayer, currentLayerDef) {
425
442
  return importLayer !== currentLayer && !currentLayerDef.allowedDependencies.includes(importLayer);
426
443
  }
427
- function resolveImportLayer(importPath, filename, layers) {
444
+ function resolveImportLayer(importPath, filename, layers, projectRoot) {
428
445
  if (!importPath.startsWith(".")) return null;
429
- const resolvedImport = resolveImportPath(importPath, filename);
446
+ const resolvedImport = resolveImportPath(importPath, filename, projectRoot ?? void 0);
430
447
  return getLayerForFile(resolvedImport, layers) ?? null;
431
448
  }
432
- function checkLayerImport(node, context, currentLayer, currentLayerDef, layers) {
433
- const importLayer = resolveImportLayer(node.source.value, context.filename, layers);
449
+ function checkLayerImport(node, context, currentLayer, currentLayerDef, layers, projectRoot) {
450
+ const importLayer = resolveImportLayer(node.source.value, context.filename, layers, projectRoot);
434
451
  if (!importLayer) return;
435
452
  if (isLayerViolation(importLayer, currentLayer, currentLayerDef)) {
436
453
  context.report({
@@ -458,7 +475,8 @@ var no_layer_violation_default = createRule4({
458
475
  if (!config?.layers?.length) {
459
476
  return {};
460
477
  }
461
- const filePath = normalizePath2(context.filename);
478
+ const projectRoot = getConfigRoot(context.filename);
479
+ const filePath = normalizePath2(context.filename, projectRoot ?? void 0);
462
480
  const currentLayer = getLayerForFile(filePath, config.layers);
463
481
  if (!currentLayer) {
464
482
  return {};
@@ -470,7 +488,7 @@ var no_layer_violation_default = createRule4({
470
488
  const layers = config.layers;
471
489
  return {
472
490
  ImportDeclaration(node) {
473
- checkLayerImport(node, context, currentLayer, currentLayerDef, layers);
491
+ checkLayerImport(node, context, currentLayer, currentLayerDef, layers, projectRoot);
474
492
  }
475
493
  };
476
494
  }
@@ -935,11 +953,74 @@ var no_hardcoded_path_separator_default = createRule10({
935
953
  }
936
954
  });
937
955
 
938
- // src/rules/require-path-normalization.ts
956
+ // src/rules/no-process-env-in-spawn.ts
939
957
  import { ESLintUtils as ESLintUtils11 } from "@typescript-eslint/utils";
940
958
  var createRule11 = ESLintUtils11.RuleCreator(
941
959
  (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
942
960
  );
961
+ var SPAWN_FUNCTIONS = /* @__PURE__ */ new Set(["spawn", "spawnSync", "execFile", "execFileSync", "fork"]);
962
+ function getSpawnFunctionName(node) {
963
+ if (node.callee.type === "Identifier" && SPAWN_FUNCTIONS.has(node.callee.name)) {
964
+ return node.callee.name;
965
+ }
966
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && SPAWN_FUNCTIONS.has(node.callee.property.name)) {
967
+ return node.callee.property.name;
968
+ }
969
+ return void 0;
970
+ }
971
+ function isProcessEnv(node) {
972
+ return node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "process" && node.property.type === "Identifier" && node.property.name === "env";
973
+ }
974
+ function hasProcessEnvProperty(node) {
975
+ for (const prop of node.properties) {
976
+ if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "env" && isProcessEnv(prop.value)) {
977
+ return prop;
978
+ }
979
+ if (prop.type === "SpreadElement" && isProcessEnv(prop.argument)) {
980
+ return prop;
981
+ }
982
+ }
983
+ return void 0;
984
+ }
985
+ function findEnvViolation(args) {
986
+ for (const arg of args) {
987
+ if (arg.type !== "ObjectExpression") continue;
988
+ const envProp = hasProcessEnvProperty(arg);
989
+ if (envProp) return envProp;
990
+ }
991
+ return void 0;
992
+ }
993
+ var no_process_env_in_spawn_default = createRule11({
994
+ name: "no-process-env-in-spawn",
995
+ meta: {
996
+ type: "problem",
997
+ docs: {
998
+ description: "Disallow passing process.env directly to spawn/execFile/fork, which leaks all server-side secrets to child processes"
999
+ },
1000
+ messages: {
1001
+ processEnvInSpawn: "Do not pass process.env directly to '{{name}}'. Build an explicit env object with only the variables the child process needs."
1002
+ },
1003
+ schema: []
1004
+ },
1005
+ defaultOptions: [],
1006
+ create(context) {
1007
+ function checkCall(node) {
1008
+ const name = getSpawnFunctionName(node);
1009
+ if (!name) return;
1010
+ const violation = findEnvViolation(node.arguments);
1011
+ if (violation) {
1012
+ context.report({ node: violation, messageId: "processEnvInSpawn", data: { name } });
1013
+ }
1014
+ }
1015
+ return { CallExpression: checkCall };
1016
+ }
1017
+ });
1018
+
1019
+ // src/rules/require-path-normalization.ts
1020
+ import { ESLintUtils as ESLintUtils12 } from "@typescript-eslint/utils";
1021
+ var createRule12 = ESLintUtils12.RuleCreator(
1022
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
1023
+ );
943
1024
  function isRelativeCall(node) {
944
1025
  const callee = node.callee;
945
1026
  if (callee.type === "Identifier" && callee.name === "relative") {
@@ -965,7 +1046,7 @@ function hasNormalization(node) {
965
1046
  if (grandparent?.type !== "CallExpression" || grandparent.arguments.length < 1) return false;
966
1047
  return isBackslashNormalizationArg(grandparent.arguments[0]);
967
1048
  }
968
- var require_path_normalization_default = createRule11({
1049
+ var require_path_normalization_default = createRule12({
969
1050
  name: "require-path-normalization",
970
1051
  meta: {
971
1052
  type: "problem",
@@ -1000,6 +1081,7 @@ var rules = {
1000
1081
  "no-hardcoded-path-separator": no_hardcoded_path_separator_default,
1001
1082
  "no-layer-violation": no_layer_violation_default,
1002
1083
  "no-nested-loops-in-critical": no_nested_loops_in_critical_default,
1084
+ "no-process-env-in-spawn": no_process_env_in_spawn_default,
1003
1085
  "no-sync-io-in-async": no_sync_io_in_async_default,
1004
1086
  "no-unbounded-array-chains": no_unbounded_array_chains_default,
1005
1087
  "no-unix-shell-command": no_unix_shell_command_default,
@@ -1011,7 +1093,7 @@ var rules = {
1011
1093
  var plugin = {
1012
1094
  meta: {
1013
1095
  name: "@harness-engineering/eslint-plugin",
1014
- version: "0.2.4"
1096
+ version: "0.3.0"
1015
1097
  },
1016
1098
  rules,
1017
1099
  configs: {
@@ -1027,8 +1109,12 @@ var plugin = {
1027
1109
  "@harness-engineering/no-forbidden-imports": "error",
1028
1110
  "@harness-engineering/require-boundary-schema": "warn",
1029
1111
  "@harness-engineering/enforce-doc-exports": "warn",
1112
+ "@harness-engineering/no-nested-loops-in-critical": "warn",
1113
+ "@harness-engineering/no-sync-io-in-async": "warn",
1114
+ "@harness-engineering/no-unbounded-array-chains": "warn",
1030
1115
  "@harness-engineering/no-unix-shell-command": "warn",
1031
1116
  "@harness-engineering/no-hardcoded-path-separator": "warn",
1117
+ "@harness-engineering/no-process-env-in-spawn": "error",
1032
1118
  "@harness-engineering/require-path-normalization": "warn"
1033
1119
  }
1034
1120
  },
@@ -1044,8 +1130,12 @@ var plugin = {
1044
1130
  "@harness-engineering/no-forbidden-imports": "error",
1045
1131
  "@harness-engineering/require-boundary-schema": "error",
1046
1132
  "@harness-engineering/enforce-doc-exports": "error",
1133
+ "@harness-engineering/no-nested-loops-in-critical": "error",
1134
+ "@harness-engineering/no-sync-io-in-async": "error",
1135
+ "@harness-engineering/no-unbounded-array-chains": "error",
1047
1136
  "@harness-engineering/no-unix-shell-command": "error",
1048
1137
  "@harness-engineering/no-hardcoded-path-separator": "error",
1138
+ "@harness-engineering/no-process-env-in-spawn": "error",
1049
1139
  "@harness-engineering/require-path-normalization": "error"
1050
1140
  }
1051
1141
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/eslint-plugin",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "ESLint plugin for harness engineering architectural constraints",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,19 +16,19 @@
16
16
  "README.md"
17
17
  ],
18
18
  "dependencies": {
19
- "@typescript-eslint/utils": "^8.58.0",
19
+ "@typescript-eslint/utils": "^8.59.0",
20
20
  "minimatch": "^10.2.5",
21
21
  "zod": "^3.25.76"
22
22
  },
23
23
  "devDependencies": {
24
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",
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
29
  "tsup": "^8.5.1",
30
30
  "typescript": "^5.9.3",
31
- "vitest": "^4.1.2"
31
+ "vitest": "^4.1.5"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0",