pumuki 6.3.285 → 6.3.287

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.
@@ -41,6 +41,8 @@ const loggingMethodNames = new Set([
41
41
  'verbose',
42
42
  'warn',
43
43
  ]);
44
+ const backendLogContextKeys = new Set(['correlationId', 'requestId', 'traceId', 'userId']);
45
+ const backendLogSignalKeys = new Set(['err', 'error', 'exception', 'message']);
44
46
  const mockOrSpyMethodNames = new Set([
45
47
  'fn',
46
48
  'mock',
@@ -53,6 +55,8 @@ const mockOrSpyMethodNames = new Set([
53
55
  ]);
54
56
  const httpResponseObjectNames = new Set(['ctx', 'reply', 'res', 'response']);
55
57
  const httpResponseMethodNames = new Set(['json', 'send']);
58
+ const backendErrorResponseRequiredKeys = new Set(['message', 'path', 'statusCode', 'timestamp']);
59
+ const backendErrorResponseSignalKeys = new Set(['error', 'message', 'path', 'statusCode', 'timestamp']);
56
60
  type AstNode = Record<string, string | number | boolean | bigint | symbol | null | Date | object>;
57
61
  type TypeScriptSemanticNode = {
58
62
  kind: 'class' | 'property' | 'call' | 'member';
@@ -1797,6 +1801,27 @@ export const hasBackendGenericErrorThrow = (node: unknown): boolean => {
1797
1801
  });
1798
1802
  };
1799
1803
 
1804
+ const isBackendRawThrowExpression = (node: AstNode): boolean => {
1805
+ if (node.type !== 'ThrowStatement' || !isObject(node.argument)) {
1806
+ return false;
1807
+ }
1808
+ const argument = node.argument;
1809
+ return (
1810
+ argument.type === 'StringLiteral' ||
1811
+ argument.type === 'NumericLiteral' ||
1812
+ argument.type === 'BooleanLiteral' ||
1813
+ argument.type === 'ObjectExpression' ||
1814
+ argument.type === 'Identifier'
1815
+ );
1816
+ };
1817
+
1818
+ export const findBackendRawThrowExpressionLines = (ast: unknown): readonly number[] => {
1819
+ return collectLineMatchesWithAncestors(ast, (node) => isBackendRawThrowExpression(node));
1820
+ };
1821
+
1822
+ export const hasBackendRawThrowExpression = (ast: unknown): boolean =>
1823
+ findBackendRawThrowExpressionLines(ast).length > 0;
1824
+
1800
1825
  export const hasBackendProductionMockOrSpy = (node: unknown): boolean => {
1801
1826
  return hasNode(node, (value) => {
1802
1827
  if (value.type !== 'CallExpression' || !isObject(value.callee)) {
@@ -1857,6 +1882,251 @@ export const hasBackendErrorStackInHttpResponse = (node: unknown): boolean => {
1857
1882
  });
1858
1883
  };
1859
1884
 
1885
+ const backendObjectPropertyName = (property: unknown): string | null => {
1886
+ if (!isObject(property)) {
1887
+ return null;
1888
+ }
1889
+ const key = property.key;
1890
+ if (!isObject(key)) {
1891
+ return null;
1892
+ }
1893
+ if (typeof key.name === 'string') {
1894
+ return key.name;
1895
+ }
1896
+ if (typeof key.value === 'string') {
1897
+ return key.value;
1898
+ }
1899
+ return null;
1900
+ };
1901
+
1902
+ const objectExpressionPropertyNames = (node: AstNode): Set<string> => {
1903
+ if (!Array.isArray(node.properties)) {
1904
+ return new Set();
1905
+ }
1906
+ return new Set(
1907
+ node.properties
1908
+ .map((property) => backendObjectPropertyName(property))
1909
+ .filter((name): name is string => typeof name === 'string')
1910
+ );
1911
+ };
1912
+
1913
+ const isErrorStatusCallExpression = (node: unknown): boolean => {
1914
+ if (!isObject(node) || node.type !== 'CallExpression') {
1915
+ return false;
1916
+ }
1917
+ const callee = node.callee;
1918
+ if (!isObject(callee) || callee.type !== 'MemberExpression') {
1919
+ return false;
1920
+ }
1921
+ if (memberExpressionPropertyName(callee) !== 'status') {
1922
+ return false;
1923
+ }
1924
+ const [statusArgument] = Array.isArray(node.arguments) ? node.arguments : [];
1925
+ return (
1926
+ isObject(statusArgument) &&
1927
+ statusArgument.type === 'NumericLiteral' &&
1928
+ typeof statusArgument.value === 'number' &&
1929
+ statusArgument.value >= 400
1930
+ );
1931
+ };
1932
+
1933
+ const isSuccessStatusCallExpression = (node: unknown): boolean => {
1934
+ if (!isObject(node) || node.type !== 'CallExpression') {
1935
+ return false;
1936
+ }
1937
+ const callee = node.callee;
1938
+ if (!isObject(callee) || callee.type !== 'MemberExpression') {
1939
+ return false;
1940
+ }
1941
+ if (memberExpressionPropertyName(callee) !== 'status') {
1942
+ return false;
1943
+ }
1944
+ const [statusArgument] = Array.isArray(node.arguments) ? node.arguments : [];
1945
+ return (
1946
+ isObject(statusArgument) &&
1947
+ statusArgument.type === 'NumericLiteral' &&
1948
+ typeof statusArgument.value === 'number' &&
1949
+ statusArgument.value >= 200 &&
1950
+ statusArgument.value < 400
1951
+ );
1952
+ };
1953
+
1954
+ const hasCatchClauseAncestor = (ancestors: ReadonlyArray<AstNode>): boolean => {
1955
+ return ancestors.some((ancestor) => ancestor.type === 'CatchClause');
1956
+ };
1957
+
1958
+ const isHttpErrorResponseCallContext = (
1959
+ node: AstNode,
1960
+ ancestors: ReadonlyArray<AstNode>
1961
+ ): boolean => {
1962
+ const callee = node.callee;
1963
+ if (!isObject(callee) || callee.type !== 'MemberExpression') {
1964
+ return false;
1965
+ }
1966
+ const methodName = memberExpressionPropertyName(callee);
1967
+ if (!methodName || !httpResponseMethodNames.has(methodName)) {
1968
+ return false;
1969
+ }
1970
+ if (isErrorStatusCallExpression(callee.object)) {
1971
+ return true;
1972
+ }
1973
+ return hasCatchClauseAncestor(ancestors);
1974
+ };
1975
+
1976
+ const isBackendInconsistentErrorResponseCall = (
1977
+ node: AstNode,
1978
+ ancestors: ReadonlyArray<AstNode>
1979
+ ): boolean => {
1980
+ if (node.type !== 'CallExpression' || !isHttpErrorResponseCallContext(node, ancestors)) {
1981
+ return false;
1982
+ }
1983
+ const [payload] = Array.isArray(node.arguments) ? node.arguments : [];
1984
+ if (!isObject(payload) || payload.type !== 'ObjectExpression') {
1985
+ return false;
1986
+ }
1987
+ const propertyNames = objectExpressionPropertyNames(payload);
1988
+ const hasErrorSignal = Array.from(backendErrorResponseSignalKeys).some((key) =>
1989
+ propertyNames.has(key)
1990
+ );
1991
+ if (!hasErrorSignal) {
1992
+ return false;
1993
+ }
1994
+ return Array.from(backendErrorResponseRequiredKeys).some((key) => !propertyNames.has(key));
1995
+ };
1996
+
1997
+ export const findBackendInconsistentErrorResponseLines = (
1998
+ node: unknown
1999
+ ): readonly number[] => {
2000
+ return collectLineMatchesWithAncestors(node, isBackendInconsistentErrorResponseCall);
2001
+ };
2002
+
2003
+ export const hasBackendInconsistentErrorResponse = (node: unknown): boolean => {
2004
+ return hasNodeWithAncestors(node, isBackendInconsistentErrorResponseCall);
2005
+ };
2006
+
2007
+ const isBackendErrorPayloadObject = (node: unknown): node is AstNode => {
2008
+ if (!isObject(node) || node.type !== 'ObjectExpression') {
2009
+ return false;
2010
+ }
2011
+ const propertyNames = objectExpressionPropertyNames(node);
2012
+ return Array.from(backendErrorResponseSignalKeys).some((key) => propertyNames.has(key));
2013
+ };
2014
+
2015
+ const isBackendErrorPayloadWithSuccessStatusCall = (node: AstNode): boolean => {
2016
+ if (node.type !== 'CallExpression') {
2017
+ return false;
2018
+ }
2019
+ const callee = node.callee;
2020
+ if (!isObject(callee) || callee.type !== 'MemberExpression') {
2021
+ return false;
2022
+ }
2023
+ const methodName = memberExpressionPropertyName(callee);
2024
+ if (!methodName || !httpResponseMethodNames.has(methodName)) {
2025
+ return false;
2026
+ }
2027
+ if (!isSuccessStatusCallExpression(callee.object)) {
2028
+ return false;
2029
+ }
2030
+ const [payload] = Array.isArray(node.arguments) ? node.arguments : [];
2031
+ return isBackendErrorPayloadObject(payload);
2032
+ };
2033
+
2034
+ export const findBackendErrorPayloadWithSuccessStatusLines = (
2035
+ node: unknown
2036
+ ): readonly number[] => {
2037
+ return collectLineMatchesWithAncestors(node, (value) =>
2038
+ isBackendErrorPayloadWithSuccessStatusCall(value)
2039
+ );
2040
+ };
2041
+
2042
+ export const hasBackendErrorPayloadWithSuccessStatus = (node: unknown): boolean => {
2043
+ return hasNodeWithAncestors(node, (value) => isBackendErrorPayloadWithSuccessStatusCall(value));
2044
+ };
2045
+
2046
+ const typeNameFromTypeReference = (node: unknown): string | null => {
2047
+ if (!isObject(node)) {
2048
+ return null;
2049
+ }
2050
+ const typeName = node.typeName;
2051
+ if (!isObject(typeName)) {
2052
+ return null;
2053
+ }
2054
+ if (typeof typeName.name === 'string') {
2055
+ return typeName.name;
2056
+ }
2057
+ if (typeName.type === 'TSQualifiedName' && isObject(typeName.right)) {
2058
+ return typeof typeName.right.name === 'string' ? typeName.right.name : null;
2059
+ }
2060
+ return null;
2061
+ };
2062
+
2063
+ const isEntityTypeName = (name: string | null | undefined): boolean => {
2064
+ return typeof name === 'string' && /(Entity|Model)$/.test(name);
2065
+ };
2066
+
2067
+ const typeNodeContainsEntityType = (node: unknown): boolean => {
2068
+ if (!isObject(node)) {
2069
+ return false;
2070
+ }
2071
+ if (node.type === 'TSTypeReference') {
2072
+ if (isEntityTypeName(typeNameFromTypeReference(node))) {
2073
+ return true;
2074
+ }
2075
+ if (isObject(node.typeParameters) && Array.isArray(node.typeParameters.params)) {
2076
+ return node.typeParameters.params.some(typeNodeContainsEntityType);
2077
+ }
2078
+ if (isObject(node.typeArguments) && Array.isArray(node.typeArguments.params)) {
2079
+ return node.typeArguments.params.some(typeNodeContainsEntityType);
2080
+ }
2081
+ }
2082
+ if (node.type === 'TSArrayType') {
2083
+ return typeNodeContainsEntityType(node.elementType);
2084
+ }
2085
+ if (node.type === 'TSUnionType' && Array.isArray(node.types)) {
2086
+ return node.types.some(typeNodeContainsEntityType);
2087
+ }
2088
+ return false;
2089
+ };
2090
+
2091
+ const hasEntityReturnTypeAnnotation = (node: AstNode): boolean => {
2092
+ if (!isObject(node.returnType)) {
2093
+ return false;
2094
+ }
2095
+ const returnType = node.returnType;
2096
+ return isObject(returnType.typeAnnotation) && typeNodeContainsEntityType(returnType.typeAnnotation);
2097
+ };
2098
+
2099
+ const isNewEntityExpression = (node: unknown): boolean => {
2100
+ if (!isObject(node) || node.type !== 'NewExpression') {
2101
+ return false;
2102
+ }
2103
+ return isEntityTypeName(methodNameFromNode(node.callee));
2104
+ };
2105
+
2106
+ const isBackendControllerEntityResponseNode = (node: AstNode): boolean => {
2107
+ if (
2108
+ (node.type === 'ClassMethod' ||
2109
+ node.type === 'ClassPrivateMethod' ||
2110
+ node.type === 'ObjectMethod') &&
2111
+ hasEntityReturnTypeAnnotation(node)
2112
+ ) {
2113
+ return true;
2114
+ }
2115
+ return node.type === 'ReturnStatement' && isNewEntityExpression(node.argument);
2116
+ };
2117
+
2118
+ export const findBackendControllerEntityResponseLines = (
2119
+ node: unknown
2120
+ ): readonly number[] => {
2121
+ return collectLineMatchesWithAncestors(node, (value) =>
2122
+ isBackendControllerEntityResponseNode(value)
2123
+ );
2124
+ };
2125
+
2126
+ export const hasBackendControllerEntityResponse = (node: unknown): boolean => {
2127
+ return hasNodeWithAncestors(node, (value) => isBackendControllerEntityResponseNode(value));
2128
+ };
2129
+
1860
2130
  export const hasBackendAnemicDomainModel = (node: unknown): boolean => {
1861
2131
  const match = findFirstNode(node, (value) => {
1862
2132
  if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
@@ -1878,6 +2148,117 @@ export const hasBackendAnemicDomainModel = (node: unknown): boolean => {
1878
2148
  return match !== undefined;
1879
2149
  };
1880
2150
 
2151
+ const isCorsPermissiveValue = (node: unknown): boolean => {
2152
+ return (
2153
+ (isObject(node) && node.type === 'StringLiteral' && node.value === '*') ||
2154
+ (isObject(node) && node.type === 'BooleanLiteral' && node.value === true)
2155
+ );
2156
+ };
2157
+
2158
+ const objectPropertyName = (node: unknown): string | undefined => {
2159
+ if (!isObject(node) || (node.type !== 'ObjectProperty' && node.type !== 'Property')) {
2160
+ return undefined;
2161
+ }
2162
+ return methodNameFromNode(node.key);
2163
+ };
2164
+
2165
+ const objectExpressionHasPermissiveCorsProperty = (node: unknown): boolean => {
2166
+ if (!isObject(node) || node.type !== 'ObjectExpression' || !Array.isArray(node.properties)) {
2167
+ return false;
2168
+ }
2169
+ return node.properties.some((property) => {
2170
+ if (!isObject(property)) {
2171
+ return false;
2172
+ }
2173
+ const propertyName = objectPropertyName(property);
2174
+ return (
2175
+ (propertyName === 'origin' || propertyName === 'cors') &&
2176
+ isCorsPermissiveValue(property.value)
2177
+ );
2178
+ });
2179
+ };
2180
+
2181
+ const isNestFactoryCreateCall = (node: AstNode): boolean => {
2182
+ if (node.type !== 'CallExpression' || !isObject(node.callee)) {
2183
+ return false;
2184
+ }
2185
+ const objectName = memberExpressionObjectName(node.callee);
2186
+ const propertyName = memberExpressionPropertyName(node.callee);
2187
+ return objectName === 'NestFactory' && propertyName === 'create';
2188
+ };
2189
+
2190
+ const isEnableCorsCall = (node: AstNode): boolean => {
2191
+ if (node.type !== 'CallExpression' || !isObject(node.callee)) {
2192
+ return false;
2193
+ }
2194
+ return memberExpressionPropertyName(node.callee) === 'enableCors';
2195
+ };
2196
+
2197
+ const isCorsMiddlewareCall = (node: AstNode): boolean => {
2198
+ return (
2199
+ node.type === 'CallExpression' &&
2200
+ isObject(node.callee) &&
2201
+ node.callee.type === 'Identifier' &&
2202
+ node.callee.name === 'cors'
2203
+ );
2204
+ };
2205
+
2206
+ const isBackendPermissiveCorsConfigurationCall = (node: AstNode): boolean => {
2207
+ if (!Array.isArray(node.arguments)) {
2208
+ return false;
2209
+ }
2210
+ if (isEnableCorsCall(node) || isCorsMiddlewareCall(node)) {
2211
+ return (
2212
+ node.arguments.length === 0 ||
2213
+ node.arguments.some(objectExpressionHasPermissiveCorsProperty)
2214
+ );
2215
+ }
2216
+ return (
2217
+ isNestFactoryCreateCall(node) &&
2218
+ node.arguments.some(objectExpressionHasPermissiveCorsProperty)
2219
+ );
2220
+ };
2221
+
2222
+ export const findBackendPermissiveCorsConfigurationLines = (
2223
+ ast: unknown
2224
+ ): readonly number[] => {
2225
+ return collectLineMatchesWithAncestors(ast, (node) =>
2226
+ isBackendPermissiveCorsConfigurationCall(node)
2227
+ );
2228
+ };
2229
+
2230
+ export const hasBackendPermissiveCorsConfiguration = (ast: unknown): boolean =>
2231
+ findBackendPermissiveCorsConfigurationLines(ast).length > 0;
2232
+
2233
+ const isStringLiteralTypeNode = (node: unknown): boolean => {
2234
+ return (
2235
+ isObject(node) &&
2236
+ node.type === 'TSLiteralType' &&
2237
+ isObject(node.literal) &&
2238
+ node.literal.type === 'StringLiteral' &&
2239
+ typeof node.literal.value === 'string'
2240
+ );
2241
+ };
2242
+
2243
+ const isBackendStringLiteralUnionTypeNode = (node: AstNode): boolean => {
2244
+ if (node.type !== 'TSUnionType' || !Array.isArray(node.types)) {
2245
+ return false;
2246
+ }
2247
+ const stringLiteralTypes = node.types.filter(isStringLiteralTypeNode);
2248
+ return stringLiteralTypes.length >= 2 && stringLiteralTypes.length === node.types.length;
2249
+ };
2250
+
2251
+ export const findBackendStringLiteralUnionEnumCandidateLines = (
2252
+ ast: unknown
2253
+ ): readonly number[] => {
2254
+ return collectLineMatchesWithAncestors(ast, (node) =>
2255
+ isBackendStringLiteralUnionTypeNode(node)
2256
+ );
2257
+ };
2258
+
2259
+ export const hasBackendStringLiteralUnionEnumCandidate = (ast: unknown): boolean =>
2260
+ findBackendStringLiteralUnionEnumCandidateLines(ast).length > 0;
2261
+
1881
2262
  const isLoggingCall = (node: AstNode): boolean => {
1882
2263
  if (node.type !== 'CallExpression' || !isObject(node.callee)) {
1883
2264
  return false;
@@ -1910,6 +2291,62 @@ export const hasSensitiveDataLoggingCall = (node: unknown): boolean => {
1910
2291
  });
1911
2292
  };
1912
2293
 
2294
+ const hasBackendLogContextObject = (node: unknown): boolean => {
2295
+ if (!isObject(node) || node.type !== 'ObjectExpression') {
2296
+ return false;
2297
+ }
2298
+ for (const propertyName of objectExpressionPropertyNames(node)) {
2299
+ if (backendLogContextKeys.has(propertyName)) {
2300
+ return true;
2301
+ }
2302
+ }
2303
+ return false;
2304
+ };
2305
+
2306
+ const hasBackendLogSignal = (node: unknown): boolean => {
2307
+ if (!isObject(node)) {
2308
+ return false;
2309
+ }
2310
+ const identifierName = identifierNameFromNode(node);
2311
+ if (identifierName !== undefined && backendLogSignalKeys.has(identifierName)) {
2312
+ return true;
2313
+ }
2314
+ if (node.type === 'StringLiteral') {
2315
+ return typeof node.value === 'string' && node.value.trim().length > 0;
2316
+ }
2317
+ if (node.type === 'ObjectExpression') {
2318
+ for (const propertyName of objectExpressionPropertyNames(node)) {
2319
+ if (backendLogSignalKeys.has(propertyName)) {
2320
+ return true;
2321
+ }
2322
+ }
2323
+ return false;
2324
+ }
2325
+ if (node.type === 'MemberExpression') {
2326
+ const propertyName = identifierNameFromNode(node.property);
2327
+ return propertyName !== undefined && backendLogSignalKeys.has(propertyName);
2328
+ }
2329
+ return false;
2330
+ };
2331
+
2332
+ const isBackendLogWithoutContextCall = (node: AstNode): boolean => {
2333
+ if (!isLoggingCall(node)) {
2334
+ return false;
2335
+ }
2336
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
2337
+ if (!args.some(hasBackendLogSignal)) {
2338
+ return false;
2339
+ }
2340
+ return !args.some(hasBackendLogContextObject);
2341
+ };
2342
+
2343
+ export const findBackendLogWithoutContextLines = (ast: unknown): readonly number[] => {
2344
+ return collectLineMatchesWithAncestors(ast, (node) => isBackendLogWithoutContextCall(node));
2345
+ };
2346
+
2347
+ export const hasBackendLogWithoutContext = (ast: unknown): boolean =>
2348
+ findBackendLogWithoutContextLines(ast).length > 0;
2349
+
1913
2350
  export const hasEvalCall = (node: unknown): boolean => {
1914
2351
  return hasNode(node, (value) => {
1915
2352
  if (value.type !== 'CallExpression') {
@@ -2626,6 +3063,45 @@ export const findPersistenceMutationWithoutAuditEventMatch = (
2626
3063
  export const hasPersistenceMutationWithoutAuditEvent = (ast: unknown): boolean =>
2627
3064
  findPersistenceMutationWithoutAuditEventMatch(ast) !== undefined;
2628
3065
 
3066
+ const hardDeleteMethodNames = new Set(['delete', 'deleteMany', 'hardDelete', 'remove']);
3067
+
3068
+ const memberExpressionRootObjectName = (node: unknown): string | undefined => {
3069
+ if (!isObject(node)) {
3070
+ return undefined;
3071
+ }
3072
+ if (node.type === 'Identifier' && typeof node.name === 'string') {
3073
+ return node.name;
3074
+ }
3075
+ if (node.type === 'MemberExpression') {
3076
+ return memberExpressionRootObjectName(node.object);
3077
+ }
3078
+ return undefined;
3079
+ };
3080
+
3081
+ const isBackendHardDeleteCall = (node: AstNode): boolean => {
3082
+ const memberName = callExpressionMemberName(node);
3083
+ const objectName = callExpressionObjectName(node);
3084
+ const rootObjectName =
3085
+ node.type === 'CallExpression' && isObject(node.callee)
3086
+ ? memberExpressionRootObjectName(node.callee)
3087
+ : undefined;
3088
+ return (
3089
+ memberName !== undefined &&
3090
+ hardDeleteMethodNames.has(memberName) &&
3091
+ ((objectName !== undefined && persistenceObjectPattern.test(objectName)) ||
3092
+ (rootObjectName !== undefined && persistenceObjectPattern.test(rootObjectName)))
3093
+ );
3094
+ };
3095
+
3096
+ export const findBackendHardDeleteWithoutSoftDeleteLines = (
3097
+ ast: unknown
3098
+ ): readonly number[] => {
3099
+ return collectLineMatchesWithAncestors(ast, (node) => isBackendHardDeleteCall(node));
3100
+ };
3101
+
3102
+ export const hasBackendHardDeleteWithoutSoftDelete = (ast: unknown): boolean =>
3103
+ findBackendHardDeleteWithoutSoftDeleteLines(ast).length > 0;
3104
+
2629
3105
  const classValidatorDecoratorPattern =
2630
3106
  /^(Array|Is|Validate|Min|Max|Length|Matches|Contains|Equals|NotEquals|Allow|Expose|Exclude|Transform|Type)/;
2631
3107
  const dtoNestedDecoratorNames = new Set(['Type', 'ValidateNested']);