pumuki 6.3.293 → 6.3.295

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.
@@ -1910,6 +1910,149 @@ const objectExpressionPropertyNames = (node: AstNode): Set<string> => {
1910
1910
  );
1911
1911
  };
1912
1912
 
1913
+ const hasPropertyMatching = (propertyNames: ReadonlySet<string>, pattern: RegExp): boolean => {
1914
+ for (const propertyName of propertyNames) {
1915
+ if (pattern.test(propertyName)) {
1916
+ return true;
1917
+ }
1918
+ }
1919
+ return false;
1920
+ };
1921
+
1922
+ const backendAccessTokenPropertyPattern = /^(accessToken|access_token)$/;
1923
+ const backendRefreshTokenPropertyPattern = /^(refreshToken|refresh_token)$/;
1924
+
1925
+ const isBackendAuthResponseObjectWithoutRefreshToken = (node: AstNode): boolean => {
1926
+ if (node.type !== 'ObjectExpression') {
1927
+ return false;
1928
+ }
1929
+ const propertyNames = objectExpressionPropertyNames(node);
1930
+ return (
1931
+ hasPropertyMatching(propertyNames, backendAccessTokenPropertyPattern) &&
1932
+ !hasPropertyMatching(propertyNames, backendRefreshTokenPropertyPattern)
1933
+ );
1934
+ };
1935
+
1936
+ export const findBackendAuthResponseWithoutRefreshTokenLines = (ast: unknown): readonly number[] => {
1937
+ return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
1938
+ if (!isBackendAuthResponseObjectWithoutRefreshToken(node)) {
1939
+ return false;
1940
+ }
1941
+ const parent = ancestors[ancestors.length - 1];
1942
+ if (isObject(parent) && parent.type === 'ReturnStatement' && parent.argument === node) {
1943
+ return true;
1944
+ }
1945
+ return ancestors.some((ancestor) => {
1946
+ if (ancestor.type !== 'CallExpression' || !Array.isArray(ancestor.arguments)) {
1947
+ return false;
1948
+ }
1949
+ if (!ancestor.arguments.includes(node) || !isObject(ancestor.callee)) {
1950
+ return false;
1951
+ }
1952
+ const methodName = memberExpressionPropertyName(ancestor.callee);
1953
+ return methodName !== undefined && httpResponseMethodNames.has(methodName);
1954
+ });
1955
+ });
1956
+ };
1957
+
1958
+ export const hasBackendAuthResponseWithoutRefreshToken = (ast: unknown): boolean =>
1959
+ findBackendAuthResponseWithoutRefreshTokenLines(ast).length > 0;
1960
+
1961
+ const isProcessEnvMemberExpression = (node: unknown): boolean => {
1962
+ if (!isObject(node) || node.type !== 'MemberExpression') {
1963
+ return false;
1964
+ }
1965
+ const propertyName = memberExpressionPropertyName(node);
1966
+ if (typeof propertyName !== 'string' || propertyName.length === 0) {
1967
+ return false;
1968
+ }
1969
+ const objectNode = node.object;
1970
+ return (
1971
+ isObject(objectNode) &&
1972
+ objectNode.type === 'MemberExpression' &&
1973
+ memberExpressionObjectName(objectNode) === 'process' &&
1974
+ memberExpressionPropertyName(objectNode) === 'env'
1975
+ );
1976
+ };
1977
+
1978
+ const isLiteralConfigFallback = (node: unknown): boolean => {
1979
+ if (!isObject(node)) {
1980
+ return false;
1981
+ }
1982
+ return (
1983
+ node.type === 'StringLiteral' ||
1984
+ node.type === 'NumericLiteral' ||
1985
+ node.type === 'BooleanLiteral' ||
1986
+ (node.type === 'TemplateLiteral' &&
1987
+ Array.isArray(node.expressions) &&
1988
+ node.expressions.length === 0)
1989
+ );
1990
+ };
1991
+
1992
+ const isBackendProcessEnvDefaultFallback = (node: AstNode): boolean => {
1993
+ return (
1994
+ node.type === 'LogicalExpression' &&
1995
+ (node.operator === '||' || node.operator === '??') &&
1996
+ isProcessEnvMemberExpression(node.left) &&
1997
+ isLiteralConfigFallback(node.right)
1998
+ );
1999
+ };
2000
+
2001
+ export const findBackendProcessEnvDefaultFallbackLines = (ast: unknown): readonly number[] =>
2002
+ collectLineMatchesWithAncestors(ast, (node) => isBackendProcessEnvDefaultFallback(node), {
2003
+ max: 12,
2004
+ });
2005
+
2006
+ export const hasBackendProcessEnvDefaultFallback = (ast: unknown): boolean =>
2007
+ findBackendProcessEnvDefaultFallbackLines(ast).length > 0;
2008
+
2009
+ export const findBackendDirectProcessEnvReadLines = (ast: unknown): readonly number[] =>
2010
+ collectLineMatchesWithAncestors(ast, (node) => isProcessEnvMemberExpression(node), {
2011
+ max: 12,
2012
+ });
2013
+
2014
+ export const hasBackendDirectProcessEnvRead = (ast: unknown): boolean =>
2015
+ findBackendDirectProcessEnvReadLines(ast).length > 0;
2016
+
2017
+ const isConfigModuleForRootCall = (node: AstNode): boolean => {
2018
+ if (node.type !== 'CallExpression' || !isObject(node.callee)) {
2019
+ return false;
2020
+ }
2021
+ const callee = node.callee;
2022
+ return (
2023
+ callee.type === 'MemberExpression' &&
2024
+ memberExpressionObjectName(callee) === 'ConfigModule' &&
2025
+ memberExpressionPropertyName(callee) === 'forRoot'
2026
+ );
2027
+ };
2028
+
2029
+ const hasConfigValidationOption = (node: unknown): boolean => {
2030
+ if (!isObject(node) || node.type !== 'ObjectExpression') {
2031
+ return false;
2032
+ }
2033
+ const propertyNames = objectExpressionPropertyNames(node);
2034
+ return propertyNames.has('validationSchema') || propertyNames.has('validate');
2035
+ };
2036
+
2037
+ const isBackendConfigModuleForRootWithoutValidation = (node: AstNode): boolean => {
2038
+ if (!isConfigModuleForRootCall(node)) {
2039
+ return false;
2040
+ }
2041
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
2042
+ const options = args.find((argument) => isObject(argument) && argument.type === 'ObjectExpression');
2043
+ return !hasConfigValidationOption(options);
2044
+ };
2045
+
2046
+ export const findBackendConfigModuleWithoutValidationLines = (ast: unknown): readonly number[] =>
2047
+ collectLineMatchesWithAncestors(
2048
+ ast,
2049
+ (node) => isBackendConfigModuleForRootWithoutValidation(node),
2050
+ { max: 12 }
2051
+ );
2052
+
2053
+ export const hasBackendConfigModuleWithoutValidation = (ast: unknown): boolean =>
2054
+ findBackendConfigModuleWithoutValidationLines(ast).length > 0;
2055
+
1913
2056
  const isErrorStatusCallExpression = (node: unknown): boolean => {
1914
2057
  if (!isObject(node) || node.type !== 'CallExpression') {
1915
2058
  return false;
@@ -2155,7 +2298,7 @@ const isCorsPermissiveValue = (node: unknown): boolean => {
2155
2298
  );
2156
2299
  };
2157
2300
 
2158
- const objectPropertyName = (node: unknown): string | undefined => {
2301
+ const validationPipeObjectPropertyName = (node: unknown): string | undefined => {
2159
2302
  if (!isObject(node) || (node.type !== 'ObjectProperty' && node.type !== 'Property')) {
2160
2303
  return undefined;
2161
2304
  }
@@ -2259,6 +2402,34 @@ export const findBackendStringLiteralUnionEnumCandidateLines = (
2259
2402
  export const hasBackendStringLiteralUnionEnumCandidate = (ast: unknown): boolean =>
2260
2403
  findBackendStringLiteralUnionEnumCandidateLines(ast).length > 0;
2261
2404
 
2405
+ const backendCacheWriteMethodNames = new Set(['hset', 'mset', 'set', 'setex']);
2406
+ const backendCacheObjectPattern = /(cache|cacheManager|redis)/i;
2407
+
2408
+ const isBackendSensitiveCacheWriteCall = (node: AstNode): boolean => {
2409
+ if (node.type !== 'CallExpression' || !isObject(node.callee)) {
2410
+ return false;
2411
+ }
2412
+ const callee = node.callee;
2413
+ const methodName = memberExpressionPropertyName(callee);
2414
+ if (methodName === undefined || !backendCacheWriteMethodNames.has(methodName)) {
2415
+ return false;
2416
+ }
2417
+ const objectName =
2418
+ callee.type === 'MemberExpression' ? identifierNameFromNode(callee.object) : undefined;
2419
+ if (objectName === undefined || !backendCacheObjectPattern.test(objectName)) {
2420
+ return false;
2421
+ }
2422
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
2423
+ return args.some(hasSensitiveIdentifierReference);
2424
+ };
2425
+
2426
+ export const findBackendSensitiveCacheWriteLines = (ast: unknown): readonly number[] => {
2427
+ return collectLineMatchesWithAncestors(ast, (node) => isBackendSensitiveCacheWriteCall(node));
2428
+ };
2429
+
2430
+ export const hasBackendSensitiveCacheWrite = (ast: unknown): boolean =>
2431
+ findBackendSensitiveCacheWriteLines(ast).length > 0;
2432
+
2262
2433
  const isLoggingCall = (node: AstNode): boolean => {
2263
2434
  if (node.type !== 'CallExpression' || !isObject(node.callee)) {
2264
2435
  return false;
@@ -2766,6 +2937,12 @@ const nestJsClassDecoratorNames = new Set([
2766
2937
  ]);
2767
2938
  const nestJsRouteDecoratorNames = new Set(['Delete', 'Get', 'Head', 'Options', 'Patch', 'Post', 'Put']);
2768
2939
  const nestJsUseGuardsDecoratorNames = new Set(['UseGuards']);
2940
+ const nestJsRolesDecoratorNames = new Set(['Roles']);
2941
+ const nestJsOnEventDecoratorNames = new Set(['OnEvent']);
2942
+ const nestJsThrottleDecoratorNames = new Set(['Throttle']);
2943
+ const backendEventHandlerNamePattern = /(^handle[A-Z].*Event$|EventHandler$|EventsHandler$)/;
2944
+ const backendAuthRouteNamePattern =
2945
+ /(forgot|login|password|refresh|register|reset|signin|sign-in|signup|sign-up|token)/i;
2769
2946
 
2770
2947
  const decoratorExpressionName = (node: unknown): string | undefined => {
2771
2948
  if (!isObject(node)) {
@@ -2801,12 +2978,42 @@ const hasDecoratorFrom = (node: AstNode, allowedNames: ReadonlySet<string>): boo
2801
2978
  });
2802
2979
  };
2803
2980
 
2981
+ const decoratorCallArguments = (decorator: AstNode): readonly unknown[] => {
2982
+ const expression = decorator.expression;
2983
+ if (!isObject(expression) || expression.type !== 'CallExpression') {
2984
+ return [];
2985
+ }
2986
+ return Array.isArray(expression.arguments) ? expression.arguments : [];
2987
+ };
2988
+
2804
2989
  const hasNestJsRouteDecorator = (node: AstNode): boolean =>
2805
2990
  hasDecoratorFrom(node, nestJsRouteDecoratorNames);
2806
2991
 
2807
2992
  const hasNestJsUseGuardsDecorator = (node: AstNode): boolean =>
2808
2993
  hasDecoratorFrom(node, nestJsUseGuardsDecoratorNames);
2809
2994
 
2995
+ const isRolesSetMetadataDecorator = (decorator: AstNode): boolean => {
2996
+ const name = decoratorExpressionName(decorator.expression);
2997
+ if (name !== 'SetMetadata') {
2998
+ return false;
2999
+ }
3000
+ const [metadataKey] = decoratorCallArguments(decorator);
3001
+ return isObject(metadataKey) && metadataKey.type === 'StringLiteral' && metadataKey.value === 'roles';
3002
+ };
3003
+
3004
+ const hasNestJsRolesDecorator = (node: AstNode): boolean => {
3005
+ if (!Array.isArray(node.decorators)) {
3006
+ return false;
3007
+ }
3008
+ return node.decorators.some((decorator) => {
3009
+ if (!isObject(decorator)) {
3010
+ return false;
3011
+ }
3012
+ const name = decoratorExpressionName(decorator.expression);
3013
+ return (name !== undefined && nestJsRolesDecoratorNames.has(name)) || isRolesSetMetadataDecorator(decorator);
3014
+ });
3015
+ };
3016
+
2810
3017
  const isClassLikeNode = (node: unknown): node is AstNode =>
2811
3018
  isObject(node) && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression');
2812
3019
 
@@ -2926,6 +3133,102 @@ export const findBackendControllerRouteWithoutGuardLines = (ast: unknown): reado
2926
3133
  export const hasBackendControllerRouteWithoutGuard = (ast: unknown): boolean =>
2927
3134
  findBackendControllerRouteWithoutGuardLines(ast).length > 0;
2928
3135
 
3136
+ export const findBackendControllerRouteWithoutRolesLines = (ast: unknown): readonly number[] => {
3137
+ return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
3138
+ if (!isClassMethodLikeNode(node) || !hasNestJsRouteDecorator(node)) {
3139
+ return false;
3140
+ }
3141
+ const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
3142
+ const hasGuard = hasNestJsUseGuardsDecorator(node) || (ownerClass !== undefined && hasNestJsUseGuardsDecorator(ownerClass));
3143
+ if (!hasGuard) {
3144
+ return false;
3145
+ }
3146
+ return !hasNestJsRolesDecorator(node) && (ownerClass === undefined || !hasNestJsRolesDecorator(ownerClass));
3147
+ });
3148
+ };
3149
+
3150
+ export const hasBackendControllerRouteWithoutRoles = (ast: unknown): boolean =>
3151
+ findBackendControllerRouteWithoutRolesLines(ast).length > 0;
3152
+
3153
+ const isBackendEventHandlerClassOrMethod = (node: AstNode): boolean => {
3154
+ if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
3155
+ return backendEventHandlerNamePattern.test(classNameFromNode(node));
3156
+ }
3157
+ if (!isClassMethodLikeNode(node)) {
3158
+ return false;
3159
+ }
3160
+ const methodName = methodNameFromNode(node.key);
3161
+ return typeof methodName === 'string' && backendEventHandlerNamePattern.test(methodName);
3162
+ };
3163
+
3164
+ const hasOnEventDecoratedClassMember = (node: AstNode): boolean => {
3165
+ if (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression') {
3166
+ return false;
3167
+ }
3168
+ const members = isObject(node.body) && Array.isArray(node.body.body) ? node.body.body : [];
3169
+ return members.some((member) => isObject(member) && hasDecoratorFrom(member, nestJsOnEventDecoratorNames));
3170
+ };
3171
+
3172
+ export const findBackendEventHandlerWithoutOnEventLines = (ast: unknown): readonly number[] => {
3173
+ return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
3174
+ if (!isBackendEventHandlerClassOrMethod(node)) {
3175
+ return false;
3176
+ }
3177
+ if (hasDecoratorFrom(node, nestJsOnEventDecoratorNames)) {
3178
+ return false;
3179
+ }
3180
+ if (isClassLikeNode(node) && hasOnEventDecoratedClassMember(node)) {
3181
+ return false;
3182
+ }
3183
+ const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
3184
+ return ownerClass === undefined || !hasDecoratorFrom(ownerClass, nestJsOnEventDecoratorNames);
3185
+ });
3186
+ };
3187
+
3188
+ export const hasBackendEventHandlerWithoutOnEvent = (ast: unknown): boolean =>
3189
+ findBackendEventHandlerWithoutOnEventLines(ast).length > 0;
3190
+
3191
+ const isBackendAuthRouteMethod = (node: AstNode): boolean => {
3192
+ if (!isClassMethodLikeNode(node) || !hasNestJsRouteDecorator(node)) {
3193
+ return false;
3194
+ }
3195
+ const methodName = methodNameFromNode(node.key);
3196
+ if (methodName !== undefined && backendAuthRouteNamePattern.test(methodName)) {
3197
+ return true;
3198
+ }
3199
+ if (!Array.isArray(node.decorators)) {
3200
+ return false;
3201
+ }
3202
+ return node.decorators.some((decorator) => {
3203
+ if (!isObject(decorator)) {
3204
+ return false;
3205
+ }
3206
+ const decoratorName = decoratorExpressionName(decorator.expression);
3207
+ if (decoratorName === undefined || !nestJsRouteDecoratorNames.has(decoratorName)) {
3208
+ return false;
3209
+ }
3210
+ const [firstArgument] = decoratorCallArguments(decorator);
3211
+ const routePath = literalTextFromNode(firstArgument);
3212
+ return routePath !== undefined && backendAuthRouteNamePattern.test(routePath);
3213
+ });
3214
+ };
3215
+
3216
+ export const findBackendAuthRouteWithoutThrottleLines = (ast: unknown): readonly number[] => {
3217
+ return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
3218
+ if (!isBackendAuthRouteMethod(node)) {
3219
+ return false;
3220
+ }
3221
+ if (hasDecoratorFrom(node, nestJsThrottleDecoratorNames)) {
3222
+ return false;
3223
+ }
3224
+ const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
3225
+ return ownerClass === undefined || !hasDecoratorFrom(ownerClass, nestJsThrottleDecoratorNames);
3226
+ });
3227
+ };
3228
+
3229
+ export const hasBackendAuthRouteWithoutThrottle = (ast: unknown): boolean =>
3230
+ findBackendAuthRouteWithoutThrottleLines(ast).length > 0;
3231
+
2929
3232
  const persistenceMutationNames = new Set([
2930
3233
  'create',
2931
3234
  'delete',
@@ -3128,6 +3431,19 @@ const hasClassValidatorDecorator = (node: AstNode): boolean => {
3128
3431
  });
3129
3432
  };
3130
3433
 
3434
+ const hasApiPropertyDecorator = (node: AstNode): boolean => {
3435
+ if (!Array.isArray(node.decorators)) {
3436
+ return false;
3437
+ }
3438
+ return node.decorators.some((decorator) => {
3439
+ if (!isObject(decorator)) {
3440
+ return false;
3441
+ }
3442
+ const name = decoratorExpressionName(decorator.expression);
3443
+ return name === 'ApiProperty' || name === 'ApiPropertyOptional';
3444
+ });
3445
+ };
3446
+
3131
3447
  const hasDtoNestedValidationDecorator = (node: AstNode): boolean => {
3132
3448
  if (!Array.isArray(node.decorators)) {
3133
3449
  return false;
@@ -3227,6 +3543,19 @@ export const findDtoPropertyWithoutValidationMatch = (
3227
3543
  export const hasDtoPropertyWithoutValidation = (ast: unknown): boolean =>
3228
3544
  findDtoPropertyWithoutValidationMatch(ast) !== undefined;
3229
3545
 
3546
+ export const findDtoPropertyWithoutApiPropertyLines = (ast: unknown): readonly number[] =>
3547
+ collectLineMatchesWithAncestors(ast, (node, ancestors) => {
3548
+ if (!isDtoPropertyNode(node) || hasApiPropertyDecorator(node)) {
3549
+ return false;
3550
+ }
3551
+ return ancestors.some(
3552
+ (ancestor) => ancestor.type === 'ClassDeclaration' && isDtoClassName(classNameFromNode(ancestor))
3553
+ );
3554
+ }, { max: 12 });
3555
+
3556
+ export const hasDtoPropertyWithoutApiProperty = (ast: unknown): boolean =>
3557
+ findDtoPropertyWithoutApiPropertyLines(ast).length > 0;
3558
+
3230
3559
  export type TypeScriptDtoNestedPropertyWithoutNestedValidationMatch = {
3231
3560
  lines: readonly number[];
3232
3561
  primary_node: string;
@@ -3290,6 +3619,329 @@ export const findDtoNestedPropertyWithoutNestedValidationMatch = (
3290
3619
  export const hasDtoNestedPropertyWithoutNestedValidation = (ast: unknown): boolean =>
3291
3620
  findDtoNestedPropertyWithoutNestedValidationMatch(ast) !== undefined;
3292
3621
 
3622
+ const isValidationPipeCallee = (node: unknown): boolean => {
3623
+ return isObject(node) && node.type === 'Identifier' && node.name === 'ValidationPipe';
3624
+ };
3625
+
3626
+ const objectPropertyName = (node: unknown): string | undefined => {
3627
+ if (!isObject(node)) {
3628
+ return undefined;
3629
+ }
3630
+ if (isObject(node.key)) {
3631
+ return methodNameFromNode(node.key);
3632
+ }
3633
+ return undefined;
3634
+ };
3635
+
3636
+ const isBooleanTrueLiteral = (node: unknown): boolean => {
3637
+ return isObject(node) && node.type === 'BooleanLiteral' && node.value === true;
3638
+ };
3639
+
3640
+ const validationPipeOptionsEnableWhitelist = (node: unknown): boolean => {
3641
+ if (!isObject(node) || node.type !== 'ObjectExpression' || !Array.isArray(node.properties)) {
3642
+ return false;
3643
+ }
3644
+ return node.properties.some((property) => {
3645
+ return (
3646
+ isObject(property) &&
3647
+ validationPipeObjectPropertyName(property) === 'whitelist' &&
3648
+ isBooleanTrueLiteral(property.value)
3649
+ );
3650
+ });
3651
+ };
3652
+
3653
+ const isStrictValidationPipeExpression = (node: unknown): boolean => {
3654
+ if (!isObject(node)) {
3655
+ return false;
3656
+ }
3657
+ if (node.type !== 'NewExpression' && node.type !== 'CallExpression') {
3658
+ return false;
3659
+ }
3660
+ if (!isValidationPipeCallee(node.callee)) {
3661
+ return false;
3662
+ }
3663
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
3664
+ return args.some(validationPipeOptionsEnableWhitelist);
3665
+ };
3666
+
3667
+ const isUseGlobalPipesCall = (node: AstNode): boolean => {
3668
+ return node.type === 'CallExpression' && callExpressionMemberName(node) === 'useGlobalPipes';
3669
+ };
3670
+
3671
+ const isStrictUseGlobalPipesCall = (node: AstNode): boolean => {
3672
+ if (!isUseGlobalPipesCall(node)) {
3673
+ return false;
3674
+ }
3675
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
3676
+ return args.some(isStrictValidationPipeExpression);
3677
+ };
3678
+
3679
+ const isBackendValidationNestFactoryCreateCall = (node: AstNode): boolean => {
3680
+ if (node.type !== 'CallExpression') {
3681
+ return false;
3682
+ }
3683
+ const memberName = callExpressionMemberName(node);
3684
+ const objectName = callExpressionObjectName(node);
3685
+ return memberName === 'create' && objectName === 'NestFactory';
3686
+ };
3687
+
3688
+ export const findBackendMissingGlobalValidationPipeLines = (ast: unknown): readonly number[] => {
3689
+ const unsafeUseGlobalPipesLines = collectLineMatchesWithAncestors(
3690
+ ast,
3691
+ (node) => isUseGlobalPipesCall(node) && !isStrictUseGlobalPipesCall(node)
3692
+ );
3693
+ if (unsafeUseGlobalPipesLines.length > 0) {
3694
+ return unsafeUseGlobalPipesLines;
3695
+ }
3696
+
3697
+ if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
3698
+ return [];
3699
+ }
3700
+ if (hasNode(ast, isStrictUseGlobalPipesCall)) {
3701
+ return [];
3702
+ }
3703
+ return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
3704
+ };
3705
+
3706
+ export const hasBackendMissingGlobalValidationPipe = (ast: unknown): boolean =>
3707
+ findBackendMissingGlobalValidationPipeLines(ast).length > 0;
3708
+
3709
+ const isHelmetCallee = (node: unknown): boolean => {
3710
+ return isObject(node) && node.type === 'Identifier' && node.name === 'helmet';
3711
+ };
3712
+
3713
+ const isHelmetCallExpression = (node: unknown): boolean => {
3714
+ return isObject(node) && node.type === 'CallExpression' && isHelmetCallee(node.callee);
3715
+ };
3716
+
3717
+ const isAppUseHelmetCall = (node: AstNode): boolean => {
3718
+ if (node.type !== 'CallExpression' || callExpressionMemberName(node) !== 'use') {
3719
+ return false;
3720
+ }
3721
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
3722
+ return args.some(isHelmetCallExpression);
3723
+ };
3724
+
3725
+ export const findBackendMissingHelmetSecurityHeadersLines = (ast: unknown): readonly number[] => {
3726
+ if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
3727
+ return [];
3728
+ }
3729
+ if (hasNode(ast, isAppUseHelmetCall)) {
3730
+ return [];
3731
+ }
3732
+ return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
3733
+ };
3734
+
3735
+ export const hasBackendMissingHelmetSecurityHeaders = (ast: unknown): boolean =>
3736
+ findBackendMissingHelmetSecurityHeadersLines(ast).length > 0;
3737
+
3738
+ const isCompressionCallee = (node: unknown): boolean => {
3739
+ return isObject(node) && node.type === 'Identifier' && node.name === 'compression';
3740
+ };
3741
+
3742
+ const isCompressionCallExpression = (node: unknown): boolean => {
3743
+ return isObject(node) && node.type === 'CallExpression' && isCompressionCallee(node.callee);
3744
+ };
3745
+
3746
+ const isAppUseCompressionCall = (node: AstNode): boolean => {
3747
+ if (node.type !== 'CallExpression' || callExpressionMemberName(node) !== 'use') {
3748
+ return false;
3749
+ }
3750
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
3751
+ return args.some(isCompressionCallExpression);
3752
+ };
3753
+
3754
+ export const findBackendMissingCompressionMiddlewareLines = (
3755
+ ast: unknown
3756
+ ): readonly number[] => {
3757
+ if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
3758
+ return [];
3759
+ }
3760
+ if (hasNode(ast, isAppUseCompressionCall)) {
3761
+ return [];
3762
+ }
3763
+ return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
3764
+ };
3765
+
3766
+ export const hasBackendMissingCompressionMiddleware = (ast: unknown): boolean =>
3767
+ findBackendMissingCompressionMiddlewareLines(ast).length > 0;
3768
+
3769
+ const backendRouteDecoratorNames = new Set([
3770
+ 'All',
3771
+ 'Delete',
3772
+ 'Get',
3773
+ 'Head',
3774
+ 'Options',
3775
+ 'Patch',
3776
+ 'Post',
3777
+ 'Put',
3778
+ ]);
3779
+
3780
+ const backendControllerBusinessLogicNodeTypes = new Set([
3781
+ 'DoWhileStatement',
3782
+ 'ForInStatement',
3783
+ 'ForOfStatement',
3784
+ 'ForStatement',
3785
+ 'IfStatement',
3786
+ 'SwitchStatement',
3787
+ 'WhileStatement',
3788
+ ]);
3789
+
3790
+ const hasBackendRouteDecorator = (node: AstNode): boolean => {
3791
+ if (!Array.isArray(node.decorators)) {
3792
+ return false;
3793
+ }
3794
+ return node.decorators.some((decorator) => {
3795
+ if (!isObject(decorator)) {
3796
+ return false;
3797
+ }
3798
+ const name = decoratorExpressionName(decorator.expression);
3799
+ return name !== undefined && backendRouteDecoratorNames.has(name);
3800
+ });
3801
+ };
3802
+
3803
+ const isBackendControllerRouteMethod = (node: AstNode): boolean => {
3804
+ return (
3805
+ (node.type === 'ClassMethod' ||
3806
+ node.type === 'ClassPrivateMethod' ||
3807
+ node.type === 'ObjectMethod') &&
3808
+ hasBackendRouteDecorator(node)
3809
+ );
3810
+ };
3811
+
3812
+ const isBackendControllerBusinessLogicNode = (node: AstNode): boolean => {
3813
+ if (typeof node.type === 'string' && backendControllerBusinessLogicNodeTypes.has(node.type)) {
3814
+ return true;
3815
+ }
3816
+ if (isPersistenceMutationCall(node)) {
3817
+ return true;
3818
+ }
3819
+ const objectName = callExpressionObjectName(node);
3820
+ const rootObjectName =
3821
+ node.type === 'CallExpression' && isObject(node.callee)
3822
+ ? memberExpressionRootObjectName(node.callee)
3823
+ : undefined;
3824
+ return (
3825
+ (objectName !== undefined && /(?:repository|repo|prisma|supabase|model|collection)$/i.test(objectName)) ||
3826
+ (rootObjectName !== undefined && /(?:repository|repo|prisma|supabase|model|collection)$/i.test(rootObjectName))
3827
+ );
3828
+ };
3829
+
3830
+ export type TypeScriptBackendControllerBusinessLogicMatch = {
3831
+ lines: readonly number[];
3832
+ primary_node: string;
3833
+ related_nodes: readonly string[];
3834
+ why: string;
3835
+ impact: string;
3836
+ expected_fix: string;
3837
+ };
3838
+
3839
+ export const findBackendControllerBusinessLogicMatch = (
3840
+ ast: unknown
3841
+ ): TypeScriptBackendControllerBusinessLogicMatch | undefined => {
3842
+ const match = findFirstNodeWithAncestors(ast, (node) => {
3843
+ if (!isBackendControllerRouteMethod(node)) {
3844
+ return false;
3845
+ }
3846
+ return hasDescendantNode(
3847
+ node,
3848
+ (nested) => nested !== node && isBackendControllerBusinessLogicNode(nested)
3849
+ );
3850
+ });
3851
+
3852
+ if (match === undefined) {
3853
+ return undefined;
3854
+ }
3855
+
3856
+ const methodName = methodNameFromNode(match.node.key) ?? 'anonymous controller route';
3857
+ const line = toPositiveLine(match.node);
3858
+
3859
+ return {
3860
+ lines: line === null ? [] : sortedUniqueLines([line]),
3861
+ primary_node: methodName,
3862
+ related_nodes: ['NestJS route handler', 'business logic in controller'],
3863
+ why: `El handler ${methodName} contiene logica de negocio o acceso a persistencia dentro del controller.`,
3864
+ impact:
3865
+ 'El controller deja de ser una frontera HTTP delgada, mezcla routing con dominio/aplicacion y dificulta testabilidad y reutilizacion.',
3866
+ expected_fix:
3867
+ 'Mueve la logica a un service/use case inyectado y deja el controller solo con routing, validacion de entrada y delegacion.',
3868
+ };
3869
+ };
3870
+
3871
+ export const findBackendControllerBusinessLogicLines = (ast: unknown): readonly number[] =>
3872
+ findBackendControllerBusinessLogicMatch(ast)?.lines ?? [];
3873
+
3874
+ export const hasBackendControllerBusinessLogic = (ast: unknown): boolean =>
3875
+ findBackendControllerBusinessLogicMatch(ast) !== undefined;
3876
+
3877
+ const isBackendRepositoryClassName = (name: string | undefined): boolean =>
3878
+ name !== undefined && /(?:Repository|Repo)$/i.test(name);
3879
+
3880
+ const isBackendRepositoryBusinessLogicMethod = (node: AstNode): boolean => {
3881
+ if (
3882
+ node.type !== 'ClassMethod' &&
3883
+ node.type !== 'ClassPrivateMethod' &&
3884
+ node.type !== 'ObjectMethod'
3885
+ ) {
3886
+ return false;
3887
+ }
3888
+ return hasDescendantNode(node, (nested) => {
3889
+ return (
3890
+ nested !== node &&
3891
+ typeof nested.type === 'string' &&
3892
+ backendControllerBusinessLogicNodeTypes.has(nested.type)
3893
+ );
3894
+ });
3895
+ };
3896
+
3897
+ export type TypeScriptBackendRepositoryBusinessLogicMatch = {
3898
+ lines: readonly number[];
3899
+ primary_node: string;
3900
+ related_nodes: readonly string[];
3901
+ why: string;
3902
+ impact: string;
3903
+ expected_fix: string;
3904
+ };
3905
+
3906
+ export const findBackendRepositoryBusinessLogicMatch = (
3907
+ ast: unknown
3908
+ ): TypeScriptBackendRepositoryBusinessLogicMatch | undefined => {
3909
+ const match = findFirstNodeWithAncestors(ast, (node) => {
3910
+ if (node.type !== 'ClassDeclaration' || !isBackendRepositoryClassName(classNameFromNode(node))) {
3911
+ return false;
3912
+ }
3913
+ const body = isObject(node.body) && Array.isArray(node.body.body) ? node.body.body : [];
3914
+ return body.some((member) => isObject(member) && isBackendRepositoryBusinessLogicMethod(member));
3915
+ });
3916
+
3917
+ if (match === undefined) {
3918
+ return undefined;
3919
+ }
3920
+
3921
+ const className = classNameFromNode(match.node) ?? 'Repository';
3922
+ const body = isObject(match.node.body) && Array.isArray(match.node.body.body) ? match.node.body.body : [];
3923
+ const method = body.find((member) => isObject(member) && isBackendRepositoryBusinessLogicMethod(member));
3924
+ const methodName = isObject(method) ? methodNameFromNode(method.key) : undefined;
3925
+ const line = isObject(method) ? toPositiveLine(method) : toPositiveLine(match.node);
3926
+
3927
+ return {
3928
+ lines: line === null ? [] : sortedUniqueLines([line]),
3929
+ primary_node: className,
3930
+ related_nodes: methodName === undefined ? ['repository method'] : [methodName],
3931
+ why: `El repositorio ${className} contiene ramas de decision dentro de un metodo de persistencia.`,
3932
+ impact:
3933
+ 'El repositorio mezcla reglas de negocio con acceso a datos, rompiendo Clean Architecture y dificultando pruebas de casos de uso.',
3934
+ expected_fix:
3935
+ 'Mueve la decision de negocio a un use case/service y deja el repositorio limitado a CRUD, queries expresivas y mapeo de datos.',
3936
+ };
3937
+ };
3938
+
3939
+ export const findBackendRepositoryBusinessLogicLines = (ast: unknown): readonly number[] =>
3940
+ findBackendRepositoryBusinessLogicMatch(ast)?.lines ?? [];
3941
+
3942
+ export const hasBackendRepositoryBusinessLogic = (ast: unknown): boolean =>
3943
+ findBackendRepositoryBusinessLogicMatch(ast) !== undefined;
3944
+
3293
3945
  const isRecordTypeName = (node: unknown): boolean => {
3294
3946
  return (
3295
3947
  isObject(node) &&