@sarj/eslint-plugin 2.2.0 → 2.3.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.cjs +1156 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +1162 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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 (
|
|
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 (
|
|
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
|
|
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
|
|
1591
|
+
let keyName2 = null;
|
|
1592
1592
|
if (prop.key.type === "Identifier" && !prop.computed) {
|
|
1593
|
-
|
|
1593
|
+
keyName2 = prop.key.name;
|
|
1594
1594
|
} else if (prop.key.type === "Literal" && typeof prop.key.value === "string") {
|
|
1595
|
-
|
|
1595
|
+
keyName2 = prop.key.value;
|
|
1596
1596
|
}
|
|
1597
|
-
if (
|
|
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,1130 @@ 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/no-template-literal-in-log.ts
|
|
2573
|
+
var import_utils25 = require("@typescript-eslint/utils");
|
|
2574
|
+
var LOG_METHODS2 = /* @__PURE__ */ new Set([
|
|
2575
|
+
"debug",
|
|
2576
|
+
"info",
|
|
2577
|
+
"warn",
|
|
2578
|
+
"warning",
|
|
2579
|
+
"error",
|
|
2580
|
+
"exception",
|
|
2581
|
+
"critical",
|
|
2582
|
+
"trace",
|
|
2583
|
+
"log",
|
|
2584
|
+
"fatal",
|
|
2585
|
+
"success"
|
|
2586
|
+
]);
|
|
2587
|
+
var LOGGER_NAMES2 = /* @__PURE__ */ new Set([
|
|
2588
|
+
"console",
|
|
2589
|
+
"logger",
|
|
2590
|
+
"log",
|
|
2591
|
+
"_log",
|
|
2592
|
+
"_logger"
|
|
2593
|
+
]);
|
|
2594
|
+
var LOGGER_FACTORIES2 = /* @__PURE__ */ new Set([
|
|
2595
|
+
"getlogger",
|
|
2596
|
+
"createlogger"
|
|
2597
|
+
]);
|
|
2598
|
+
function looksLikeLogger(node) {
|
|
2599
|
+
switch (node.type) {
|
|
2600
|
+
case "Identifier": {
|
|
2601
|
+
const name = node.name.toLowerCase();
|
|
2602
|
+
return LOGGER_NAMES2.has(name) || LOGGER_FACTORIES2.has(name);
|
|
2603
|
+
}
|
|
2604
|
+
case "MemberExpression": {
|
|
2605
|
+
if (!node.computed && node.property.type === "Identifier") {
|
|
2606
|
+
const prop = node.property.name.toLowerCase();
|
|
2607
|
+
if (LOGGER_NAMES2.has(prop) || LOGGER_FACTORIES2.has(prop)) {
|
|
2608
|
+
return true;
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
return looksLikeLogger(node.object);
|
|
2612
|
+
}
|
|
2613
|
+
case "CallExpression":
|
|
2614
|
+
return looksLikeLogger(node.callee);
|
|
2615
|
+
default:
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
function findInterpolatingTemplate(node) {
|
|
2620
|
+
if (node.type === "TemplateLiteral") {
|
|
2621
|
+
return node.expressions.length > 0 ? node : null;
|
|
2622
|
+
}
|
|
2623
|
+
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
2624
|
+
return findInterpolatingTemplate(node.left) ?? findInterpolatingTemplate(node.right);
|
|
2625
|
+
}
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
function messageArg(node, method, receiver) {
|
|
2629
|
+
const levelFirst = method === "log" && !isConsoleReceiver(receiver);
|
|
2630
|
+
const arg = node.arguments[levelFirst ? 1 : 0];
|
|
2631
|
+
if (arg === void 0 || arg.type === "SpreadElement") {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
return arg;
|
|
2635
|
+
}
|
|
2636
|
+
function isConsoleReceiver(node) {
|
|
2637
|
+
return node.type === "Identifier" && node.name === "console";
|
|
2638
|
+
}
|
|
2639
|
+
var no_template_literal_in_log_default = import_utils25.ESLintUtils.RuleCreator(
|
|
2640
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
2641
|
+
)({
|
|
2642
|
+
name: "no-template-literal-in-log",
|
|
2643
|
+
meta: {
|
|
2644
|
+
type: "problem",
|
|
2645
|
+
docs: {
|
|
2646
|
+
description: "Disallow an interpolating template literal as a logging message \u2014 pass variables as structured fields so logs stay filterable and templates stay constant."
|
|
2647
|
+
},
|
|
2648
|
+
schema: [],
|
|
2649
|
+
messages: {
|
|
2650
|
+
noTemplateLiteralInLog: "Interpolating template literal as a logging message \u2014 pass variables as structured fields (logger.info('msg', { key })) instead."
|
|
2651
|
+
}
|
|
2652
|
+
},
|
|
2653
|
+
defaultOptions: [],
|
|
2654
|
+
create(context) {
|
|
2655
|
+
return {
|
|
2656
|
+
CallExpression(node) {
|
|
2657
|
+
const callee = node.callee;
|
|
2658
|
+
if (callee.type !== "MemberExpression" || callee.computed) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (callee.property.type !== "Identifier") {
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
const method = callee.property.name;
|
|
2665
|
+
if (!LOG_METHODS2.has(method)) {
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
if (!looksLikeLogger(callee.object)) {
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
const arg = messageArg(node, method, callee.object);
|
|
2672
|
+
if (arg === null) {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (findInterpolatingTemplate(arg) !== null) {
|
|
2676
|
+
context.report({ node, messageId: "noTemplateLiteralInLog" });
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
// src/rules/prefer-string-literal-union.ts
|
|
2684
|
+
var import_utils26 = require("@typescript-eslint/utils");
|
|
2685
|
+
var CHOICE_TOKENS = /* @__PURE__ */ new Set([
|
|
2686
|
+
"status",
|
|
2687
|
+
"state",
|
|
2688
|
+
"kind",
|
|
2689
|
+
"role",
|
|
2690
|
+
"priority",
|
|
2691
|
+
"severity",
|
|
2692
|
+
"direction",
|
|
2693
|
+
"tier",
|
|
2694
|
+
"stage",
|
|
2695
|
+
"type",
|
|
2696
|
+
"mode",
|
|
2697
|
+
"level"
|
|
2698
|
+
]);
|
|
2699
|
+
var LOWER_TOKEN_RE = /^[a-z][a-z0-9_-]{0,30}$/;
|
|
2700
|
+
var MIN_CLUSTER_SIZE = 2;
|
|
2701
|
+
var IGNORE_PATTERNS = [
|
|
2702
|
+
/[\\/]generated[\\/]/,
|
|
2703
|
+
/\.gen\.tsx?$/,
|
|
2704
|
+
/\.generated\.tsx?$/,
|
|
2705
|
+
/\.d\.ts$/
|
|
2706
|
+
];
|
|
2707
|
+
function isIgnoredFile(filename, sourceText) {
|
|
2708
|
+
if (IGNORE_PATTERNS.some((re) => re.test(filename))) {
|
|
2709
|
+
return true;
|
|
2710
|
+
}
|
|
2711
|
+
return /@generated\b/.test(sourceText.slice(0, 1024));
|
|
2712
|
+
}
|
|
2713
|
+
function lastWord(name) {
|
|
2714
|
+
const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[_\s]+/).filter((w) => w.length > 0);
|
|
2715
|
+
const last = words[words.length - 1] ?? name;
|
|
2716
|
+
return last.toLowerCase();
|
|
2717
|
+
}
|
|
2718
|
+
function isChoiceLikeName(name) {
|
|
2719
|
+
return CHOICE_TOKENS.has(lastWord(name));
|
|
2720
|
+
}
|
|
2721
|
+
function keyName(key) {
|
|
2722
|
+
if (key.type === import_utils26.AST_NODE_TYPES.Identifier) {
|
|
2723
|
+
return key.name;
|
|
2724
|
+
}
|
|
2725
|
+
if (key.type === import_utils26.AST_NODE_TYPES.Literal && typeof key.value === "string") {
|
|
2726
|
+
return key.value;
|
|
2727
|
+
}
|
|
2728
|
+
return null;
|
|
2729
|
+
}
|
|
2730
|
+
function isStringLiteralUnion(node) {
|
|
2731
|
+
if (node?.type !== import_utils26.AST_NODE_TYPES.TSUnionType) {
|
|
2732
|
+
return false;
|
|
2733
|
+
}
|
|
2734
|
+
const stringMembers = node.types.filter(
|
|
2735
|
+
(t) => t.type === import_utils26.AST_NODE_TYPES.TSLiteralType && t.literal.type === import_utils26.AST_NODE_TYPES.Literal && typeof t.literal.value === "string"
|
|
2736
|
+
);
|
|
2737
|
+
return stringMembers.length >= MIN_CLUSTER_SIZE;
|
|
2738
|
+
}
|
|
2739
|
+
function refKey(node) {
|
|
2740
|
+
if (node.type === import_utils26.AST_NODE_TYPES.Identifier) {
|
|
2741
|
+
return node.name;
|
|
2742
|
+
}
|
|
2743
|
+
if (node.type === import_utils26.AST_NODE_TYPES.MemberExpression && !node.computed) {
|
|
2744
|
+
const inner = refKey(node.object);
|
|
2745
|
+
if (inner === null || node.property.type !== import_utils26.AST_NODE_TYPES.Identifier) {
|
|
2746
|
+
return null;
|
|
2747
|
+
}
|
|
2748
|
+
return `${inner}.${node.property.name}`;
|
|
2749
|
+
}
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
function strLiteral(node) {
|
|
2753
|
+
if (node.type === import_utils26.AST_NODE_TYPES.Literal && typeof node.value === "string") {
|
|
2754
|
+
return node.value;
|
|
2755
|
+
}
|
|
2756
|
+
return null;
|
|
2757
|
+
}
|
|
2758
|
+
var prefer_string_literal_union_default = import_utils26.ESLintUtils.RuleCreator(
|
|
2759
|
+
(name) => `https://github.com/sarj-ai/linting/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
2760
|
+
)({
|
|
2761
|
+
name: "prefer-string-literal-union",
|
|
2762
|
+
meta: {
|
|
2763
|
+
type: "suggestion",
|
|
2764
|
+
docs: {
|
|
2765
|
+
description: "Flag raw `string` choice fields and string-literal comparison clusters; prefer a string-literal union type."
|
|
2766
|
+
},
|
|
2767
|
+
schema: [],
|
|
2768
|
+
messages: {
|
|
2769
|
+
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.',
|
|
2770
|
+
comparisonCluster: '`{{key}}` is compared against a closed set of string literals \u2014 define a string-literal union type (e.g. `type X = "a" | "b"`).'
|
|
2771
|
+
}
|
|
2772
|
+
},
|
|
2773
|
+
defaultOptions: [],
|
|
2774
|
+
create(context) {
|
|
2775
|
+
const filename = context.filename;
|
|
2776
|
+
const sourceText = context.sourceCode.getText();
|
|
2777
|
+
if (isIgnoredFile(filename, sourceText)) {
|
|
2778
|
+
return {};
|
|
2779
|
+
}
|
|
2780
|
+
const scopeStack = [];
|
|
2781
|
+
const validClusters = [];
|
|
2782
|
+
const bareChoiceProps = [];
|
|
2783
|
+
const containersWithUnion = /* @__PURE__ */ new Set();
|
|
2784
|
+
function pushScope() {
|
|
2785
|
+
scopeStack.push(/* @__PURE__ */ new Map());
|
|
2786
|
+
}
|
|
2787
|
+
function popScope() {
|
|
2788
|
+
const clusters = scopeStack.pop();
|
|
2789
|
+
if (clusters === void 0) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
for (const entry of clusters.values()) {
|
|
2793
|
+
if (entry.allTokens && entry.literals.size >= MIN_CLUSTER_SIZE) {
|
|
2794
|
+
validClusters.push(entry.node);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
function accumulate(key, literals, node) {
|
|
2799
|
+
const scope = scopeStack[scopeStack.length - 1];
|
|
2800
|
+
if (scope === void 0) {
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
const allTokens = literals.every((lit) => LOWER_TOKEN_RE.test(lit));
|
|
2804
|
+
const existing = scope.get(key);
|
|
2805
|
+
if (existing === void 0) {
|
|
2806
|
+
scope.set(key, {
|
|
2807
|
+
node,
|
|
2808
|
+
literals: new Set(literals),
|
|
2809
|
+
allTokens
|
|
2810
|
+
});
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
for (const lit of literals) {
|
|
2814
|
+
existing.literals.add(lit);
|
|
2815
|
+
}
|
|
2816
|
+
existing.allTokens = existing.allTokens && allTokens;
|
|
2817
|
+
}
|
|
2818
|
+
function collectProperty(key, typeNode, container, node) {
|
|
2819
|
+
if (isStringLiteralUnion(typeNode)) {
|
|
2820
|
+
containersWithUnion.add(container);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
if (typeNode?.type !== import_utils26.AST_NODE_TYPES.TSStringKeyword) {
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
const name = keyName(key);
|
|
2827
|
+
if (name === null || !isChoiceLikeName(name)) {
|
|
2828
|
+
return;
|
|
2829
|
+
}
|
|
2830
|
+
bareChoiceProps.push({ name, container, node });
|
|
2831
|
+
}
|
|
2832
|
+
return {
|
|
2833
|
+
FunctionDeclaration: pushScope,
|
|
2834
|
+
"FunctionDeclaration:exit": popScope,
|
|
2835
|
+
FunctionExpression: pushScope,
|
|
2836
|
+
"FunctionExpression:exit": popScope,
|
|
2837
|
+
ArrowFunctionExpression: pushScope,
|
|
2838
|
+
"ArrowFunctionExpression:exit": popScope,
|
|
2839
|
+
BinaryExpression(node) {
|
|
2840
|
+
if (node.operator !== "===" && node.operator !== "!==" && node.operator !== "==" && node.operator !== "!=") {
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
const leftKey = refKey(node.left);
|
|
2844
|
+
const rightLit = strLiteral(node.right);
|
|
2845
|
+
const rightKey = refKey(node.right);
|
|
2846
|
+
const leftLit = strLiteral(node.left);
|
|
2847
|
+
if (leftKey !== null && rightLit !== null) {
|
|
2848
|
+
accumulate(leftKey, [rightLit], node);
|
|
2849
|
+
} else if (rightKey !== null && leftLit !== null) {
|
|
2850
|
+
accumulate(rightKey, [leftLit], node);
|
|
2851
|
+
}
|
|
2852
|
+
},
|
|
2853
|
+
SwitchStatement(node) {
|
|
2854
|
+
const key = refKey(node.discriminant);
|
|
2855
|
+
if (key === null) {
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const literals = [];
|
|
2859
|
+
for (const c of node.cases) {
|
|
2860
|
+
if (c.test !== null) {
|
|
2861
|
+
const lit = strLiteral(c.test);
|
|
2862
|
+
if (lit !== null) {
|
|
2863
|
+
literals.push(lit);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (literals.length > 0) {
|
|
2868
|
+
accumulate(key, literals, node);
|
|
2869
|
+
}
|
|
2870
|
+
},
|
|
2871
|
+
TSPropertySignature(node) {
|
|
2872
|
+
collectProperty(
|
|
2873
|
+
node.key,
|
|
2874
|
+
node.typeAnnotation?.typeAnnotation,
|
|
2875
|
+
node.parent,
|
|
2876
|
+
node
|
|
2877
|
+
);
|
|
2878
|
+
},
|
|
2879
|
+
PropertyDefinition(node) {
|
|
2880
|
+
collectProperty(
|
|
2881
|
+
node.key,
|
|
2882
|
+
node.typeAnnotation?.typeAnnotation,
|
|
2883
|
+
node.parent,
|
|
2884
|
+
node
|
|
2885
|
+
);
|
|
2886
|
+
},
|
|
2887
|
+
"Program:exit"() {
|
|
2888
|
+
for (const clusterNode of validClusters) {
|
|
2889
|
+
context.report({
|
|
2890
|
+
node: clusterNode,
|
|
2891
|
+
messageId: "comparisonCluster",
|
|
2892
|
+
data: { key: refKeyText(clusterNode) }
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
for (const prop of bareChoiceProps) {
|
|
2896
|
+
if (containersWithUnion.has(prop.container)) {
|
|
2897
|
+
context.report({
|
|
2898
|
+
node: prop.node,
|
|
2899
|
+
messageId: "bareChoiceField",
|
|
2900
|
+
data: { name: prop.name }
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
};
|
|
2906
|
+
function refKeyText(node) {
|
|
2907
|
+
if (node.type === import_utils26.AST_NODE_TYPES.BinaryExpression) {
|
|
2908
|
+
return refKey(node.left) ?? refKey(node.right) ?? "value";
|
|
2909
|
+
}
|
|
2910
|
+
if (node.type === import_utils26.AST_NODE_TYPES.SwitchStatement) {
|
|
2911
|
+
return refKey(node.discriminant) ?? "value";
|
|
2912
|
+
}
|
|
2913
|
+
return "value";
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
// src/rules/single-public-export.ts
|
|
2919
|
+
var import_utils27 = require("@typescript-eslint/utils");
|
|
2920
|
+
var JUNK_DRAWER_STEMS = /* @__PURE__ */ new Set([
|
|
2921
|
+
"util",
|
|
2922
|
+
"utils",
|
|
2923
|
+
"helper",
|
|
2924
|
+
"helpers",
|
|
2925
|
+
"common",
|
|
2926
|
+
"constant",
|
|
2927
|
+
"constants",
|
|
2928
|
+
"type",
|
|
2929
|
+
"types",
|
|
2930
|
+
"model",
|
|
2931
|
+
"models",
|
|
2932
|
+
"shared",
|
|
2933
|
+
"misc"
|
|
2934
|
+
]);
|
|
2935
|
+
var CONVENTIONAL_BUCKET_EXPORTS = /* @__PURE__ */ new Set(["cn"]);
|
|
2936
|
+
var ACRONYM_OVERRIDES = [
|
|
2937
|
+
[/OAuth/g, "Oauth"],
|
|
2938
|
+
[/GraphQL/g, "Graphql"],
|
|
2939
|
+
[/gRPC/g, "Grpc"]
|
|
2940
|
+
];
|
|
2941
|
+
var CAMEL_BOUNDARY_RE = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
|
|
2942
|
+
var TEST_FILE_RE = /\.(test|spec)\.[cm]?[jt]sx?$/i;
|
|
2943
|
+
var SCRIPT_EXT_RE = /\.[cm]?[jt]sx?$/i;
|
|
2944
|
+
var basename = (filename) => filename.split(/[/\\]/).pop() ?? filename;
|
|
2945
|
+
var stemOf = (base) => base.replace(SCRIPT_EXT_RE, "");
|
|
2946
|
+
var kebabCase = (name) => {
|
|
2947
|
+
let normalized = name;
|
|
2948
|
+
for (const [pattern, replacement] of ACRONYM_OVERRIDES) {
|
|
2949
|
+
normalized = normalized.replace(pattern, replacement);
|
|
2950
|
+
}
|
|
2951
|
+
return normalized.replace(CAMEL_BOUNDARY_RE, "-").toLowerCase();
|
|
2952
|
+
};
|
|
2953
|
+
var isFunctionExpression2 = (node) => node !== null && (node.type === import_utils27.AST_NODE_TYPES.ArrowFunctionExpression || node.type === import_utils27.AST_NODE_TYPES.FunctionExpression);
|
|
2954
|
+
var functionConstName = (decl) => {
|
|
2955
|
+
if (decl.declarations.length !== 1) return null;
|
|
2956
|
+
const [declarator] = decl.declarations;
|
|
2957
|
+
if (declarator === void 0) return null;
|
|
2958
|
+
if (declarator.id.type !== import_utils27.AST_NODE_TYPES.Identifier) return null;
|
|
2959
|
+
if (!isFunctionExpression2(declarator.init)) return null;
|
|
2960
|
+
return declarator.id.name;
|
|
2961
|
+
};
|
|
2962
|
+
var summarizeExports = (body) => {
|
|
2963
|
+
let names = 0;
|
|
2964
|
+
let hasReExport = false;
|
|
2965
|
+
let candidate = null;
|
|
2966
|
+
const addCandidate = (name, node) => {
|
|
2967
|
+
names += 1;
|
|
2968
|
+
candidate = { name, node };
|
|
2969
|
+
};
|
|
2970
|
+
for (const statement of body) {
|
|
2971
|
+
switch (statement.type) {
|
|
2972
|
+
case import_utils27.AST_NODE_TYPES.ExportAllDeclaration:
|
|
2973
|
+
hasReExport = true;
|
|
2974
|
+
break;
|
|
2975
|
+
case import_utils27.AST_NODE_TYPES.ExportDefaultDeclaration: {
|
|
2976
|
+
names += 1;
|
|
2977
|
+
const decl = statement.declaration;
|
|
2978
|
+
if (decl.type === import_utils27.AST_NODE_TYPES.FunctionDeclaration && decl.id !== null) {
|
|
2979
|
+
candidate = { name: decl.id.name, node: statement };
|
|
2980
|
+
} else if (decl.type === import_utils27.AST_NODE_TYPES.ClassDeclaration && decl.id !== null) {
|
|
2981
|
+
candidate = { name: decl.id.name, node: statement };
|
|
2982
|
+
}
|
|
2983
|
+
break;
|
|
2984
|
+
}
|
|
2985
|
+
case import_utils27.AST_NODE_TYPES.ExportNamedDeclaration: {
|
|
2986
|
+
if (statement.source !== null) {
|
|
2987
|
+
hasReExport = true;
|
|
2988
|
+
break;
|
|
2989
|
+
}
|
|
2990
|
+
const decl = statement.declaration;
|
|
2991
|
+
if (decl === null) {
|
|
2992
|
+
names += statement.specifiers.length;
|
|
2993
|
+
break;
|
|
2994
|
+
}
|
|
2995
|
+
switch (decl.type) {
|
|
2996
|
+
case import_utils27.AST_NODE_TYPES.FunctionDeclaration:
|
|
2997
|
+
if (decl.id !== null) addCandidate(decl.id.name, statement);
|
|
2998
|
+
else names += 1;
|
|
2999
|
+
break;
|
|
3000
|
+
case import_utils27.AST_NODE_TYPES.ClassDeclaration:
|
|
3001
|
+
if (decl.id !== null) addCandidate(decl.id.name, statement);
|
|
3002
|
+
else names += 1;
|
|
3003
|
+
break;
|
|
3004
|
+
case import_utils27.AST_NODE_TYPES.VariableDeclaration: {
|
|
3005
|
+
const fnName = functionConstName(decl);
|
|
3006
|
+
if (fnName !== null && decl.declarations.length === 1) {
|
|
3007
|
+
addCandidate(fnName, statement);
|
|
3008
|
+
} else {
|
|
3009
|
+
names += decl.declarations.length;
|
|
3010
|
+
}
|
|
3011
|
+
break;
|
|
3012
|
+
}
|
|
3013
|
+
default:
|
|
3014
|
+
names += 1;
|
|
3015
|
+
}
|
|
3016
|
+
break;
|
|
3017
|
+
}
|
|
3018
|
+
default:
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
return { names, hasReExport, candidate };
|
|
3023
|
+
};
|
|
3024
|
+
var single_public_export_default = import_utils27.ESLintUtils.RuleCreator(
|
|
3025
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
3026
|
+
)({
|
|
3027
|
+
name: "single-public-export",
|
|
3028
|
+
meta: {
|
|
3029
|
+
type: "suggestion",
|
|
3030
|
+
docs: {
|
|
3031
|
+
description: "A junk-drawer module stem (`utils`, `helpers`, `types`, ...) with a single public function/class/const export should be renamed after that export."
|
|
3032
|
+
},
|
|
3033
|
+
schema: [],
|
|
3034
|
+
messages: {
|
|
3035
|
+
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."
|
|
3036
|
+
}
|
|
3037
|
+
},
|
|
3038
|
+
defaultOptions: [],
|
|
3039
|
+
create(context) {
|
|
3040
|
+
const base = basename(context.filename);
|
|
3041
|
+
if (base.endsWith(".d.ts")) return {};
|
|
3042
|
+
if (TEST_FILE_RE.test(base)) return {};
|
|
3043
|
+
const stem = stemOf(base);
|
|
3044
|
+
if (!JUNK_DRAWER_STEMS.has(stem.toLowerCase())) return {};
|
|
3045
|
+
return {
|
|
3046
|
+
Program(node) {
|
|
3047
|
+
const { names, hasReExport, candidate } = summarizeExports(node.body);
|
|
3048
|
+
if (hasReExport) return;
|
|
3049
|
+
if (names !== 1 || candidate === null) return;
|
|
3050
|
+
if (CONVENTIONAL_BUCKET_EXPORTS.has(candidate.name)) return;
|
|
3051
|
+
const expected = kebabCase(candidate.name);
|
|
3052
|
+
if (stem === expected) return;
|
|
3053
|
+
context.report({
|
|
3054
|
+
node: candidate.node,
|
|
3055
|
+
messageId: "renameJunkDrawer",
|
|
3056
|
+
data: { stem, name: candidate.name, expected }
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
|
|
1939
3063
|
// src/index.ts
|
|
1940
3064
|
var rules = {
|
|
1941
3065
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -1957,12 +3081,18 @@ var rules = {
|
|
|
1957
3081
|
"prefer-shadcn": prefer_shadcn_default,
|
|
1958
3082
|
"require-assert-never": require_assert_never_default,
|
|
1959
3083
|
"require-zod-form-validation": require_zod_form_validation_default,
|
|
1960
|
-
"zod-naming-convention": zod_naming_convention_default
|
|
3084
|
+
"zod-naming-convention": zod_naming_convention_default,
|
|
3085
|
+
"no-cors-wildcard-with-credentials": no_cors_wildcard_with_credentials_default,
|
|
3086
|
+
"no-fat-try-blocks": no_fat_try_blocks_default,
|
|
3087
|
+
"no-secret-in-log": no_secret_in_log_default,
|
|
3088
|
+
"no-template-literal-in-log": no_template_literal_in_log_default,
|
|
3089
|
+
"prefer-string-literal-union": prefer_string_literal_union_default,
|
|
3090
|
+
"single-public-export": single_public_export_default
|
|
1961
3091
|
};
|
|
1962
3092
|
var plugin = {
|
|
1963
3093
|
meta: {
|
|
1964
3094
|
name: "@sarj/eslint-plugin",
|
|
1965
|
-
version: "2.
|
|
3095
|
+
version: "2.3.0"
|
|
1966
3096
|
},
|
|
1967
3097
|
rules,
|
|
1968
3098
|
configs: {
|
|
@@ -1987,7 +3117,14 @@ var plugin = {
|
|
|
1987
3117
|
"@sarj/prefer-discriminated-union": "warn",
|
|
1988
3118
|
"@sarj/no-comment-cruft": "warn",
|
|
1989
3119
|
// Frontend / styling — distilled from frontend PR-review mining.
|
|
1990
|
-
"@sarj/prefer-semantic-colors": "warn"
|
|
3120
|
+
"@sarj/prefer-semantic-colors": "warn",
|
|
3121
|
+
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
3122
|
+
"@sarj/no-fat-try-blocks": "warn",
|
|
3123
|
+
"@sarj/no-cors-wildcard-with-credentials": "warn",
|
|
3124
|
+
"@sarj/no-template-literal-in-log": "warn",
|
|
3125
|
+
"@sarj/no-secret-in-log": "warn",
|
|
3126
|
+
"@sarj/single-public-export": "warn",
|
|
3127
|
+
"@sarj/prefer-string-literal-union": "warn"
|
|
1991
3128
|
}
|
|
1992
3129
|
},
|
|
1993
3130
|
strict: {
|
|
@@ -2015,7 +3152,15 @@ var plugin = {
|
|
|
2015
3152
|
"@sarj/no-comment-cruft": "error",
|
|
2016
3153
|
// Frontend / styling — distilled from frontend PR-review mining. Stylistic,
|
|
2017
3154
|
// no autofix → warn (rollout should prove the FP rate before raising it).
|
|
2018
|
-
"@sarj/prefer-semantic-colors": "warn"
|
|
3155
|
+
"@sarj/prefer-semantic-colors": "warn",
|
|
3156
|
+
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
3157
|
+
"@sarj/no-fat-try-blocks": "error",
|
|
3158
|
+
"@sarj/no-cors-wildcard-with-credentials": "error",
|
|
3159
|
+
"@sarj/no-template-literal-in-log": "error",
|
|
3160
|
+
"@sarj/no-secret-in-log": "error",
|
|
3161
|
+
"@sarj/single-public-export": "error",
|
|
3162
|
+
// High-volume/stylistic — warn until rollout proves FP rate.
|
|
3163
|
+
"@sarj/prefer-string-literal-union": "warn"
|
|
2019
3164
|
}
|
|
2020
3165
|
}
|
|
2021
3166
|
}
|