@harness-engineering/eslint-plugin 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -89,6 +89,14 @@ Create `harness.config.json` in your project root:
89
89
  | `no-sync-io-in-async` | Disallow synchronous I/O in async functions | warn |
90
90
  | `no-unbounded-array-chains` | Disallow unbounded array method chains | warn |
91
91
 
92
+ ### Cross-Platform Rules
93
+
94
+ | Rule | Description | Default |
95
+ | ----------------------------- | --------------------------------------------- | ------- |
96
+ | `no-unix-shell-command` | Disallow Unix-specific shell commands | warn |
97
+ | `no-hardcoded-path-separator` | Disallow hardcoded path separators | warn |
98
+ | `require-path-normalization` | Require path normalization for cross-platform | warn |
99
+
92
100
  ## Configs
93
101
 
94
102
  - **recommended**: Architecture rules as errors, others as warnings
package/dist/index.d.ts CHANGED
@@ -34,6 +34,9 @@ declare const rules: {
34
34
  'require-boundary-schema': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSchema", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
35
35
  name: string;
36
36
  };
37
+ 'require-path-normalization': _typescript_eslint_utils_ts_eslint.RuleModule<"missingNormalization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
38
+ name: string;
39
+ };
37
40
  };
38
41
 
39
42
  declare const plugin: {
@@ -75,6 +78,9 @@ declare const plugin: {
75
78
  'require-boundary-schema': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSchema", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
76
79
  name: string;
77
80
  };
81
+ 'require-path-normalization': _typescript_eslint_utils_ts_eslint.RuleModule<"missingNormalization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
82
+ name: string;
83
+ };
78
84
  };
79
85
  configs: {
80
86
  recommended: {
@@ -89,6 +95,7 @@ declare const plugin: {
89
95
  '@harness-engineering/enforce-doc-exports': string;
90
96
  '@harness-engineering/no-unix-shell-command': string;
91
97
  '@harness-engineering/no-hardcoded-path-separator': string;
98
+ '@harness-engineering/require-path-normalization': string;
92
99
  };
93
100
  };
94
101
  strict: {
@@ -103,6 +110,7 @@ declare const plugin: {
103
110
  '@harness-engineering/enforce-doc-exports': string;
104
111
  '@harness-engineering/no-unix-shell-command': string;
105
112
  '@harness-engineering/no-hardcoded-path-separator': string;
113
+ '@harness-engineering/require-path-normalization': string;
106
114
  };
107
115
  };
108
116
  };
@@ -150,6 +158,9 @@ declare const configs: {
150
158
  'require-boundary-schema': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSchema", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
151
159
  name: string;
152
160
  };
161
+ 'require-path-normalization': _typescript_eslint_utils_ts_eslint.RuleModule<"missingNormalization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
162
+ name: string;
163
+ };
153
164
  };
154
165
  configs: /*elided*/ any;
155
166
  };
@@ -162,6 +173,7 @@ declare const configs: {
162
173
  '@harness-engineering/enforce-doc-exports': string;
163
174
  '@harness-engineering/no-unix-shell-command': string;
164
175
  '@harness-engineering/no-hardcoded-path-separator': string;
176
+ '@harness-engineering/require-path-normalization': string;
165
177
  };
166
178
  };
167
179
  strict: {
@@ -205,6 +217,9 @@ declare const configs: {
205
217
  'require-boundary-schema': _typescript_eslint_utils_ts_eslint.RuleModule<"missingSchema", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
206
218
  name: string;
207
219
  };
220
+ 'require-path-normalization': _typescript_eslint_utils_ts_eslint.RuleModule<"missingNormalization", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
221
+ name: string;
222
+ };
208
223
  };
209
224
  configs: /*elided*/ any;
210
225
  };
@@ -217,6 +232,7 @@ declare const configs: {
217
232
  '@harness-engineering/enforce-doc-exports': string;
218
233
  '@harness-engineering/no-unix-shell-command': string;
219
234
  '@harness-engineering/no-hardcoded-path-separator': string;
235
+ '@harness-engineering/require-path-normalization': string;
220
236
  };
221
237
  };
222
238
  };
package/dist/index.js CHANGED
@@ -23,34 +23,41 @@ function hasJSDocComment(node, sourceCode) {
23
23
  }
24
24
  return foundJSDoc;
25
25
  }
26
+ var SKIP_KEYS = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
27
+ var ZOD_PARSE_METHODS = /* @__PURE__ */ new Set(["parse", "safeParse"]);
28
+ function isZodParseCall(node) {
29
+ if (node.type !== AST_NODE_TYPES.CallExpression) return false;
30
+ const callee = node.callee;
31
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
32
+ const prop = callee.property;
33
+ if (prop.type !== AST_NODE_TYPES.Identifier) return false;
34
+ return ZOD_PARSE_METHODS.has(prop.name);
35
+ }
36
+ function visitChildren(node, visitor) {
37
+ for (const key of Object.keys(node)) {
38
+ if (SKIP_KEYS.has(key)) continue;
39
+ const value = node[key];
40
+ if (!value || typeof value !== "object") continue;
41
+ if (Array.isArray(value)) {
42
+ for (const item of value) {
43
+ if (item && typeof item === "object" && "type" in item) {
44
+ visitor(item);
45
+ }
46
+ }
47
+ } else if ("type" in value) {
48
+ visitor(value);
49
+ }
50
+ }
51
+ }
26
52
  function hasZodValidation(body) {
27
53
  let found = false;
28
- const skipKeys = /* @__PURE__ */ new Set(["parent", "loc", "range", "tokens", "comments"]);
29
54
  function visit(node) {
30
55
  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
- }
56
+ if (isZodParseCall(node)) {
57
+ found = true;
58
+ return;
53
59
  }
60
+ visitChildren(node, visit);
54
61
  }
55
62
  visit(body);
56
63
  return found;
@@ -106,30 +113,34 @@ var enforce_doc_exports_default = createRule({
106
113
  });
107
114
  }
108
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
+ }
109
138
  return {
110
139
  ExportNamedDeclaration(node) {
111
140
  const decl = node.declaration;
112
141
  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);
142
+ for (const info of getExportInfo(decl)) {
143
+ checkExport(node, info.kind, info.name);
133
144
  }
134
145
  }
135
146
  };
@@ -819,21 +830,21 @@ function isImportOrRequire(node) {
819
830
  }
820
831
  return false;
821
832
  }
833
+ function isMemberCall(callee, objectNames, methods) {
834
+ return callee.object.type === "Identifier" && objectNames.has(callee.object.name) && callee.property.type === "Identifier" && methods.has(callee.property.name);
835
+ }
836
+ var PATH_OBJECTS = /* @__PURE__ */ new Set(["path"]);
837
+ var FS_OBJECTS = /* @__PURE__ */ new Set(["fs", "fsp"]);
822
838
  function isInFlaggedContext(node) {
823
839
  const parent = node.parent;
824
840
  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;
841
+ if (parent.type !== "CallExpression") return false;
842
+ const callee = parent.callee;
843
+ if (callee.type !== "MemberExpression") return false;
844
+ if (isMemberCall(callee, PATH_OBJECTS, PATH_METHODS)) return true;
845
+ if (isMemberCall(callee, FS_OBJECTS, FS_METHODS)) return true;
846
+ if (callee.property.type === "Identifier" && STRING_METHODS.has(callee.property.name)) {
847
+ return true;
837
848
  }
838
849
  return false;
839
850
  }
@@ -867,6 +878,63 @@ var no_hardcoded_path_separator_default = createRule10({
867
878
  }
868
879
  });
869
880
 
881
+ // src/rules/require-path-normalization.ts
882
+ import { ESLintUtils as ESLintUtils11 } from "@typescript-eslint/utils";
883
+ var createRule11 = ESLintUtils11.RuleCreator(
884
+ (name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`
885
+ );
886
+ function isRelativeCall(node) {
887
+ const callee = node.callee;
888
+ if (callee.type === "Identifier" && callee.name === "relative") {
889
+ return true;
890
+ }
891
+ if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "path" && callee.property.type === "Identifier" && callee.property.name === "relative") {
892
+ return true;
893
+ }
894
+ return false;
895
+ }
896
+ function isReplaceMethod(parent) {
897
+ return parent.type === "MemberExpression" && parent.property.type === "Identifier" && (parent.property.name === "replaceAll" || parent.property.name === "replace");
898
+ }
899
+ function isBackslashNormalizationArg(firstArg) {
900
+ if (firstArg.type === "Literal" && firstArg.value === "\\") return true;
901
+ if ("regex" in firstArg && firstArg.type === "Literal") return true;
902
+ return false;
903
+ }
904
+ function hasNormalization(node) {
905
+ const parent = node.parent;
906
+ if (!parent || !isReplaceMethod(parent)) return false;
907
+ const grandparent = parent.parent;
908
+ if (grandparent?.type !== "CallExpression" || grandparent.arguments.length < 1) return false;
909
+ return isBackslashNormalizationArg(grandparent.arguments[0]);
910
+ }
911
+ var require_path_normalization_default = createRule11({
912
+ name: "require-path-normalization",
913
+ meta: {
914
+ type: "problem",
915
+ docs: {
916
+ description: 'Require path.relative() results to be normalized with .replaceAll("\\\\", "/") for cross-platform safety'
917
+ },
918
+ messages: {
919
+ missingNormalization: "path.relative() result must be normalized with .replaceAll('\\\\', '/') for cross-platform compatibility."
920
+ },
921
+ schema: []
922
+ },
923
+ defaultOptions: [],
924
+ create(context) {
925
+ return {
926
+ CallExpression(node) {
927
+ if (!isRelativeCall(node)) return;
928
+ if (hasNormalization(node)) return;
929
+ context.report({
930
+ node,
931
+ messageId: "missingNormalization"
932
+ });
933
+ }
934
+ };
935
+ }
936
+ });
937
+
870
938
  // src/rules/index.ts
871
939
  var rules = {
872
940
  "enforce-doc-exports": enforce_doc_exports_default,
@@ -878,7 +946,8 @@ var rules = {
878
946
  "no-sync-io-in-async": no_sync_io_in_async_default,
879
947
  "no-unbounded-array-chains": no_unbounded_array_chains_default,
880
948
  "no-unix-shell-command": no_unix_shell_command_default,
881
- "require-boundary-schema": require_boundary_schema_default
949
+ "require-boundary-schema": require_boundary_schema_default,
950
+ "require-path-normalization": require_path_normalization_default
882
951
  };
883
952
 
884
953
  // src/index.ts
@@ -902,7 +971,8 @@ var plugin = {
902
971
  "@harness-engineering/require-boundary-schema": "warn",
903
972
  "@harness-engineering/enforce-doc-exports": "warn",
904
973
  "@harness-engineering/no-unix-shell-command": "warn",
905
- "@harness-engineering/no-hardcoded-path-separator": "warn"
974
+ "@harness-engineering/no-hardcoded-path-separator": "warn",
975
+ "@harness-engineering/require-path-normalization": "warn"
906
976
  }
907
977
  },
908
978
  strict: {
@@ -918,7 +988,8 @@ var plugin = {
918
988
  "@harness-engineering/require-boundary-schema": "error",
919
989
  "@harness-engineering/enforce-doc-exports": "error",
920
990
  "@harness-engineering/no-unix-shell-command": "error",
921
- "@harness-engineering/no-hardcoded-path-separator": "error"
991
+ "@harness-engineering/no-hardcoded-path-separator": "error",
992
+ "@harness-engineering/require-path-normalization": "error"
922
993
  }
923
994
  }
924
995
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/eslint-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "ESLint plugin for harness engineering architectural constraints",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",