@harness-engineering/eslint-plugin 0.3.0 → 0.4.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.
package/dist/index.d.ts CHANGED
@@ -28,6 +28,9 @@ declare const rules: {
28
28
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
29
29
  name: string;
30
30
  };
31
+ 'no-undefined-optional-assignment': _typescript_eslint_utils_ts_eslint.RuleModule<"undefinedOptionalAssignment", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
32
+ name: string;
33
+ };
31
34
  'no-unbounded-array-chains': _typescript_eslint_utils_ts_eslint.RuleModule<"unboundedArrayChain", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
32
35
  name: string;
33
36
  };
@@ -75,6 +78,9 @@ declare const plugin: {
75
78
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
76
79
  name: string;
77
80
  };
81
+ 'no-undefined-optional-assignment': _typescript_eslint_utils_ts_eslint.RuleModule<"undefinedOptionalAssignment", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
82
+ name: string;
83
+ };
78
84
  'no-unbounded-array-chains': _typescript_eslint_utils_ts_eslint.RuleModule<"unboundedArrayChain", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
79
85
  name: string;
80
86
  };
@@ -99,6 +105,9 @@ declare const plugin: {
99
105
  '@harness-engineering/no-forbidden-imports': string;
100
106
  '@harness-engineering/require-boundary-schema': string;
101
107
  '@harness-engineering/enforce-doc-exports': string;
108
+ '@harness-engineering/no-nested-loops-in-critical': string;
109
+ '@harness-engineering/no-sync-io-in-async': string;
110
+ '@harness-engineering/no-unbounded-array-chains': string;
102
111
  '@harness-engineering/no-unix-shell-command': string;
103
112
  '@harness-engineering/no-hardcoded-path-separator': string;
104
113
  '@harness-engineering/no-process-env-in-spawn': string;
@@ -115,6 +124,9 @@ declare const plugin: {
115
124
  '@harness-engineering/no-forbidden-imports': string;
116
125
  '@harness-engineering/require-boundary-schema': string;
117
126
  '@harness-engineering/enforce-doc-exports': string;
127
+ '@harness-engineering/no-nested-loops-in-critical': string;
128
+ '@harness-engineering/no-sync-io-in-async': string;
129
+ '@harness-engineering/no-unbounded-array-chains': string;
118
130
  '@harness-engineering/no-unix-shell-command': string;
119
131
  '@harness-engineering/no-hardcoded-path-separator': string;
120
132
  '@harness-engineering/no-process-env-in-spawn': string;
@@ -160,6 +172,9 @@ declare const configs: {
160
172
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
161
173
  name: string;
162
174
  };
175
+ 'no-undefined-optional-assignment': _typescript_eslint_utils_ts_eslint.RuleModule<"undefinedOptionalAssignment", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
176
+ name: string;
177
+ };
163
178
  'no-unbounded-array-chains': _typescript_eslint_utils_ts_eslint.RuleModule<"unboundedArrayChain", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
164
179
  name: string;
165
180
  };
@@ -182,6 +197,9 @@ declare const configs: {
182
197
  '@harness-engineering/no-forbidden-imports': string;
183
198
  '@harness-engineering/require-boundary-schema': string;
184
199
  '@harness-engineering/enforce-doc-exports': string;
200
+ '@harness-engineering/no-nested-loops-in-critical': string;
201
+ '@harness-engineering/no-sync-io-in-async': string;
202
+ '@harness-engineering/no-unbounded-array-chains': string;
185
203
  '@harness-engineering/no-unix-shell-command': string;
186
204
  '@harness-engineering/no-hardcoded-path-separator': string;
187
205
  '@harness-engineering/no-process-env-in-spawn': string;
@@ -223,6 +241,9 @@ declare const configs: {
223
241
  'no-sync-io-in-async': _typescript_eslint_utils_ts_eslint.RuleModule<"syncIoInAsync", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
224
242
  name: string;
225
243
  };
244
+ 'no-undefined-optional-assignment': _typescript_eslint_utils_ts_eslint.RuleModule<"undefinedOptionalAssignment", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
245
+ name: string;
246
+ };
226
247
  'no-unbounded-array-chains': _typescript_eslint_utils_ts_eslint.RuleModule<"unboundedArrayChain", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
227
248
  name: string;
228
249
  };
@@ -245,6 +266,9 @@ declare const configs: {
245
266
  '@harness-engineering/no-forbidden-imports': string;
246
267
  '@harness-engineering/require-boundary-schema': string;
247
268
  '@harness-engineering/enforce-doc-exports': string;
269
+ '@harness-engineering/no-nested-loops-in-critical': string;
270
+ '@harness-engineering/no-sync-io-in-async': string;
271
+ '@harness-engineering/no-unbounded-array-chains': string;
248
272
  '@harness-engineering/no-unix-shell-command': string;
249
273
  '@harness-engineering/no-hardcoded-path-separator': string;
250
274
  '@harness-engineering/no-process-env-in-spawn': string;
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
  }
@@ -964,6 +982,14 @@ function hasProcessEnvProperty(node) {
964
982
  }
965
983
  return void 0;
966
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
+ }
967
993
  var no_process_env_in_spawn_default = createRule11({
968
994
  name: "no-process-env-in-spawn",
969
995
  meta: {
@@ -978,20 +1004,15 @@ var no_process_env_in_spawn_default = createRule11({
978
1004
  },
979
1005
  defaultOptions: [],
980
1006
  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
- }
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 } });
993
1013
  }
994
- };
1014
+ }
1015
+ return { CallExpression: checkCall };
995
1016
  }
996
1017
  });
997
1018
 
@@ -1052,6 +1073,69 @@ var require_path_normalization_default = createRule12({
1052
1073
  }
1053
1074
  });
1054
1075
 
1076
+ // src/rules/no-undefined-optional-assignment.ts
1077
+ import { ESLintUtils as ESLintUtils13 } from "@typescript-eslint/utils";
1078
+ var createRule13 = ESLintUtils13.RuleCreator(
1079
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
1080
+ );
1081
+ function unionIncludesUndefined(typeNode) {
1082
+ return typeNode?.type === "TSUnionType" && typeNode.types.some((t) => t.type === "TSUndefinedKeyword");
1083
+ }
1084
+ function declaredType(identifier, scope) {
1085
+ const ref = scope.references.find((r) => r.identifier === identifier);
1086
+ const name = ref?.resolved?.defs[0]?.name;
1087
+ if (name?.type !== "Identifier") return void 0;
1088
+ return name.typeAnnotation?.typeAnnotation;
1089
+ }
1090
+ function checksDefined(expr, name) {
1091
+ if (expr.type !== "BinaryExpression" || expr.operator !== "!==" && expr.operator !== "!=") {
1092
+ return false;
1093
+ }
1094
+ const refsName = (n) => n.type === "Identifier" && n.name === name;
1095
+ const isNullish = (n) => n.type === "Identifier" && n.name === "undefined" || n.type === "Literal" && n.value === null;
1096
+ return refsName(expr.left) && isNullish(expr.right) || refsName(expr.right) && isNullish(expr.left);
1097
+ }
1098
+ function isGuarded(node, name) {
1099
+ for (let cur = node.parent; cur; cur = cur.parent) {
1100
+ if (cur.type === "LogicalExpression" && cur.operator === "&&" && checksDefined(cur.left, name)) {
1101
+ return true;
1102
+ }
1103
+ }
1104
+ return false;
1105
+ }
1106
+ var no_undefined_optional_assignment_default = createRule13({
1107
+ name: "no-undefined-optional-assignment",
1108
+ meta: {
1109
+ type: "suggestion",
1110
+ docs: {
1111
+ description: "Disallow assigning a possibly-undefined variable directly to an object property, which breaks `exactOptionalPropertyTypes`; use a conditional spread instead."
1112
+ },
1113
+ messages: {
1114
+ undefinedOptionalAssignment: "Assigning possibly-undefined '{{name}}' directly to '{{field}}' breaks exactOptionalPropertyTypes. Use a conditional spread: ...({{name}} !== undefined && {{ '{' }} {{field}}: {{name}} {{ '}' }})."
1115
+ },
1116
+ schema: []
1117
+ },
1118
+ defaultOptions: [],
1119
+ create(context) {
1120
+ return {
1121
+ Property(node) {
1122
+ if (node.computed || node.value.type !== "Identifier") return;
1123
+ if (node.key.type !== "Identifier" && node.key.type !== "Literal") return;
1124
+ const value = node.value;
1125
+ const scope = context.sourceCode.getScope(value);
1126
+ if (!unionIncludesUndefined(declaredType(value, scope))) return;
1127
+ if (isGuarded(node, value.name)) return;
1128
+ const field = node.key.type === "Identifier" ? node.key.name : String(node.key.value);
1129
+ context.report({
1130
+ node: value,
1131
+ messageId: "undefinedOptionalAssignment",
1132
+ data: { name: value.name, field }
1133
+ });
1134
+ }
1135
+ };
1136
+ }
1137
+ });
1138
+
1055
1139
  // src/rules/index.ts
1056
1140
  var rules = {
1057
1141
  "enforce-doc-exports": enforce_doc_exports_default,
@@ -1062,6 +1146,7 @@ var rules = {
1062
1146
  "no-nested-loops-in-critical": no_nested_loops_in_critical_default,
1063
1147
  "no-process-env-in-spawn": no_process_env_in_spawn_default,
1064
1148
  "no-sync-io-in-async": no_sync_io_in_async_default,
1149
+ "no-undefined-optional-assignment": no_undefined_optional_assignment_default,
1065
1150
  "no-unbounded-array-chains": no_unbounded_array_chains_default,
1066
1151
  "no-unix-shell-command": no_unix_shell_command_default,
1067
1152
  "require-boundary-schema": require_boundary_schema_default,
@@ -1072,7 +1157,7 @@ var rules = {
1072
1157
  var plugin = {
1073
1158
  meta: {
1074
1159
  name: "@harness-engineering/eslint-plugin",
1075
- version: "0.2.4"
1160
+ version: "0.3.0"
1076
1161
  },
1077
1162
  rules,
1078
1163
  configs: {
@@ -1088,6 +1173,9 @@ var plugin = {
1088
1173
  "@harness-engineering/no-forbidden-imports": "error",
1089
1174
  "@harness-engineering/require-boundary-schema": "warn",
1090
1175
  "@harness-engineering/enforce-doc-exports": "warn",
1176
+ "@harness-engineering/no-nested-loops-in-critical": "warn",
1177
+ "@harness-engineering/no-sync-io-in-async": "warn",
1178
+ "@harness-engineering/no-unbounded-array-chains": "warn",
1091
1179
  "@harness-engineering/no-unix-shell-command": "warn",
1092
1180
  "@harness-engineering/no-hardcoded-path-separator": "warn",
1093
1181
  "@harness-engineering/no-process-env-in-spawn": "error",
@@ -1106,6 +1194,9 @@ var plugin = {
1106
1194
  "@harness-engineering/no-forbidden-imports": "error",
1107
1195
  "@harness-engineering/require-boundary-schema": "error",
1108
1196
  "@harness-engineering/enforce-doc-exports": "error",
1197
+ "@harness-engineering/no-nested-loops-in-critical": "error",
1198
+ "@harness-engineering/no-sync-io-in-async": "error",
1199
+ "@harness-engineering/no-unbounded-array-chains": "error",
1109
1200
  "@harness-engineering/no-unix-shell-command": "error",
1110
1201
  "@harness-engineering/no-hardcoded-path-separator": "error",
1111
1202
  "@harness-engineering/no-process-env-in-spawn": "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/eslint-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "ESLint plugin for harness engineering architectural constraints",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",