@sarj/eslint-plugin 2.2.0 → 2.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.cjs CHANGED
@@ -848,14 +848,14 @@ var no_sequential_await_default = import_utils10.ESLintUtils.RuleCreator(
848
848
  const value = node[key];
849
849
  if (Array.isArray(value)) {
850
850
  for (const child of value) {
851
- if (isNode2(child) && !isLoop(child)) {
851
+ if (isNode4(child) && !isLoop(child)) {
852
852
  const found = findAwaitInScope(child);
853
853
  if (found) {
854
854
  return found;
855
855
  }
856
856
  }
857
857
  }
858
- } else if (isNode2(value) && !isLoop(value)) {
858
+ } else if (isNode4(value) && !isLoop(value)) {
859
859
  const found = findAwaitInScope(value);
860
860
  if (found) {
861
861
  return found;
@@ -864,7 +864,7 @@ var no_sequential_await_default = import_utils10.ESLintUtils.RuleCreator(
864
864
  }
865
865
  return null;
866
866
  }
867
- function isNode2(value) {
867
+ function isNode4(value) {
868
868
  return typeof value === "object" && value !== null && typeof value.type === "string";
869
869
  }
870
870
  function checkLoop(node) {
@@ -1588,13 +1588,13 @@ function getPropertyNode(objNode, propName2) {
1588
1588
  if (!objNode || objNode.type !== "ObjectExpression") return null;
1589
1589
  for (const prop of objNode.properties) {
1590
1590
  if (prop.type !== "Property") continue;
1591
- let keyName = null;
1591
+ let keyName2 = null;
1592
1592
  if (prop.key.type === "Identifier" && !prop.computed) {
1593
- keyName = prop.key.name;
1593
+ keyName2 = prop.key.name;
1594
1594
  } else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
1595
- keyName = prop.key.value;
1595
+ keyName2 = prop.key.value;
1596
1596
  }
1597
- if (keyName === propName2) {
1597
+ if (keyName2 === propName2) {
1598
1598
  if (prop.value.type === "AssignmentPattern" || prop.value.type === "ArrayPattern" || prop.value.type === "ObjectPattern") {
1599
1599
  return null;
1600
1600
  }
@@ -1936,6 +1936,1019 @@ var zod_naming_convention_default = import_utils21.ESLintUtils.RuleCreator(
1936
1936
  }
1937
1937
  });
1938
1938
 
1939
+ // src/rules/no-cors-wildcard-with-credentials.ts
1940
+ var import_utils22 = require("@typescript-eslint/utils");
1941
+ var ACAO_HEADER = "access-control-allow-origin";
1942
+ var ACAC_HEADER = "access-control-allow-credentials";
1943
+ var HEADER_SET_METHODS = /* @__PURE__ */ new Set(["setheader", "set", "append"]);
1944
+ function isTrueLiteral(node) {
1945
+ return node.type === "Literal" && node.value === true;
1946
+ }
1947
+ function isCredentialsTrueValue(node) {
1948
+ if (node.type === "Literal") {
1949
+ if (node.value === true) {
1950
+ return true;
1951
+ }
1952
+ if (typeof node.value === "string") {
1953
+ return node.value.trim().toLowerCase() === "true";
1954
+ }
1955
+ }
1956
+ return false;
1957
+ }
1958
+ function isStarLiteral(node) {
1959
+ return node.type === "Literal" && node.value === "*";
1960
+ }
1961
+ function subtreeContainsStarLiteral(node) {
1962
+ if (isStarLiteral(node)) {
1963
+ return true;
1964
+ }
1965
+ for (const key of Object.keys(node)) {
1966
+ if (key === "parent" || key === "loc" || key === "range") {
1967
+ continue;
1968
+ }
1969
+ const value = node[key];
1970
+ if (Array.isArray(value)) {
1971
+ for (const child of value) {
1972
+ if (isNode2(child) && subtreeContainsStarLiteral(child)) {
1973
+ return true;
1974
+ }
1975
+ }
1976
+ } else if (isNode2(value) && subtreeContainsStarLiteral(value)) {
1977
+ return true;
1978
+ }
1979
+ }
1980
+ return false;
1981
+ }
1982
+ function isNode2(value) {
1983
+ return typeof value === "object" && value !== null && typeof value.type === "string";
1984
+ }
1985
+ function propertyKeyName(prop) {
1986
+ if (prop.computed) {
1987
+ return void 0;
1988
+ }
1989
+ const key = prop.key;
1990
+ if (key.type === "Identifier") {
1991
+ return key.name;
1992
+ }
1993
+ if (key.type === "Literal" && typeof key.value === "string") {
1994
+ return key.value;
1995
+ }
1996
+ return void 0;
1997
+ }
1998
+ function calleeName(node) {
1999
+ const callee = node.callee;
2000
+ if (callee.type === "Identifier") {
2001
+ return callee.name;
2002
+ }
2003
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier") {
2004
+ return callee.property.name;
2005
+ }
2006
+ return void 0;
2007
+ }
2008
+ function isCorsWildcardCredentialsCall(node) {
2009
+ const name = calleeName(node);
2010
+ if (name === void 0 || name.toLowerCase() !== "cors") {
2011
+ return false;
2012
+ }
2013
+ const options = node.arguments.find(
2014
+ (arg) => arg.type === "ObjectExpression"
2015
+ );
2016
+ if (options === void 0) {
2017
+ return false;
2018
+ }
2019
+ let hasCredentials = false;
2020
+ let hasWildcardOrigin = false;
2021
+ for (const prop of options.properties) {
2022
+ if (prop.type !== "Property") {
2023
+ continue;
2024
+ }
2025
+ const key = propertyKeyName(prop);
2026
+ if (key === "credentials" && isTrueLiteral(prop.value)) {
2027
+ hasCredentials = true;
2028
+ } else if (key === "origin" && subtreeContainsStarLiteral(prop.value)) {
2029
+ hasWildcardOrigin = true;
2030
+ }
2031
+ }
2032
+ return hasCredentials && hasWildcardOrigin;
2033
+ }
2034
+ function isWildcardCredentialsHeaderObject(node) {
2035
+ let wildcardOrigin = false;
2036
+ let credentialsTrue = false;
2037
+ for (const prop of node.properties) {
2038
+ if (prop.type !== "Property") {
2039
+ continue;
2040
+ }
2041
+ const key = propertyKeyName(prop);
2042
+ if (key === void 0) {
2043
+ continue;
2044
+ }
2045
+ const header = key.toLowerCase();
2046
+ if (header === ACAO_HEADER && isStarLiteral(prop.value)) {
2047
+ wildcardOrigin = true;
2048
+ } else if (header === ACAC_HEADER && isCredentialsTrueValue(prop.value)) {
2049
+ credentialsTrue = true;
2050
+ }
2051
+ }
2052
+ return wildcardOrigin && credentialsTrue;
2053
+ }
2054
+ function classifyHeaderSetCall(node) {
2055
+ const callee = node.callee;
2056
+ if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !HEADER_SET_METHODS.has(callee.property.name.toLowerCase())) {
2057
+ return void 0;
2058
+ }
2059
+ const [nameArg, valueArg] = node.arguments;
2060
+ if (nameArg === void 0 || valueArg === void 0 || nameArg.type !== "Literal" || typeof nameArg.value !== "string") {
2061
+ return void 0;
2062
+ }
2063
+ const header = nameArg.value.toLowerCase();
2064
+ if (header === ACAO_HEADER && isStarLiteral(valueArg)) {
2065
+ return "origin";
2066
+ }
2067
+ if (header === ACAC_HEADER && isCredentialsTrueValue(valueArg)) {
2068
+ return "credentials";
2069
+ }
2070
+ return void 0;
2071
+ }
2072
+ function enclosingScope(node) {
2073
+ let current = node.parent;
2074
+ while (current) {
2075
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
2076
+ return current;
2077
+ }
2078
+ current = current.parent;
2079
+ }
2080
+ return void 0;
2081
+ }
2082
+ var no_cors_wildcard_with_credentials_default = import_utils22.ESLintUtils.RuleCreator(
2083
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
2084
+ )({
2085
+ name: "no-cors-wildcard-with-credentials",
2086
+ meta: {
2087
+ type: "problem",
2088
+ docs: {
2089
+ description: 'Disallow CORS that reflects any Origin (`"*"`) while allowing credentials; any site could then read authenticated responses. Enumerate explicit trusted origins instead.'
2090
+ },
2091
+ schema: [],
2092
+ messages: {
2093
+ corsWildcardWithCredentials: 'CORS reflects any Origin (`"*"`) while allowing credentials \u2014 any site can read authenticated responses. Enumerate explicit trusted origins instead of using `"*"` with credentials.'
2094
+ }
2095
+ },
2096
+ defaultOptions: [],
2097
+ create(context) {
2098
+ const scopeHeaderSets = /* @__PURE__ */ new Map();
2099
+ function recordHeaderSet(node, kind) {
2100
+ const key = enclosingScope(node) ?? "module";
2101
+ let entry = scopeHeaderSets.get(key);
2102
+ if (entry === void 0) {
2103
+ entry = { originNodes: [], credentialsNodes: [] };
2104
+ scopeHeaderSets.set(key, entry);
2105
+ }
2106
+ if (kind === "origin") {
2107
+ entry.originNodes.push(node);
2108
+ } else {
2109
+ entry.credentialsNodes.push(node);
2110
+ }
2111
+ }
2112
+ return {
2113
+ NewExpression(node) {
2114
+ if (isCorsWildcardCredentialsCall(node)) {
2115
+ context.report({ node, messageId: "corsWildcardWithCredentials" });
2116
+ }
2117
+ },
2118
+ CallExpression(node) {
2119
+ if (isCorsWildcardCredentialsCall(node)) {
2120
+ context.report({ node, messageId: "corsWildcardWithCredentials" });
2121
+ return;
2122
+ }
2123
+ const kind = classifyHeaderSetCall(node);
2124
+ if (kind !== void 0) {
2125
+ recordHeaderSet(node, kind);
2126
+ }
2127
+ },
2128
+ ObjectExpression(node) {
2129
+ if (isWildcardCredentialsHeaderObject(node)) {
2130
+ context.report({ node, messageId: "corsWildcardWithCredentials" });
2131
+ }
2132
+ },
2133
+ "Program:exit"() {
2134
+ for (const { originNodes, credentialsNodes } of scopeHeaderSets.values()) {
2135
+ if (originNodes.length > 0 && credentialsNodes.length > 0) {
2136
+ for (const node of originNodes) {
2137
+ context.report({
2138
+ node,
2139
+ messageId: "corsWildcardWithCredentials"
2140
+ });
2141
+ }
2142
+ }
2143
+ }
2144
+ }
2145
+ };
2146
+ }
2147
+ });
2148
+
2149
+ // src/rules/no-fat-try-blocks.ts
2150
+ var import_utils23 = require("@typescript-eslint/utils");
2151
+ var MAX_TRY_BODY_STATEMENTS = 3;
2152
+ var NESTED_FUNCTION_TYPES = /* @__PURE__ */ new Set([
2153
+ import_utils23.AST_NODE_TYPES.FunctionDeclaration,
2154
+ import_utils23.AST_NODE_TYPES.FunctionExpression,
2155
+ import_utils23.AST_NODE_TYPES.ArrowFunctionExpression
2156
+ ]);
2157
+ var PURE_METHODS = /* @__PURE__ */ new Set([
2158
+ "map",
2159
+ "filter",
2160
+ "forEach",
2161
+ "reduce",
2162
+ "reduceRight",
2163
+ "find",
2164
+ "findIndex",
2165
+ "findLast",
2166
+ "findLastIndex",
2167
+ "some",
2168
+ "every",
2169
+ "push",
2170
+ "pop",
2171
+ "shift",
2172
+ "unshift",
2173
+ "slice",
2174
+ "splice",
2175
+ "concat",
2176
+ "flat",
2177
+ "flatMap",
2178
+ "join",
2179
+ "reverse",
2180
+ "sort",
2181
+ "fill",
2182
+ "includes",
2183
+ "indexOf",
2184
+ "lastIndexOf",
2185
+ "at",
2186
+ "keys",
2187
+ "values",
2188
+ "entries",
2189
+ "has",
2190
+ "get",
2191
+ "set",
2192
+ "add",
2193
+ "delete",
2194
+ "clear",
2195
+ "toString",
2196
+ "toLocaleString",
2197
+ "valueOf",
2198
+ "charAt",
2199
+ "charCodeAt",
2200
+ "codePointAt",
2201
+ "split",
2202
+ "padStart",
2203
+ "padEnd",
2204
+ "repeat",
2205
+ "trim",
2206
+ "trimStart",
2207
+ "trimEnd",
2208
+ "toUpperCase",
2209
+ "toLowerCase",
2210
+ "toFixed",
2211
+ "toPrecision",
2212
+ "startsWith",
2213
+ "endsWith"
2214
+ ]);
2215
+ var PURE_NAMESPACES = /* @__PURE__ */ new Set([
2216
+ "Object",
2217
+ "Array",
2218
+ "Math",
2219
+ "JSON",
2220
+ "Number",
2221
+ "String",
2222
+ "Boolean",
2223
+ "console"
2224
+ ]);
2225
+ var PURE_CONSTRUCTORS = /* @__PURE__ */ new Set([
2226
+ "Map",
2227
+ "Set",
2228
+ "WeakMap",
2229
+ "WeakSet",
2230
+ "Date",
2231
+ "Error",
2232
+ "TypeError",
2233
+ "RangeError",
2234
+ "Array",
2235
+ "Object",
2236
+ "Headers",
2237
+ "URLSearchParams",
2238
+ "FormData"
2239
+ ]);
2240
+ function isNode3(value) {
2241
+ return typeof value === "object" && value !== null && typeof value.type === "string";
2242
+ }
2243
+ function isPureCall(node) {
2244
+ const callee = node.callee;
2245
+ if (callee.type !== import_utils23.AST_NODE_TYPES.MemberExpression) {
2246
+ return false;
2247
+ }
2248
+ const property = callee.property;
2249
+ if (property.type !== import_utils23.AST_NODE_TYPES.Identifier) {
2250
+ return false;
2251
+ }
2252
+ if (callee.object.type === import_utils23.AST_NODE_TYPES.Identifier && PURE_NAMESPACES.has(callee.object.name)) {
2253
+ return true;
2254
+ }
2255
+ return PURE_METHODS.has(property.name);
2256
+ }
2257
+ function isPureNew(node) {
2258
+ return node.callee.type === import_utils23.AST_NODE_TYPES.Identifier && PURE_CONSTRUCTORS.has(node.callee.name);
2259
+ }
2260
+ function subtreeMatches(stmt, predicate) {
2261
+ let found = false;
2262
+ const visit = (current) => {
2263
+ if (found) {
2264
+ return;
2265
+ }
2266
+ if (predicate(current)) {
2267
+ found = true;
2268
+ return;
2269
+ }
2270
+ for (const key of Object.keys(current)) {
2271
+ if (key === "parent") {
2272
+ continue;
2273
+ }
2274
+ if (NESTED_FUNCTION_TYPES.has(current.type) && key === "body") {
2275
+ continue;
2276
+ }
2277
+ const value = current[key];
2278
+ if (Array.isArray(value)) {
2279
+ for (const child of value) {
2280
+ if (isNode3(child)) {
2281
+ visit(child);
2282
+ }
2283
+ }
2284
+ } else if (isNode3(value)) {
2285
+ visit(value);
2286
+ }
2287
+ if (found) {
2288
+ return;
2289
+ }
2290
+ }
2291
+ };
2292
+ visit(stmt);
2293
+ return found;
2294
+ }
2295
+ var hasAwait = (stmt) => subtreeMatches(stmt, (n) => n.type === import_utils23.AST_NODE_TYPES.AwaitExpression);
2296
+ var hasThrowingCallOrNew = (stmt) => subtreeMatches(
2297
+ stmt,
2298
+ (n) => n.type === import_utils23.AST_NODE_TYPES.CallExpression && !isPureCall(n) || n.type === import_utils23.AST_NODE_TYPES.NewExpression && !isPureNew(n)
2299
+ );
2300
+ function unwrap2(expr) {
2301
+ let current = expr;
2302
+ while (current.type === import_utils23.AST_NODE_TYPES.ChainExpression || current.type === import_utils23.AST_NODE_TYPES.TSNonNullExpression) {
2303
+ current = current.expression;
2304
+ }
2305
+ return current;
2306
+ }
2307
+ function canThrow(stmt) {
2308
+ if (hasAwait(stmt)) {
2309
+ return true;
2310
+ }
2311
+ if (stmt.type === import_utils23.AST_NODE_TYPES.ExpressionStatement && unwrap2(stmt.expression).type === import_utils23.AST_NODE_TYPES.CallExpression) {
2312
+ return false;
2313
+ }
2314
+ return hasThrowingCallOrNew(stmt);
2315
+ }
2316
+ function handlerRethrows(handler) {
2317
+ if (handler === null) {
2318
+ return false;
2319
+ }
2320
+ const body = handler.body.body;
2321
+ const last = body[body.length - 1];
2322
+ return last !== void 0 && last.type === import_utils23.AST_NODE_TYPES.ThrowStatement;
2323
+ }
2324
+ var no_fat_try_blocks_default = import_utils23.ESLintUtils.RuleCreator(
2325
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
2326
+ )({
2327
+ name: "no-fat-try-blocks",
2328
+ meta: {
2329
+ type: "problem",
2330
+ docs: {
2331
+ description: "Disallow `try` blocks with more than three top-level statements that can throw \u2014 isolate the throwing statement and move non-throwing work outside."
2332
+ },
2333
+ schema: [],
2334
+ messages: {
2335
+ fatTryBlock: "This `try` block has {{count}} statements that can throw (max {{max}}). Isolate the throwing statement(s); move non-throwing work outside the `try`."
2336
+ }
2337
+ },
2338
+ defaultOptions: [],
2339
+ create(context) {
2340
+ const sourceCode = context.sourceCode;
2341
+ return {
2342
+ TryStatement(node) {
2343
+ if (node.finalizer !== null) {
2344
+ return;
2345
+ }
2346
+ if (handlerRethrows(node.handler)) {
2347
+ return;
2348
+ }
2349
+ const count = node.block.body.filter(canThrow).length;
2350
+ if (count <= MAX_TRY_BODY_STATEMENTS) {
2351
+ return;
2352
+ }
2353
+ const tryKeyword = sourceCode.getFirstToken(node);
2354
+ context.report({
2355
+ node: tryKeyword ?? node,
2356
+ messageId: "fatTryBlock",
2357
+ data: { count, max: MAX_TRY_BODY_STATEMENTS }
2358
+ });
2359
+ }
2360
+ };
2361
+ }
2362
+ });
2363
+
2364
+ // src/rules/no-secret-in-log.ts
2365
+ var import_utils24 = require("@typescript-eslint/utils");
2366
+ var LOG_METHODS = /* @__PURE__ */ new Set([
2367
+ "debug",
2368
+ "info",
2369
+ "warn",
2370
+ "warning",
2371
+ "error",
2372
+ "exception",
2373
+ "critical",
2374
+ "trace",
2375
+ "log",
2376
+ "fatal",
2377
+ "success"
2378
+ ]);
2379
+ var LOGGER_NAMES = /* @__PURE__ */ new Set([
2380
+ "logger",
2381
+ "log",
2382
+ "logging",
2383
+ "loguru",
2384
+ "console",
2385
+ "_logger",
2386
+ "_log"
2387
+ ]);
2388
+ var LOGGER_FACTORIES = /* @__PURE__ */ new Set(["getlogger", "get_logger"]);
2389
+ var SECRET_WORDS = /* @__PURE__ */ new Set([
2390
+ "token",
2391
+ "secret",
2392
+ "password",
2393
+ "passwd",
2394
+ "jwt",
2395
+ "secrets",
2396
+ "passwords",
2397
+ "credential",
2398
+ "credentials",
2399
+ "authorization",
2400
+ "signature",
2401
+ "hmac",
2402
+ "digest",
2403
+ "hash",
2404
+ "apikey"
2405
+ ]);
2406
+ var INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
2407
+ "count",
2408
+ "counts",
2409
+ "budget",
2410
+ "limit",
2411
+ "limits",
2412
+ "id",
2413
+ "ids",
2414
+ "enabled",
2415
+ "disabled",
2416
+ "flag",
2417
+ "flags",
2418
+ "present",
2419
+ "set",
2420
+ "unset",
2421
+ "configured",
2422
+ "missing",
2423
+ "required",
2424
+ "valid",
2425
+ "invalid",
2426
+ "exists",
2427
+ "type",
2428
+ "types"
2429
+ ]);
2430
+ var REDACTION_RE = /prefix|suffix|redact|mask|hash|hint|_len|length/i;
2431
+ var WHOLE_TOKEN_REDACTION_MARKERS = /* @__PURE__ */ new Set(["tag"]);
2432
+ var CAMEL_RE = /[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+/g;
2433
+ var SEGMENT_RE = /[^A-Za-z0-9]+/;
2434
+ function tokenize(identifier) {
2435
+ const tokens = [];
2436
+ for (const segment of identifier.split(SEGMENT_RE)) {
2437
+ if (!segment) {
2438
+ continue;
2439
+ }
2440
+ tokens.push(segment.toLowerCase());
2441
+ for (const part of segment.match(CAMEL_RE) ?? []) {
2442
+ tokens.push(part.toLowerCase());
2443
+ }
2444
+ }
2445
+ return tokens;
2446
+ }
2447
+ function hasApiKey(tokens) {
2448
+ for (let i = 0; i + 1 < tokens.length; i++) {
2449
+ if (tokens[i] === "api" && tokens[i + 1] === "key") {
2450
+ return true;
2451
+ }
2452
+ }
2453
+ return false;
2454
+ }
2455
+ function isSecretName(identifier) {
2456
+ const tokens = tokenize(identifier);
2457
+ const last = tokens.at(-1);
2458
+ if (last !== void 0 && INNOCUOUS_WORDS.has(last)) {
2459
+ return false;
2460
+ }
2461
+ if (tokens.some((tok) => SECRET_WORDS.has(tok))) {
2462
+ return true;
2463
+ }
2464
+ return hasApiKey(tokens);
2465
+ }
2466
+ function isSecretKeyword(name) {
2467
+ if (REDACTION_RE.test(name)) {
2468
+ return false;
2469
+ }
2470
+ if (tokenize(name).some((tok) => WHOLE_TOKEN_REDACTION_MARKERS.has(tok))) {
2471
+ return false;
2472
+ }
2473
+ return isSecretName(name);
2474
+ }
2475
+ function isLoggerExpr(expr) {
2476
+ switch (expr.type) {
2477
+ case "Identifier":
2478
+ return LOGGER_NAMES.has(expr.name.toLowerCase());
2479
+ case "MemberExpression": {
2480
+ const { property, object } = expr;
2481
+ if (!expr.computed && property.type === "Identifier") {
2482
+ const lowered = property.name.toLowerCase();
2483
+ if (LOGGER_NAMES.has(lowered) || LOGGER_FACTORIES.has(lowered)) {
2484
+ return true;
2485
+ }
2486
+ }
2487
+ return isLoggerExpr(object);
2488
+ }
2489
+ case "CallExpression": {
2490
+ const callee = expr.callee;
2491
+ if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && LOGGER_FACTORIES.has(callee.property.name.toLowerCase())) {
2492
+ return true;
2493
+ }
2494
+ if (callee.type !== "Super") {
2495
+ return isLoggerExpr(callee);
2496
+ }
2497
+ return false;
2498
+ }
2499
+ default:
2500
+ return false;
2501
+ }
2502
+ }
2503
+ function propertyKeyName2(prop) {
2504
+ if (prop.computed) {
2505
+ return null;
2506
+ }
2507
+ if (prop.key.type === "Identifier") {
2508
+ return prop.key.name;
2509
+ }
2510
+ if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
2511
+ return prop.key.value;
2512
+ }
2513
+ return null;
2514
+ }
2515
+ var no_secret_in_log_default = import_utils24.ESLintUtils.RuleCreator(
2516
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
2517
+ )({
2518
+ name: "no-secret-in-log",
2519
+ meta: {
2520
+ type: "problem",
2521
+ docs: {
2522
+ description: "Disallow passing a secret-named value to a logging call; it leaks to log sinks. Redact or omit it."
2523
+ },
2524
+ schema: [],
2525
+ messages: {
2526
+ noSecretInLog: "Secret `{{name}}` passed to a logging call leaks it to log sinks. Redact (e.g. `{{name}}Prefix: {{name}}.slice(0, 6)`) or omit it."
2527
+ }
2528
+ },
2529
+ defaultOptions: [],
2530
+ create(context) {
2531
+ return {
2532
+ CallExpression(node) {
2533
+ const callee = node.callee;
2534
+ if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier" || !LOG_METHODS.has(callee.property.name)) {
2535
+ return;
2536
+ }
2537
+ if (!isLoggerExpr(callee.object)) {
2538
+ return;
2539
+ }
2540
+ for (const arg of node.arguments) {
2541
+ if (arg.type === "Identifier") {
2542
+ if (isSecretKeyword(arg.name)) {
2543
+ context.report({
2544
+ node: arg,
2545
+ messageId: "noSecretInLog",
2546
+ data: { name: arg.name }
2547
+ });
2548
+ }
2549
+ continue;
2550
+ }
2551
+ if (arg.type === "ObjectExpression") {
2552
+ for (const prop of arg.properties) {
2553
+ if (prop.type !== "Property") {
2554
+ continue;
2555
+ }
2556
+ const keyName2 = propertyKeyName2(prop);
2557
+ if (keyName2 !== null && isSecretKeyword(keyName2)) {
2558
+ context.report({
2559
+ node: prop,
2560
+ messageId: "noSecretInLog",
2561
+ data: { name: keyName2 }
2562
+ });
2563
+ }
2564
+ }
2565
+ }
2566
+ }
2567
+ }
2568
+ };
2569
+ }
2570
+ });
2571
+
2572
+ // src/rules/prefer-string-literal-union.ts
2573
+ var import_utils25 = require("@typescript-eslint/utils");
2574
+ var CHOICE_TOKENS = /* @__PURE__ */ new Set([
2575
+ "status",
2576
+ "state",
2577
+ "kind",
2578
+ "role",
2579
+ "priority",
2580
+ "severity",
2581
+ "direction",
2582
+ "tier",
2583
+ "stage",
2584
+ "type",
2585
+ "mode",
2586
+ "level"
2587
+ ]);
2588
+ var LOWER_TOKEN_RE = /^[a-z][a-z0-9_-]{0,30}$/;
2589
+ var MIN_CLUSTER_SIZE = 2;
2590
+ var IGNORE_PATTERNS = [
2591
+ /[\\/]generated[\\/]/,
2592
+ /\.gen\.tsx?$/,
2593
+ /\.generated\.tsx?$/,
2594
+ /\.d\.ts$/
2595
+ ];
2596
+ function isIgnoredFile(filename, sourceText) {
2597
+ if (IGNORE_PATTERNS.some((re) => re.test(filename))) {
2598
+ return true;
2599
+ }
2600
+ return /@generated\b/.test(sourceText.slice(0, 1024));
2601
+ }
2602
+ function lastWord(name) {
2603
+ const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\s]+/).filter((w) => w.length > 0);
2604
+ const last = words[words.length - 1] ?? name;
2605
+ return last.toLowerCase();
2606
+ }
2607
+ function isChoiceLikeName(name) {
2608
+ return CHOICE_TOKENS.has(lastWord(name));
2609
+ }
2610
+ function keyName(key) {
2611
+ if (key.type === import_utils25.AST_NODE_TYPES.Identifier) {
2612
+ return key.name;
2613
+ }
2614
+ if (key.type === import_utils25.AST_NODE_TYPES.Literal && typeof key.value === "string") {
2615
+ return key.value;
2616
+ }
2617
+ return null;
2618
+ }
2619
+ function isStringLiteralUnion(node) {
2620
+ if (node?.type !== import_utils25.AST_NODE_TYPES.TSUnionType) {
2621
+ return false;
2622
+ }
2623
+ const stringMembers = node.types.filter(
2624
+ (t) => t.type === import_utils25.AST_NODE_TYPES.TSLiteralType && t.literal.type === import_utils25.AST_NODE_TYPES.Literal && typeof t.literal.value === "string"
2625
+ );
2626
+ return stringMembers.length >= MIN_CLUSTER_SIZE;
2627
+ }
2628
+ function refKey(node) {
2629
+ if (node.type === import_utils25.AST_NODE_TYPES.Identifier) {
2630
+ return node.name;
2631
+ }
2632
+ if (node.type === import_utils25.AST_NODE_TYPES.MemberExpression && !node.computed) {
2633
+ const inner = refKey(node.object);
2634
+ if (inner === null || node.property.type !== import_utils25.AST_NODE_TYPES.Identifier) {
2635
+ return null;
2636
+ }
2637
+ return `${inner}.${node.property.name}`;
2638
+ }
2639
+ return null;
2640
+ }
2641
+ function strLiteral(node) {
2642
+ if (node.type === import_utils25.AST_NODE_TYPES.Literal && typeof node.value === "string") {
2643
+ return node.value;
2644
+ }
2645
+ return null;
2646
+ }
2647
+ var prefer_string_literal_union_default = import_utils25.ESLintUtils.RuleCreator(
2648
+ (name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
2649
+ )({
2650
+ name: "prefer-string-literal-union",
2651
+ meta: {
2652
+ type: "suggestion",
2653
+ docs: {
2654
+ description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
2655
+ },
2656
+ schema: [],
2657
+ messages: {
2658
+ bareChoiceField: '`{{name}}: string` looks like a choice field \u2014 prefer a string-literal union type (e.g. `type X = "a" | "b"`). Enums are banned by `no-enum`; use a union.',
2659
+ comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
2660
+ }
2661
+ },
2662
+ defaultOptions: [],
2663
+ create(context) {
2664
+ const filename = context.filename;
2665
+ const sourceText = context.sourceCode.getText();
2666
+ if (isIgnoredFile(filename, sourceText)) {
2667
+ return {};
2668
+ }
2669
+ const scopeStack = [];
2670
+ const validClusters = [];
2671
+ const bareChoiceProps = [];
2672
+ const containersWithUnion = /* @__PURE__ */ new Set();
2673
+ function pushScope() {
2674
+ scopeStack.push(/* @__PURE__ */ new Map());
2675
+ }
2676
+ function popScope() {
2677
+ const clusters = scopeStack.pop();
2678
+ if (clusters === void 0) {
2679
+ return;
2680
+ }
2681
+ for (const entry of clusters.values()) {
2682
+ if (entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE) {
2683
+ validClusters.push(entry.node);
2684
+ }
2685
+ }
2686
+ }
2687
+ function accumulate(key, literals, node) {
2688
+ const scope = scopeStack[scopeStack.length - 1];
2689
+ if (scope === void 0) {
2690
+ return;
2691
+ }
2692
+ const allTokens = literals.every((lit) => LOWER_TOKEN_RE.test(lit));
2693
+ const existing = scope.get(key);
2694
+ if (existing === void 0) {
2695
+ scope.set(key, {
2696
+ node,
2697
+ literals: new Set(literals),
2698
+ allTokens
2699
+ });
2700
+ return;
2701
+ }
2702
+ for (const lit of literals) {
2703
+ existing.literals.add(lit);
2704
+ }
2705
+ existing.allTokens = existing.allTokens && allTokens;
2706
+ }
2707
+ function collectProperty(key, typeNode, container, node) {
2708
+ if (isStringLiteralUnion(typeNode)) {
2709
+ containersWithUnion.add(container);
2710
+ return;
2711
+ }
2712
+ if (typeNode?.type !== import_utils25.AST_NODE_TYPES.TSStringKeyword) {
2713
+ return;
2714
+ }
2715
+ const name = keyName(key);
2716
+ if (name === null || !isChoiceLikeName(name)) {
2717
+ return;
2718
+ }
2719
+ bareChoiceProps.push({ name, container, node });
2720
+ }
2721
+ return {
2722
+ FunctionDeclaration: pushScope,
2723
+ "FunctionDeclaration:exit": popScope,
2724
+ FunctionExpression: pushScope,
2725
+ "FunctionExpression:exit": popScope,
2726
+ ArrowFunctionExpression: pushScope,
2727
+ "ArrowFunctionExpression:exit": popScope,
2728
+ BinaryExpression(node) {
2729
+ if (node.operator !== "===" && node.operator !== "!==" && node.operator !== "==" && node.operator !== "!=") {
2730
+ return;
2731
+ }
2732
+ const leftKey = refKey(node.left);
2733
+ const rightLit = strLiteral(node.right);
2734
+ const rightKey = refKey(node.right);
2735
+ const leftLit = strLiteral(node.left);
2736
+ if (leftKey !== null && rightLit !== null) {
2737
+ accumulate(leftKey, [rightLit], node);
2738
+ } else if (rightKey !== null && leftLit !== null) {
2739
+ accumulate(rightKey, [leftLit], node);
2740
+ }
2741
+ },
2742
+ SwitchStatement(node) {
2743
+ const key = refKey(node.discriminant);
2744
+ if (key === null) {
2745
+ return;
2746
+ }
2747
+ const literals = [];
2748
+ for (const c of node.cases) {
2749
+ if (c.test !== null) {
2750
+ const lit = strLiteral(c.test);
2751
+ if (lit !== null) {
2752
+ literals.push(lit);
2753
+ }
2754
+ }
2755
+ }
2756
+ if (literals.length > 0) {
2757
+ accumulate(key, literals, node);
2758
+ }
2759
+ },
2760
+ TSPropertySignature(node) {
2761
+ collectProperty(
2762
+ node.key,
2763
+ node.typeAnnotation?.typeAnnotation,
2764
+ node.parent,
2765
+ node
2766
+ );
2767
+ },
2768
+ PropertyDefinition(node) {
2769
+ collectProperty(
2770
+ node.key,
2771
+ node.typeAnnotation?.typeAnnotation,
2772
+ node.parent,
2773
+ node
2774
+ );
2775
+ },
2776
+ "Program:exit"() {
2777
+ for (const clusterNode of validClusters) {
2778
+ context.report({
2779
+ node: clusterNode,
2780
+ messageId: "comparisonCluster",
2781
+ data: { key: refKeyText(clusterNode) }
2782
+ });
2783
+ }
2784
+ for (const prop of bareChoiceProps) {
2785
+ if (containersWithUnion.has(prop.container)) {
2786
+ context.report({
2787
+ node: prop.node,
2788
+ messageId: "bareChoiceField",
2789
+ data: { name: prop.name }
2790
+ });
2791
+ }
2792
+ }
2793
+ }
2794
+ };
2795
+ function refKeyText(node) {
2796
+ if (node.type === import_utils25.AST_NODE_TYPES.BinaryExpression) {
2797
+ return refKey(node.left) ?? refKey(node.right) ?? "value";
2798
+ }
2799
+ if (node.type === import_utils25.AST_NODE_TYPES.SwitchStatement) {
2800
+ return refKey(node.discriminant) ?? "value";
2801
+ }
2802
+ return "value";
2803
+ }
2804
+ }
2805
+ });
2806
+
2807
+ // src/rules/single-public-export.ts
2808
+ var import_utils26 = require("@typescript-eslint/utils");
2809
+ var JUNK_DRAWER_STEMS = /* @__PURE__ */ new Set([
2810
+ "util",
2811
+ "utils",
2812
+ "helper",
2813
+ "helpers",
2814
+ "common",
2815
+ "constant",
2816
+ "constants",
2817
+ "type",
2818
+ "types",
2819
+ "model",
2820
+ "models",
2821
+ "shared",
2822
+ "misc"
2823
+ ]);
2824
+ var CONVENTIONAL_BUCKET_EXPORTS = /* @__PURE__ */ new Set(["cn"]);
2825
+ var ACRONYM_OVERRIDES = [
2826
+ [/OAuth/g, "Oauth"],
2827
+ [/GraphQL/g, "Graphql"],
2828
+ [/gRPC/g, "Grpc"]
2829
+ ];
2830
+ var CAMEL_BOUNDARY_RE = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
2831
+ var TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
2832
+ var SCRIPT_EXT_RE = /\.[cm]?[jt]sx?$/i;
2833
+ var basename = (filename) => filename.split(/[/\\]/).pop() ?? filename;
2834
+ var stemOf = (base) => base.replace(SCRIPT_EXT_RE, "");
2835
+ var kebabCase = (name) => {
2836
+ let normalized = name;
2837
+ for (const [pattern, replacement] of ACRONYM_OVERRIDES) {
2838
+ normalized = normalized.replace(pattern, replacement);
2839
+ }
2840
+ return normalized.replace(CAMEL_BOUNDARY_RE, "-").toLowerCase();
2841
+ };
2842
+ var isFunctionExpression2 = (node) => node !== null && (node.type === import_utils26.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils26.AST_NODE_TYPES.FunctionExpression);
2843
+ var functionConstName = (decl) => {
2844
+ if (decl.declarations.length !== 1) return null;
2845
+ const [declarator] = decl.declarations;
2846
+ if (declarator === void 0) return null;
2847
+ if (declarator.id.type !== import_utils26.AST_NODE_TYPES.Identifier) return null;
2848
+ if (!isFunctionExpression2(declarator.init)) return null;
2849
+ return declarator.id.name;
2850
+ };
2851
+ var summarizeExports = (body) => {
2852
+ let names = 0;
2853
+ let hasReExport = false;
2854
+ let candidate = null;
2855
+ const addCandidate = (name, node) => {
2856
+ names += 1;
2857
+ candidate = { name, node };
2858
+ };
2859
+ for (const statement of body) {
2860
+ switch (statement.type) {
2861
+ case import_utils26.AST_NODE_TYPES.ExportAllDeclaration:
2862
+ hasReExport = true;
2863
+ break;
2864
+ case import_utils26.AST_NODE_TYPES.ExportDefaultDeclaration: {
2865
+ names += 1;
2866
+ const decl = statement.declaration;
2867
+ if (decl.type === import_utils26.AST_NODE_TYPES.FunctionDeclaration && decl.id !== null) {
2868
+ candidate = { name: decl.id.name, node: statement };
2869
+ } else if (decl.type === import_utils26.AST_NODE_TYPES.ClassDeclaration && decl.id !== null) {
2870
+ candidate = { name: decl.id.name, node: statement };
2871
+ }
2872
+ break;
2873
+ }
2874
+ case import_utils26.AST_NODE_TYPES.ExportNamedDeclaration: {
2875
+ if (statement.source !== null) {
2876
+ hasReExport = true;
2877
+ break;
2878
+ }
2879
+ const decl = statement.declaration;
2880
+ if (decl === null) {
2881
+ names += statement.specifiers.length;
2882
+ break;
2883
+ }
2884
+ switch (decl.type) {
2885
+ case import_utils26.AST_NODE_TYPES.FunctionDeclaration:
2886
+ if (decl.id !== null) addCandidate(decl.id.name, statement);
2887
+ else names += 1;
2888
+ break;
2889
+ case import_utils26.AST_NODE_TYPES.ClassDeclaration:
2890
+ if (decl.id !== null) addCandidate(decl.id.name, statement);
2891
+ else names += 1;
2892
+ break;
2893
+ case import_utils26.AST_NODE_TYPES.VariableDeclaration: {
2894
+ const fnName = functionConstName(decl);
2895
+ if (fnName !== null && decl.declarations.length === 1) {
2896
+ addCandidate(fnName, statement);
2897
+ } else {
2898
+ names += decl.declarations.length;
2899
+ }
2900
+ break;
2901
+ }
2902
+ default:
2903
+ names += 1;
2904
+ }
2905
+ break;
2906
+ }
2907
+ default:
2908
+ break;
2909
+ }
2910
+ }
2911
+ return { names, hasReExport, candidate };
2912
+ };
2913
+ var single_public_export_default = import_utils26.ESLintUtils.RuleCreator(
2914
+ (name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
2915
+ )({
2916
+ name: "single-public-export",
2917
+ meta: {
2918
+ type: "suggestion",
2919
+ docs: {
2920
+ description: "A junk-drawer module stem (`utils`, `helpers`, `types`, ...) with a single public function/class/const export should be renamed after that export."
2921
+ },
2922
+ schema: [],
2923
+ messages: {
2924
+ renameJunkDrawer: "Module stem `{{stem}}` is a generic junk-drawer name; its sole public export is `{{name}}` \u2014 rename the file to `{{expected}}.ts` to describe its responsibility."
2925
+ }
2926
+ },
2927
+ defaultOptions: [],
2928
+ create(context) {
2929
+ const base = basename(context.filename);
2930
+ if (base.endsWith(".d.ts")) return {};
2931
+ if (TEST_FILE_RE.test(base)) return {};
2932
+ const stem = stemOf(base);
2933
+ if (!JUNK_DRAWER_STEMS.has(stem.toLowerCase())) return {};
2934
+ return {
2935
+ Program(node) {
2936
+ const { names, hasReExport, candidate } = summarizeExports(node.body);
2937
+ if (hasReExport) return;
2938
+ if (names !== 1 || candidate === null) return;
2939
+ if (CONVENTIONAL_BUCKET_EXPORTS.has(candidate.name)) return;
2940
+ const expected = kebabCase(candidate.name);
2941
+ if (stem === expected) return;
2942
+ context.report({
2943
+ node: candidate.node,
2944
+ messageId: "renameJunkDrawer",
2945
+ data: { stem, name: candidate.name, expected }
2946
+ });
2947
+ }
2948
+ };
2949
+ }
2950
+ });
2951
+
1939
2952
  // src/index.ts
1940
2953
  var rules = {
1941
2954
  "enforce-file-structure": enforce_file_structure_default,
@@ -1957,12 +2970,17 @@ var rules = {
1957
2970
  "prefer-shadcn": prefer_shadcn_default,
1958
2971
  "require-assert-never": require_assert_never_default,
1959
2972
  "require-zod-form-validation": require_zod_form_validation_default,
1960
- "zod-naming-convention": zod_naming_convention_default
2973
+ "zod-naming-convention": zod_naming_convention_default,
2974
+ "no-cors-wildcard-with-credentials": no_cors_wildcard_with_credentials_default,
2975
+ "no-fat-try-blocks": no_fat_try_blocks_default,
2976
+ "no-secret-in-log": no_secret_in_log_default,
2977
+ "prefer-string-literal-union": prefer_string_literal_union_default,
2978
+ "single-public-export": single_public_export_default
1961
2979
  };
1962
2980
  var plugin = {
1963
2981
  meta: {
1964
2982
  name: "@sarj/eslint-plugin",
1965
- version: "2.2.0"
2983
+ version: "2.3.1"
1966
2984
  },
1967
2985
  rules,
1968
2986
  configs: {
@@ -1987,7 +3005,13 @@ var plugin = {
1987
3005
  "@sarj/prefer-discriminated-union": "warn",
1988
3006
  "@sarj/no-comment-cruft": "warn",
1989
3007
  // Frontend / styling — distilled from frontend PR-review mining.
1990
- "@sarj/prefer-semantic-colors": "warn"
3008
+ "@sarj/prefer-semantic-colors": "warn",
3009
+ // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
3010
+ "@sarj/no-fat-try-blocks": "warn",
3011
+ "@sarj/no-cors-wildcard-with-credentials": "warn",
3012
+ "@sarj/no-secret-in-log": "warn",
3013
+ "@sarj/single-public-export": "warn",
3014
+ "@sarj/prefer-string-literal-union": "warn"
1991
3015
  }
1992
3016
  },
1993
3017
  strict: {
@@ -2015,7 +3039,14 @@ var plugin = {
2015
3039
  "@sarj/no-comment-cruft": "error",
2016
3040
  // Frontend / styling — distilled from frontend PR-review mining. Stylistic,
2017
3041
  // no autofix → warn (rollout should prove the FP rate before raising it).
2018
- "@sarj/prefer-semantic-colors": "warn"
3042
+ "@sarj/prefer-semantic-colors": "warn",
3043
+ // Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
3044
+ "@sarj/no-fat-try-blocks": "error",
3045
+ "@sarj/no-cors-wildcard-with-credentials": "error",
3046
+ "@sarj/no-secret-in-log": "error",
3047
+ "@sarj/single-public-export": "error",
3048
+ // High-volume/stylistic — warn until rollout proves FP rate.
3049
+ "@sarj/prefer-string-literal-union": "warn"
2019
3050
  }
2020
3051
  }
2021
3052
  }