@harness-engineering/eslint-plugin 0.1.1 → 0.2.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 (62) hide show
  1. package/README.md +8 -0
  2. package/dist/index.d.ts +113 -23
  3. package/dist/index.js +929 -43
  4. package/package.json +8 -7
  5. package/dist/configs/index.d.ts +0 -5
  6. package/dist/configs/index.d.ts.map +0 -1
  7. package/dist/configs/index.js +0 -8
  8. package/dist/configs/index.js.map +0 -1
  9. package/dist/configs/recommended.d.ts +0 -4
  10. package/dist/configs/recommended.d.ts.map +0 -1
  11. package/dist/configs/recommended.js +0 -16
  12. package/dist/configs/recommended.js.map +0 -1
  13. package/dist/configs/strict.d.ts +0 -4
  14. package/dist/configs/strict.d.ts.map +0 -1
  15. package/dist/configs/strict.js +0 -16
  16. package/dist/configs/strict.js.map +0 -1
  17. package/dist/index.d.ts.map +0 -1
  18. package/dist/index.js.map +0 -1
  19. package/dist/rules/enforce-doc-exports.d.ts +0 -12
  20. package/dist/rules/enforce-doc-exports.d.ts.map +0 -1
  21. package/dist/rules/enforce-doc-exports.js +0 -78
  22. package/dist/rules/enforce-doc-exports.js.map +0 -1
  23. package/dist/rules/index.d.ts +0 -21
  24. package/dist/rules/index.d.ts.map +0 -1
  25. package/dist/rules/index.js +0 -14
  26. package/dist/rules/index.js.map +0 -1
  27. package/dist/rules/no-circular-deps.d.ts +0 -20
  28. package/dist/rules/no-circular-deps.d.ts.map +0 -1
  29. package/dist/rules/no-circular-deps.js +0 -110
  30. package/dist/rules/no-circular-deps.js.map +0 -1
  31. package/dist/rules/no-forbidden-imports.d.ts +0 -6
  32. package/dist/rules/no-forbidden-imports.d.ts.map +0 -1
  33. package/dist/rules/no-forbidden-imports.js +0 -58
  34. package/dist/rules/no-forbidden-imports.js.map +0 -1
  35. package/dist/rules/no-layer-violation.d.ts +0 -6
  36. package/dist/rules/no-layer-violation.d.ts.map +0 -1
  37. package/dist/rules/no-layer-violation.js +0 -62
  38. package/dist/rules/no-layer-violation.js.map +0 -1
  39. package/dist/rules/require-boundary-schema.d.ts +0 -6
  40. package/dist/rules/require-boundary-schema.d.ts.map +0 -1
  41. package/dist/rules/require-boundary-schema.js +0 -53
  42. package/dist/rules/require-boundary-schema.js.map +0 -1
  43. package/dist/utils/ast-helpers.d.ts +0 -14
  44. package/dist/utils/ast-helpers.d.ts.map +0 -1
  45. package/dist/utils/ast-helpers.js +0 -94
  46. package/dist/utils/ast-helpers.js.map +0 -1
  47. package/dist/utils/config-loader.d.ts +0 -10
  48. package/dist/utils/config-loader.d.ts.map +0 -1
  49. package/dist/utils/config-loader.js +0 -56
  50. package/dist/utils/config-loader.js.map +0 -1
  51. package/dist/utils/index.d.ts +0 -5
  52. package/dist/utils/index.d.ts.map +0 -1
  53. package/dist/utils/index.js +0 -6
  54. package/dist/utils/index.js.map +0 -1
  55. package/dist/utils/path-utils.d.ts +0 -24
  56. package/dist/utils/path-utils.d.ts.map +0 -1
  57. package/dist/utils/path-utils.js +0 -62
  58. package/dist/utils/path-utils.js.map +0 -1
  59. package/dist/utils/schema.d.ts +0 -117
  60. package/dist/utils/schema.d.ts.map +0 -1
  61. package/dist/utils/schema.js +0 -34
  62. package/dist/utils/schema.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,46 +1,932 @@
1
- // src/index.ts
2
- import { rules } from './rules';
3
- // Define the plugin object
4
- const plugin = {
5
- meta: {
6
- name: '@harness-engineering/eslint-plugin',
7
- version: '0.1.0',
8
- },
9
- rules,
10
- configs: {
11
- recommended: {
12
- plugins: {
13
- get '@harness-engineering'() {
14
- return plugin;
15
- },
16
- },
17
- rules: {
18
- '@harness-engineering/no-layer-violation': 'error',
19
- '@harness-engineering/no-circular-deps': 'error',
20
- '@harness-engineering/no-forbidden-imports': 'error',
21
- '@harness-engineering/require-boundary-schema': 'warn',
22
- '@harness-engineering/enforce-doc-exports': 'warn',
23
- },
24
- },
25
- strict: {
26
- plugins: {
27
- get '@harness-engineering'() {
28
- return plugin;
29
- },
30
- },
31
- rules: {
32
- '@harness-engineering/no-layer-violation': 'error',
33
- '@harness-engineering/no-circular-deps': 'error',
34
- '@harness-engineering/no-forbidden-imports': 'error',
35
- '@harness-engineering/require-boundary-schema': 'error',
36
- '@harness-engineering/enforce-doc-exports': 'error',
37
- },
1
+ // src/rules/enforce-doc-exports.ts
2
+ import { ESLintUtils, AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
3
+
4
+ // src/utils/ast-helpers.ts
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;
11
+ for (let i = lines.length - 1; i >= 0; i--) {
12
+ const line = lines[i]?.trim() ?? "";
13
+ if (line === "") continue;
14
+ if (line.endsWith("*/")) {
15
+ const startIdx = textBefore.lastIndexOf("/**");
16
+ const endIdx = textBefore.lastIndexOf("*/");
17
+ if (startIdx !== -1 && endIdx > startIdx) {
18
+ foundJSDoc = true;
19
+ }
20
+ break;
21
+ }
22
+ break;
23
+ }
24
+ return foundJSDoc;
25
+ }
26
+ function hasZodValidation(body) {
27
+ let found = false;
28
+ const skipKeys = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
29
+ function visit(node) {
30
+ if (found) return;
31
+ if (node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression) {
32
+ const prop = node.callee.property;
33
+ const propType = prop.type;
34
+ if (propType === AST_NODE_TYPES.Identifier && (prop.name === "parse" || prop.name === "safeParse")) {
35
+ found = true;
36
+ return;
37
+ }
38
+ }
39
+ for (const key of Object.keys(node)) {
40
+ if (skipKeys.has(key)) continue;
41
+ const value = node[key];
42
+ if (value && typeof value === "object") {
43
+ if (Array.isArray(value)) {
44
+ for (const item of value) {
45
+ if (item && typeof item === "object" && "type" in item) {
46
+ visit(item);
47
+ }
48
+ }
49
+ } else if ("type" in value) {
50
+ visit(value);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ visit(body);
56
+ return found;
57
+ }
58
+ function isMarkedInternal(node, sourceCode) {
59
+ if (!node.range) return false;
60
+ const textBefore = sourceCode.slice(0, node.range[0]);
61
+ const lastComment = textBefore.lastIndexOf("/**");
62
+ if (lastComment === -1) return false;
63
+ const commentEnd = textBefore.lastIndexOf("*/");
64
+ if (commentEnd === -1 || commentEnd < lastComment) return false;
65
+ const comment = textBefore.slice(lastComment, commentEnd + 2);
66
+ return comment.includes("@internal");
67
+ }
68
+
69
+ // src/rules/enforce-doc-exports.ts
70
+ var createRule = ESLintUtils.RuleCreator(
71
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
72
+ );
73
+ var enforce_doc_exports_default = createRule({
74
+ name: "enforce-doc-exports",
75
+ meta: {
76
+ type: "suggestion",
77
+ docs: {
78
+ description: "Require JSDoc comments on public exports"
79
+ },
80
+ messages: {
81
+ missingJSDoc: 'Exported {{kind}} "{{name}}" is missing JSDoc documentation'
82
+ },
83
+ schema: [
84
+ {
85
+ type: "object",
86
+ properties: {
87
+ ignoreTypes: { type: "boolean", default: false },
88
+ ignoreInternal: { type: "boolean", default: true }
38
89
  },
90
+ additionalProperties: false
91
+ }
92
+ ]
93
+ },
94
+ defaultOptions: [{ ignoreTypes: false, ignoreInternal: true }],
95
+ create(context, [options]) {
96
+ const sourceCode = context.sourceCode.getText();
97
+ function checkExport(node, kind, name) {
98
+ if (options.ignoreInternal && isMarkedInternal(node, sourceCode)) {
99
+ return;
100
+ }
101
+ if (!hasJSDocComment(node, sourceCode)) {
102
+ context.report({
103
+ node,
104
+ messageId: "missingJSDoc",
105
+ data: { kind, name }
106
+ });
107
+ }
108
+ }
109
+ return {
110
+ ExportNamedDeclaration(node) {
111
+ const decl = node.declaration;
112
+ if (!decl) return;
113
+ const declType = decl.type;
114
+ if (declType === AST_NODE_TYPES2.FunctionDeclaration) {
115
+ const fn = decl;
116
+ if (fn.id) checkExport(node, "function", fn.id.name);
117
+ } else if (declType === AST_NODE_TYPES2.ClassDeclaration) {
118
+ const cls = decl;
119
+ if (cls.id) checkExport(node, "class", cls.id.name);
120
+ } else if (declType === AST_NODE_TYPES2.VariableDeclaration) {
121
+ const varDecl = decl;
122
+ for (const declarator of varDecl.declarations) {
123
+ if (declarator.id.type === AST_NODE_TYPES2.Identifier) {
124
+ checkExport(node, "variable", declarator.id.name);
125
+ }
126
+ }
127
+ } else if (declType === AST_NODE_TYPES2.TSTypeAliasDeclaration && !options.ignoreTypes) {
128
+ const typeAlias = decl;
129
+ checkExport(node, "type", typeAlias.id.name);
130
+ } else if (declType === AST_NODE_TYPES2.TSInterfaceDeclaration && !options.ignoreTypes) {
131
+ const iface = decl;
132
+ checkExport(node, "interface", iface.id.name);
133
+ }
134
+ }
135
+ };
136
+ }
137
+ });
138
+
139
+ // src/rules/no-circular-deps.ts
140
+ import { ESLintUtils as ESLintUtils2 } from "@typescript-eslint/utils";
141
+ import * as path from "path";
142
+ var createRule2 = ESLintUtils2.RuleCreator(
143
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
144
+ );
145
+ var importGraph = /* @__PURE__ */ new Map();
146
+ function addEdge(from, to) {
147
+ if (!importGraph.has(from)) {
148
+ importGraph.set(from, /* @__PURE__ */ new Set());
149
+ }
150
+ importGraph.get(from).add(to);
151
+ }
152
+ function detectCycle(from, to) {
153
+ const visited = /* @__PURE__ */ new Set();
154
+ const cyclePath = [to];
155
+ function dfs(current) {
156
+ if (current === from) {
157
+ return true;
158
+ }
159
+ if (visited.has(current)) {
160
+ return false;
161
+ }
162
+ visited.add(current);
163
+ const deps = importGraph.get(current);
164
+ if (deps) {
165
+ for (const dep of deps) {
166
+ cyclePath.push(dep);
167
+ if (dfs(dep)) {
168
+ return true;
169
+ }
170
+ cyclePath.pop();
171
+ }
172
+ }
173
+ return false;
174
+ }
175
+ if (dfs(to)) {
176
+ return [from, ...cyclePath];
177
+ }
178
+ return null;
179
+ }
180
+ function normalizePath(filePath) {
181
+ const normalized = filePath.replace(/\\/g, "/");
182
+ const srcIndex = normalized.indexOf("/src/");
183
+ if (srcIndex !== -1) {
184
+ return normalized.slice(srcIndex + 1);
185
+ }
186
+ return path.basename(filePath);
187
+ }
188
+ var no_circular_deps_default = createRule2({
189
+ name: "no-circular-deps",
190
+ meta: {
191
+ type: "problem",
192
+ docs: {
193
+ description: "Detect circular import dependencies"
194
+ },
195
+ messages: {
196
+ circularDep: "Circular dependency detected: {{cycle}}"
197
+ },
198
+ schema: []
199
+ },
200
+ defaultOptions: [],
201
+ create(context) {
202
+ const currentFile = normalizePath(context.filename);
203
+ return {
204
+ ImportDeclaration(node) {
205
+ const importPath = node.source.value;
206
+ if (!importPath.startsWith(".")) {
207
+ return;
208
+ }
209
+ const importingDir = path.dirname(context.filename);
210
+ const resolvedPath = path.resolve(importingDir, importPath);
211
+ const normalizedImport = normalizePath(resolvedPath);
212
+ const cycle = detectCycle(currentFile, normalizedImport);
213
+ if (cycle) {
214
+ context.report({
215
+ node,
216
+ messageId: "circularDep",
217
+ data: {
218
+ cycle: cycle.map((f) => path.basename(f)).join(" \u2192 ")
219
+ }
220
+ });
221
+ }
222
+ addEdge(currentFile, normalizedImport);
223
+ }
224
+ };
225
+ }
226
+ });
227
+
228
+ // src/rules/no-forbidden-imports.ts
229
+ import { ESLintUtils as ESLintUtils3 } from "@typescript-eslint/utils";
230
+
231
+ // src/utils/config-loader.ts
232
+ import * as fs from "fs";
233
+ import * as path2 from "path";
234
+
235
+ // src/utils/schema.ts
236
+ import { z } from "zod";
237
+ var LayerSchema = z.object({
238
+ name: z.string(),
239
+ pattern: z.string(),
240
+ allowedDependencies: z.array(z.string())
241
+ });
242
+ var ForbiddenImportSchema = z.object({
243
+ from: z.string(),
244
+ disallow: z.array(z.string()),
245
+ message: z.string().optional()
246
+ });
247
+ var BoundaryConfigSchema = z.object({
248
+ requireSchema: z.array(z.string())
249
+ });
250
+ var HarnessConfigSchema = z.object({
251
+ version: z.literal(1),
252
+ layers: z.array(LayerSchema).optional(),
253
+ forbiddenImports: z.array(ForbiddenImportSchema).optional(),
254
+ boundaries: BoundaryConfigSchema.optional()
255
+ });
256
+
257
+ // src/utils/config-loader.ts
258
+ var CONFIG_FILENAME = "harness.config.json";
259
+ var cachedConfig = null;
260
+ var cachedConfigPath = null;
261
+ function findConfigFile(startDir) {
262
+ let currentDir = path2.resolve(startDir);
263
+ const root = path2.parse(currentDir).root;
264
+ while (currentDir !== root) {
265
+ const configPath = path2.join(currentDir, CONFIG_FILENAME);
266
+ if (fs.existsSync(configPath)) {
267
+ return configPath;
268
+ }
269
+ currentDir = path2.dirname(currentDir);
270
+ }
271
+ return null;
272
+ }
273
+ function getConfig(filePath) {
274
+ const configPath = findConfigFile(path2.dirname(filePath));
275
+ if (!configPath) {
276
+ return null;
277
+ }
278
+ if (cachedConfigPath === configPath && cachedConfig) {
279
+ return cachedConfig;
280
+ }
281
+ try {
282
+ const content = fs.readFileSync(configPath, "utf-8");
283
+ const parsed = HarnessConfigSchema.safeParse(JSON.parse(content));
284
+ if (!parsed.success) {
285
+ return null;
286
+ }
287
+ cachedConfig = parsed.data;
288
+ cachedConfigPath = configPath;
289
+ return cachedConfig;
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+
295
+ // src/utils/path-utils.ts
296
+ import * as path3 from "path";
297
+ import { minimatch } from "minimatch";
298
+ function resolveImportPath(importPath, importingFile) {
299
+ if (!importPath.startsWith(".")) {
300
+ return importPath;
301
+ }
302
+ const importingDir = path3.dirname(importingFile);
303
+ const resolved = path3.resolve(importingDir, importPath);
304
+ const normalized = resolved.replace(/\\/g, "/");
305
+ const srcIndex = normalized.indexOf("/src/");
306
+ if (srcIndex !== -1) {
307
+ return normalized.slice(srcIndex + 1);
308
+ }
309
+ return importPath;
310
+ }
311
+ function matchesPattern(filePath, pattern) {
312
+ const normalizedPath = filePath.replace(/\\/g, "/");
313
+ const normalizedPattern = pattern.replace(/\\/g, "/");
314
+ return minimatch(normalizedPath, normalizedPattern, { matchBase: false });
315
+ }
316
+ function getLayerForFile(filePath, layers) {
317
+ for (const layer of layers) {
318
+ if (matchesPattern(filePath, layer.pattern)) {
319
+ return layer.name;
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+ function getLayerByName(name, layers) {
325
+ return layers.find((l) => l.name === name);
326
+ }
327
+ function normalizePath2(filePath) {
328
+ const normalized = filePath.replace(/\\/g, "/");
329
+ const srcIndex = normalized.indexOf("/src/");
330
+ if (srcIndex !== -1) {
331
+ return normalized.slice(srcIndex + 1);
332
+ }
333
+ return filePath;
334
+ }
335
+
336
+ // src/rules/no-forbidden-imports.ts
337
+ var createRule3 = ESLintUtils3.RuleCreator(
338
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
339
+ );
340
+ var no_forbidden_imports_default = createRule3({
341
+ name: "no-forbidden-imports",
342
+ meta: {
343
+ type: "problem",
344
+ docs: {
345
+ description: "Block forbidden imports based on configurable patterns"
346
+ },
347
+ messages: {
348
+ forbiddenImport: "{{message}}"
349
+ },
350
+ schema: []
351
+ },
352
+ defaultOptions: [],
353
+ create(context) {
354
+ const config = getConfig(context.filename);
355
+ if (!config?.forbiddenImports?.length) {
356
+ return {};
357
+ }
358
+ const filePath = normalizePath2(context.filename);
359
+ const applicableRules = config.forbiddenImports.filter(
360
+ (rule) => matchesPattern(filePath, rule.from)
361
+ );
362
+ if (applicableRules.length === 0) {
363
+ return {};
364
+ }
365
+ return {
366
+ ImportDeclaration(node) {
367
+ const importPath = node.source.value;
368
+ const resolvedImport = resolveImportPath(importPath, context.filename);
369
+ for (const rule of applicableRules) {
370
+ for (const disallowed of rule.disallow) {
371
+ const isMatch = importPath === disallowed || matchesPattern(resolvedImport, disallowed) || matchesPattern(importPath, disallowed);
372
+ if (isMatch) {
373
+ context.report({
374
+ node,
375
+ messageId: "forbiddenImport",
376
+ data: {
377
+ message: rule.message || `Import "${importPath}" is forbidden in files matching "${rule.from}"`
378
+ }
379
+ });
380
+ return;
381
+ }
382
+ }
383
+ }
384
+ }
385
+ };
386
+ }
387
+ });
388
+
389
+ // src/rules/no-layer-violation.ts
390
+ import { ESLintUtils as ESLintUtils4 } from "@typescript-eslint/utils";
391
+ var createRule4 = ESLintUtils4.RuleCreator(
392
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
393
+ );
394
+ var no_layer_violation_default = createRule4({
395
+ name: "no-layer-violation",
396
+ meta: {
397
+ type: "problem",
398
+ docs: {
399
+ description: "Enforce layer boundary imports"
400
+ },
401
+ messages: {
402
+ layerViolation: 'Layer "{{fromLayer}}" cannot import from layer "{{toLayer}}"'
403
+ },
404
+ schema: []
405
+ },
406
+ defaultOptions: [],
407
+ create(context) {
408
+ const config = getConfig(context.filename);
409
+ if (!config?.layers?.length) {
410
+ return {};
411
+ }
412
+ const filePath = normalizePath2(context.filename);
413
+ const currentLayer = getLayerForFile(filePath, config.layers);
414
+ if (!currentLayer) {
415
+ return {};
416
+ }
417
+ const currentLayerDef = getLayerByName(currentLayer, config.layers);
418
+ if (!currentLayerDef) {
419
+ return {};
420
+ }
421
+ return {
422
+ ImportDeclaration(node) {
423
+ const importPath = node.source.value;
424
+ if (!importPath.startsWith(".")) {
425
+ return;
426
+ }
427
+ const resolvedImport = resolveImportPath(importPath, context.filename);
428
+ const importLayer = getLayerForFile(resolvedImport, config.layers);
429
+ if (!importLayer) {
430
+ return;
431
+ }
432
+ if (importLayer !== currentLayer && !currentLayerDef.allowedDependencies.includes(importLayer)) {
433
+ context.report({
434
+ node,
435
+ messageId: "layerViolation",
436
+ data: {
437
+ fromLayer: currentLayer,
438
+ toLayer: importLayer
439
+ }
440
+ });
441
+ }
442
+ }
443
+ };
444
+ }
445
+ });
446
+
447
+ // src/rules/no-nested-loops-in-critical.ts
448
+ import { ESLintUtils as ESLintUtils5 } from "@typescript-eslint/utils";
449
+ var createRule5 = ESLintUtils5.RuleCreator(
450
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
451
+ );
452
+ var no_nested_loops_in_critical_default = createRule5({
453
+ name: "no-nested-loops-in-critical",
454
+ meta: {
455
+ type: "suggestion",
456
+ docs: {
457
+ description: "Disallow nested loops in @perf-critical functions"
458
+ },
459
+ messages: {
460
+ nestedLoopInCritical: "Nested loop in @perf-critical code \u2014 consider alternative algorithm"
461
+ },
462
+ schema: []
463
+ },
464
+ defaultOptions: [],
465
+ create(context) {
466
+ const sourceText = context.sourceCode.getText();
467
+ if (!sourceText.includes("@perf-critical")) {
468
+ return {};
469
+ }
470
+ const criticalStack = [];
471
+ let loopDepth = 0;
472
+ function isCritical() {
473
+ return criticalStack.length > 0 && criticalStack[criticalStack.length - 1] === true;
474
+ }
475
+ function hasCriticalAnnotation(node) {
476
+ const target = node.parent?.type === "ExportNamedDeclaration" || node.parent?.type === "VariableDeclaration" ? node.parent : node;
477
+ const startLine = target.loc.start.line;
478
+ const lines = sourceText.split("\n");
479
+ for (let i = Math.max(0, startLine - 2); i < startLine; i++) {
480
+ if (lines[i]?.includes("@perf-critical")) return true;
481
+ }
482
+ return false;
483
+ }
484
+ function enterFunction(node) {
485
+ criticalStack.push(hasCriticalAnnotation(node));
486
+ loopDepth = 0;
487
+ }
488
+ function exitFunction() {
489
+ criticalStack.pop();
490
+ loopDepth = 0;
491
+ }
492
+ function enterLoop(node) {
493
+ if (!isCritical()) return;
494
+ loopDepth++;
495
+ if (loopDepth > 1) {
496
+ context.report({ node, messageId: "nestedLoopInCritical" });
497
+ }
498
+ }
499
+ function exitLoop() {
500
+ if (!isCritical()) return;
501
+ loopDepth--;
502
+ }
503
+ return {
504
+ FunctionDeclaration: enterFunction,
505
+ "FunctionDeclaration:exit": exitFunction,
506
+ FunctionExpression: enterFunction,
507
+ "FunctionExpression:exit": exitFunction,
508
+ ArrowFunctionExpression: enterFunction,
509
+ "ArrowFunctionExpression:exit": exitFunction,
510
+ ForStatement: enterLoop,
511
+ "ForStatement:exit": exitLoop,
512
+ ForInStatement: enterLoop,
513
+ "ForInStatement:exit": exitLoop,
514
+ ForOfStatement: enterLoop,
515
+ "ForOfStatement:exit": exitLoop,
516
+ WhileStatement: enterLoop,
517
+ "WhileStatement:exit": exitLoop,
518
+ DoWhileStatement: enterLoop,
519
+ "DoWhileStatement:exit": exitLoop
520
+ };
521
+ }
522
+ });
523
+
524
+ // src/rules/no-sync-io-in-async.ts
525
+ import { ESLintUtils as ESLintUtils6 } from "@typescript-eslint/utils";
526
+ var createRule6 = ESLintUtils6.RuleCreator(
527
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
528
+ );
529
+ var SYNC_FS_METHODS = /* @__PURE__ */ new Set([
530
+ "readFileSync",
531
+ "writeFileSync",
532
+ "existsSync",
533
+ "readdirSync",
534
+ "statSync",
535
+ "mkdirSync",
536
+ "unlinkSync",
537
+ "copyFileSync",
538
+ "renameSync",
539
+ "accessSync"
540
+ ]);
541
+ var no_sync_io_in_async_default = createRule6({
542
+ name: "no-sync-io-in-async",
543
+ meta: {
544
+ type: "problem",
545
+ docs: {
546
+ description: "Disallow synchronous fs operations inside async functions"
547
+ },
548
+ messages: {
549
+ syncIoInAsync: "Use async fs methods instead of '{{name}}' in async functions"
39
550
  },
551
+ schema: []
552
+ },
553
+ defaultOptions: [],
554
+ create(context) {
555
+ let asyncDepth = 0;
556
+ function enterFunction(node) {
557
+ if (node.async) {
558
+ asyncDepth++;
559
+ }
560
+ }
561
+ function exitFunction(node) {
562
+ if (node.async) {
563
+ asyncDepth--;
564
+ }
565
+ }
566
+ return {
567
+ FunctionDeclaration: enterFunction,
568
+ "FunctionDeclaration:exit": exitFunction,
569
+ FunctionExpression: enterFunction,
570
+ "FunctionExpression:exit": exitFunction,
571
+ ArrowFunctionExpression: enterFunction,
572
+ "ArrowFunctionExpression:exit": exitFunction,
573
+ CallExpression(node) {
574
+ if (asyncDepth === 0) return;
575
+ let name;
576
+ if (node.callee.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.name)) {
577
+ name = node.callee.name;
578
+ }
579
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && SYNC_FS_METHODS.has(node.callee.property.name)) {
580
+ name = node.callee.property.name;
581
+ }
582
+ if (name) {
583
+ context.report({
584
+ node,
585
+ messageId: "syncIoInAsync",
586
+ data: { name }
587
+ });
588
+ }
589
+ }
590
+ };
591
+ }
592
+ });
593
+
594
+ // src/rules/no-unbounded-array-chains.ts
595
+ import { ESLintUtils as ESLintUtils7 } from "@typescript-eslint/utils";
596
+ var createRule7 = ESLintUtils7.RuleCreator(
597
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
598
+ );
599
+ var ARRAY_METHODS = /* @__PURE__ */ new Set([
600
+ "filter",
601
+ "map",
602
+ "reduce",
603
+ "sort",
604
+ "flatMap",
605
+ "find",
606
+ "some",
607
+ "every",
608
+ "forEach"
609
+ ]);
610
+ var no_unbounded_array_chains_default = createRule7({
611
+ name: "no-unbounded-array-chains",
612
+ meta: {
613
+ type: "suggestion",
614
+ docs: {
615
+ description: "Disallow 3+ chained array operations"
616
+ },
617
+ messages: {
618
+ unboundedArrayChain: "3+ chained array operations \u2014 consider a single-pass approach"
619
+ },
620
+ schema: []
621
+ },
622
+ defaultOptions: [],
623
+ create(context) {
624
+ return {
625
+ "CallExpression > MemberExpression.callee"(node) {
626
+ if (node.property.type !== "Identifier" || !ARRAY_METHODS.has(node.property.name)) {
627
+ return;
628
+ }
629
+ const callExpr = node.parent;
630
+ if (callExpr.parent && callExpr.parent.type === "MemberExpression" && callExpr.parent.property.type === "Identifier" && ARRAY_METHODS.has(callExpr.parent.property.name)) {
631
+ return;
632
+ }
633
+ let chainLength = 1;
634
+ let current = node;
635
+ while (true) {
636
+ const memberExpr = current;
637
+ const obj = memberExpr.object;
638
+ if (obj.type === "CallExpression" && obj.callee.type === "MemberExpression" && obj.callee.property.type === "Identifier" && ARRAY_METHODS.has(obj.callee.property.name)) {
639
+ chainLength++;
640
+ current = obj.callee;
641
+ } else {
642
+ break;
643
+ }
644
+ }
645
+ if (chainLength >= 3) {
646
+ context.report({
647
+ node: callExpr,
648
+ messageId: "unboundedArrayChain"
649
+ });
650
+ }
651
+ }
652
+ };
653
+ }
654
+ });
655
+
656
+ // src/rules/require-boundary-schema.ts
657
+ import { ESLintUtils as ESLintUtils8, AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
658
+ var createRule8 = ESLintUtils8.RuleCreator(
659
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
660
+ );
661
+ var require_boundary_schema_default = createRule8({
662
+ name: "require-boundary-schema",
663
+ meta: {
664
+ type: "problem",
665
+ docs: {
666
+ description: "Require Zod schema validation at API boundaries"
667
+ },
668
+ messages: {
669
+ missingSchema: 'Exported function "{{name}}" at API boundary must validate input with Zod schema'
670
+ },
671
+ schema: []
672
+ },
673
+ defaultOptions: [],
674
+ create(context) {
675
+ const config = getConfig(context.filename);
676
+ if (!config?.boundaries?.requireSchema?.length) {
677
+ return {};
678
+ }
679
+ const filePath = normalizePath2(context.filename);
680
+ const isBoundaryFile = config.boundaries.requireSchema.some(
681
+ (pattern) => matchesPattern(filePath, pattern)
682
+ );
683
+ if (!isBoundaryFile) {
684
+ return {};
685
+ }
686
+ return {
687
+ ExportNamedDeclaration(node) {
688
+ const decl = node.declaration;
689
+ if (!decl || decl.type !== AST_NODE_TYPES3.FunctionDeclaration) {
690
+ return;
691
+ }
692
+ const fn = decl;
693
+ if (!fn.id || !fn.body) return;
694
+ if (!hasZodValidation(fn.body)) {
695
+ context.report({
696
+ node: fn,
697
+ messageId: "missingSchema",
698
+ data: { name: fn.id.name }
699
+ });
700
+ }
701
+ }
702
+ };
703
+ }
704
+ });
705
+
706
+ // src/rules/no-unix-shell-command.ts
707
+ import { ESLintUtils as ESLintUtils9 } from "@typescript-eslint/utils";
708
+ var createRule9 = ESLintUtils9.RuleCreator(
709
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
710
+ );
711
+ var UNIX_COMMANDS = ["rm", "cp", "mv", "mkdir", "chmod", "chown"];
712
+ var UNIX_COMMAND_PATTERN = new RegExp(
713
+ `(?:^|[;&|]\\s*)(?:/(?:usr/)?(?:bin|sbin)/)?(?:${UNIX_COMMANDS.join("|")})(?:\\s|$)`
714
+ );
715
+ var FLAGGED_FUNCTIONS = /* @__PURE__ */ new Set(["exec", "execSync"]);
716
+ var no_unix_shell_command_default = createRule9({
717
+ name: "no-unix-shell-command",
718
+ meta: {
719
+ type: "problem",
720
+ docs: {
721
+ description: "Disallow exec/execSync calls with Unix-specific shell commands"
722
+ },
723
+ messages: {
724
+ unixShellCommand: "Avoid Unix-specific shell commands in exec/execSync. Use Node.js fs APIs or execFile with cross-platform binaries instead."
725
+ },
726
+ schema: []
727
+ },
728
+ defaultOptions: [],
729
+ create(context) {
730
+ return {
731
+ CallExpression(node) {
732
+ let functionName;
733
+ if (node.callee.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.name)) {
734
+ functionName = node.callee.name;
735
+ }
736
+ if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && FLAGGED_FUNCTIONS.has(node.callee.property.name)) {
737
+ functionName = node.callee.property.name;
738
+ }
739
+ if (!functionName) return;
740
+ const firstArg = node.arguments[0];
741
+ if (!firstArg) return;
742
+ let commandString;
743
+ if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
744
+ commandString = firstArg.value;
745
+ } else if (firstArg.type === "TemplateLiteral" && firstArg.quasis.length > 0) {
746
+ commandString = firstArg.quasis.map((q) => q.value.raw).join(" ");
747
+ }
748
+ if (commandString && UNIX_COMMAND_PATTERN.test(commandString)) {
749
+ context.report({
750
+ node,
751
+ messageId: "unixShellCommand"
752
+ });
753
+ }
754
+ }
755
+ };
756
+ }
757
+ });
758
+
759
+ // src/rules/no-hardcoded-path-separator.ts
760
+ import { ESLintUtils as ESLintUtils10 } from "@typescript-eslint/utils";
761
+ var createRule10 = ESLintUtils10.RuleCreator(
762
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
763
+ );
764
+ var HARDCODED_SEPARATOR_PATTERN = /\/[a-zA-Z_][a-zA-Z0-9_-]*\//;
765
+ var URL_PREFIXES = ["http://", "https://", "ftp://", "file://"];
766
+ var PATH_METHODS = /* @__PURE__ */ new Set([
767
+ "join",
768
+ "resolve",
769
+ "normalize",
770
+ "relative",
771
+ "dirname",
772
+ "basename",
773
+ "extname",
774
+ "parse",
775
+ "format",
776
+ "isAbsolute"
777
+ ]);
778
+ var FS_METHODS = /* @__PURE__ */ new Set([
779
+ "readFileSync",
780
+ "writeFileSync",
781
+ "readFile",
782
+ "writeFile",
783
+ "existsSync",
784
+ "exists",
785
+ "statSync",
786
+ "stat",
787
+ "lstatSync",
788
+ "lstat",
789
+ "readdirSync",
790
+ "readdir",
791
+ "mkdirSync",
792
+ "mkdir",
793
+ "unlinkSync",
794
+ "unlink",
795
+ "rmSync",
796
+ "rm",
797
+ "cpSync",
798
+ "cp",
799
+ "copyFileSync",
800
+ "copyFile",
801
+ "renameSync",
802
+ "rename",
803
+ "accessSync",
804
+ "access",
805
+ "chmodSync",
806
+ "chmod"
807
+ ]);
808
+ var STRING_METHODS = /* @__PURE__ */ new Set(["indexOf", "includes", "startsWith", "endsWith"]);
809
+ function isUrlString(value) {
810
+ return URL_PREFIXES.some((prefix) => value.startsWith(prefix));
811
+ }
812
+ function isImportOrRequire(node) {
813
+ const parent = node.parent;
814
+ if (!parent) return false;
815
+ if (parent.type === "ImportDeclaration" && parent.source === node) return true;
816
+ if (parent.type === "ImportExpression" && parent.source === node) return true;
817
+ if (parent.type === "CallExpression" && parent.callee.type === "Identifier" && parent.callee.name === "require" && parent.arguments[0] === node) {
818
+ return true;
819
+ }
820
+ return false;
821
+ }
822
+ function isInFlaggedContext(node) {
823
+ const parent = node.parent;
824
+ if (!parent) return false;
825
+ if (parent.type === "CallExpression") {
826
+ const callee = parent.callee;
827
+ if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && PATH_METHODS.has(callee.property.name)) {
828
+ return true;
829
+ }
830
+ if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && (callee.object.name === "fs" || callee.object.name === "fsp") && callee.property.type === "Identifier" && FS_METHODS.has(callee.property.name)) {
831
+ return true;
832
+ }
833
+ if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && STRING_METHODS.has(callee.property.name)) {
834
+ return true;
835
+ }
836
+ return false;
837
+ }
838
+ return false;
839
+ }
840
+ var no_hardcoded_path_separator_default = createRule10({
841
+ name: "no-hardcoded-path-separator",
842
+ meta: {
843
+ type: "problem",
844
+ docs: {
845
+ description: "Disallow hardcoded Unix path separators in path/fs method calls and string comparisons"
846
+ },
847
+ messages: {
848
+ hardcodedPathSeparator: "Avoid hardcoded Unix path separators. Use path.join(), path.sep, or path.posix/path.win32 for cross-platform compatibility."
849
+ },
850
+ schema: []
851
+ },
852
+ defaultOptions: [],
853
+ create(context) {
854
+ return {
855
+ Literal(node) {
856
+ if (typeof node.value !== "string") return;
857
+ if (!HARDCODED_SEPARATOR_PATTERN.test(node.value)) return;
858
+ if (isUrlString(node.value)) return;
859
+ if (isImportOrRequire(node)) return;
860
+ if (!isInFlaggedContext(node)) return;
861
+ context.report({
862
+ node,
863
+ messageId: "hardcodedPathSeparator"
864
+ });
865
+ }
866
+ };
867
+ }
868
+ });
869
+
870
+ // src/rules/index.ts
871
+ var rules = {
872
+ "enforce-doc-exports": enforce_doc_exports_default,
873
+ "no-circular-deps": no_circular_deps_default,
874
+ "no-forbidden-imports": no_forbidden_imports_default,
875
+ "no-hardcoded-path-separator": no_hardcoded_path_separator_default,
876
+ "no-layer-violation": no_layer_violation_default,
877
+ "no-nested-loops-in-critical": no_nested_loops_in_critical_default,
878
+ "no-sync-io-in-async": no_sync_io_in_async_default,
879
+ "no-unbounded-array-chains": no_unbounded_array_chains_default,
880
+ "no-unix-shell-command": no_unix_shell_command_default,
881
+ "require-boundary-schema": require_boundary_schema_default
882
+ };
883
+
884
+ // src/index.ts
885
+ var plugin = {
886
+ meta: {
887
+ name: "@harness-engineering/eslint-plugin",
888
+ version: "0.1.0"
889
+ },
890
+ rules,
891
+ configs: {
892
+ recommended: {
893
+ plugins: {
894
+ get "@harness-engineering"() {
895
+ return plugin;
896
+ }
897
+ },
898
+ rules: {
899
+ "@harness-engineering/no-layer-violation": "error",
900
+ "@harness-engineering/no-circular-deps": "error",
901
+ "@harness-engineering/no-forbidden-imports": "error",
902
+ "@harness-engineering/require-boundary-schema": "warn",
903
+ "@harness-engineering/enforce-doc-exports": "warn",
904
+ "@harness-engineering/no-unix-shell-command": "warn",
905
+ "@harness-engineering/no-hardcoded-path-separator": "warn"
906
+ }
907
+ },
908
+ strict: {
909
+ plugins: {
910
+ get "@harness-engineering"() {
911
+ return plugin;
912
+ }
913
+ },
914
+ rules: {
915
+ "@harness-engineering/no-layer-violation": "error",
916
+ "@harness-engineering/no-circular-deps": "error",
917
+ "@harness-engineering/no-forbidden-imports": "error",
918
+ "@harness-engineering/require-boundary-schema": "error",
919
+ "@harness-engineering/enforce-doc-exports": "error",
920
+ "@harness-engineering/no-unix-shell-command": "error",
921
+ "@harness-engineering/no-hardcoded-path-separator": "error"
922
+ }
923
+ }
924
+ }
925
+ };
926
+ var index_default = plugin;
927
+ var configs = plugin.configs;
928
+ export {
929
+ configs,
930
+ index_default as default,
931
+ rules
40
932
  };
41
- // ESM default export
42
- export default plugin;
43
- // Named exports for flexibility
44
- export { rules };
45
- export const configs = plugin.configs;
46
- //# sourceMappingURL=index.js.map