diff --git a/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.spec.ts b/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.spec.ts index 0e936b5..6e32c5c 100644 --- a/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.spec.ts +++ b/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.spec.ts @@ -9,6 +9,8 @@ ruleTester.run("all-properties-are-whitelisted", rule, { valid: [ { code: ` +import { A } from 'class-validator'; + class A { @A b: string @@ -17,6 +19,8 @@ class A { }, { code: ` +import { A } from 'class-validator'; + class A { @A() b: string @@ -25,6 +29,8 @@ class A { }, { code: ` +import { A } from 'class-validator'; + class A { b: string } @@ -32,11 +38,25 @@ class A { }, { code: ` +import { IsString, Allow } from 'class-validator'; + class A { @IsString() b: string @Allow() + b: string +} + `, + }, + { + code: ` +import { IsInt } from 'sequelize-typescript'; + +class A { + @IsInt() + b: string + b: string } `, @@ -45,7 +65,9 @@ class A { invalid: [ { code: ` -class A { +import { Allow } from 'class-validator'; + +class MyClass { @Allow() b: string diff --git a/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.ts b/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.ts index c663725..87bee4a 100644 --- a/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.ts +++ b/src/rules/allPropertiesAreWhitelisted/allPropertiesAreWhitelisted.ts @@ -1,8 +1,6 @@ -import {AST_NODE_TYPES, TSESTree, TSESLint} from "@typescript-eslint/utils"; +import {AST_NODE_TYPES, TSESLint, TSESTree} from "@typescript-eslint/utils"; import {createRule} from "../../utils/createRule"; -import {classValidatorDecorators} from "../../utils/classValidatorDecorators"; - -const CLASS_VALIDATOR_DECORATOR_NAMES = new Set(classValidatorDecorators); +import {typedTokenHelpers} from "../../utils/typedTokenHelpers"; const rule = createRule({ name: "all-properties-are-whitelisted", @@ -29,6 +27,7 @@ const rule = createRule({ return { // eslint-disable-next-line @typescript-eslint/naming-convention ClassDeclaration(node) { + const program = typedTokenHelpers.getRootProgram(node); const withDecorator: TSESTree.PropertyDefinition[] = []; const withoutDecorator: TSESTree.PropertyDefinition[] = []; for (const element of node.body.body) { @@ -41,8 +40,9 @@ const rule = createRule({ AST_NODE_TYPES.CallExpression && decorator.expression.callee.type === AST_NODE_TYPES.Identifier && - CLASS_VALIDATOR_DECORATOR_NAMES.has( - decorator.expression.callee.name + typedTokenHelpers.decoratorIsClassValidatorDecorator( + program, + decorator ) ); if (hasDecorator) { diff --git a/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.test.ts b/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.test.ts index 685a514..79eb0b4 100644 --- a/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.test.ts +++ b/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.test.ts @@ -18,6 +18,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { // is a primitive type date with custom transform - https://github.com/darraghoriordan/eslint-plugin-nestjs-typed/issues/32 options: [{additionalTypeDecorators: ["TransformDate"]}], code: ` + import { IsOptional, IsDate } from 'class-validator'; + class ExampleDto { @ApiPropertyOptional( { @@ -72,12 +74,27 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // no validation decorator code: ` + import { IsDefined } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @IsDefined() @Type(()=> Person) members!: Person; } + `, + }, + { + // sequelize-typescript validator with class-validator name conflict + code: ` + import { IsInt } from 'sequelize-typescript' + + export class CreateOrganisationDto { + @ApiProperty({ type: Person, isArray: true }) + @IsDefined() + @IsInt + members!: Person; + } `, }, { @@ -92,6 +109,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // has the type decorator already code: ` + import { IsArray } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) @@ -104,6 +123,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is a primitive type code: ` + import { IsBoolean } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) @@ -115,6 +136,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is not a primitive type so skip code: ` + import { Allow } from 'class-validator'; + enum Foo { BAR } @@ -129,6 +152,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is an array - should have type and has it so pass! code: ` + import { ValidateNested } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) @@ -142,6 +167,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is an array - should have type code: ` + import { ValidateNested } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) @@ -157,6 +184,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is an OPTIONAL array - should have type code: ` + import { ValidateNested } from 'class-validator'; + export class Foo {} export class CreateOrganisationDto { @@ -174,6 +203,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is an array with union - should have type code: ` + import { ValidateNested, IsArray } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) @@ -190,6 +221,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is not a primitive type code: ` + import { ValidateNested, IsDate } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) @@ -206,6 +239,8 @@ ruleTester.run("validated-non-primitive-property-needs-type-decorator", rule, { { // is a custom class code: ` + import { ValidateNested, IsDefined } from 'class-validator'; + export class CreateOrganisationDto { @ApiProperty({ type: Person, isArray: true }) @ValidateNested({each:true}) diff --git a/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.ts b/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.ts index 1814acd..d63845a 100644 --- a/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.ts +++ b/src/rules/validate-non-primitves-needs-type-decorator/validateNonPrimitiveNeedsDecorators.ts @@ -7,7 +7,6 @@ import { } from "@typescript-eslint/utils"; import {createRule} from "../../utils/createRule"; import {typedTokenHelpers} from "../../utils/typedTokenHelpers"; -import {classValidatorDecorators} from "../../utils/classValidatorDecorators"; const primitiveTypes = new Set([ AST_NODE_TYPES.TSStringKeyword, @@ -141,10 +140,23 @@ const rule = createRule({ // property has a validation decorator but not IsEnum // (we don't care about un-validated properties and enums don't need Type()) const foundClassValidatorDecorators = - typedTokenHelpers.getDecoratorsNamed( - node, - classValidatorDecorators.filter((x) => x !== "IsEnum") - ); + typedTokenHelpers.getImportedClassValidatorDecorators(node); + const hasEnum = foundClassValidatorDecorators.some( + (foundClassValidatorDecorator) => + typedTokenHelpers.decoratorIsIsEnum( + foundClassValidatorDecorator + ) + ); + + if (hasEnum) { + return; + } + + // const foundClassValidatorDecorators = + // typedTokenHelpers.getDecoratorsNamed( + // node, + // classValidatorDecorators.filter((x) => x !== "IsEnum") + // ); if (foundClassValidatorDecorators.length === 0) { return; } diff --git a/src/utils/classValidatorDecorators.ts b/src/utils/classValidatorDecorators.ts deleted file mode 100644 index 454b105..0000000 --- a/src/utils/classValidatorDecorators.ts +++ /dev/null @@ -1,100 +0,0 @@ -export const classValidatorDecorators = [ - "IsBooleanString", - "IsPositive", - "IsLatLong", - "IsLongitude", - "IsLatitude", - "IsNegative", - "Contains", - "Equals", - "MinDate", - "MaxDate", - "IsAlpha", - "IsAlphanumeric", - "IsAscii", - "IsDecimal", - "IsBase64", - "IsBoolean", - "IsByteLength", - "IsCreditCard", - "IsCurrency", - "IsDate", - "IsDivisibleBy", - "IsEmail", - "IsEnum", - "IsFQDN", - "IsFullWidth", - "IsHalfWidth", - "IsVariableWidth", - "IsHexColor", - "IsHexadecimal", - "IsIP", - "IsISBN", - "IsISO8601", - "IsIn", - "IsInt", - "IsJSON", - "IsJWT", - "IsObject", - "IsNotEmptyObject", - "Length", - "IsLowercase", - "IsMongoId", - "IsMultibyte", - "IsNumberString", - "IsSurrogatePair", - "IsUrl", - "IsUUID", - "IsUppercase", - "Matches", - "MinLength", - "MaxLength", - "Min", - "Max", - "IsNotEmpty", - "IsMilitaryTime", - "ArrayNotEmpty", - "ArrayMinSize", - "ArrayMaxSize", - "NotEquals", - "IsEmpty", - "IsDefined", - "IsNotIn", - "IsNumber", - "IsString", - "NotContains", - "ArrayContains", - "ArrayNotContains", - "ArrayUnique", - "IsArray", - "IsDateString", - "IsInstance", - "IsPhoneNumber", - "IsISO31661Alpha2", - "IsISO31661Alpha3", - "IsHash", - "IsMACAddress", - "IsISSN", - "IsFirebasePushId", - "Allow", - "IsEAN", - "IsEthereumAddress", - "IsBtcAddress", - "IsDataURI", - "IsHSL", - "IsRgbColor", - "IsIdentityCard", - "IsBase32", - "IsIBAN", - "IsBIC", - "IsISRC", - "IsRFC3339", - "IsLocale", - "IsMagnetURI", - "IsMimeType", - "IsOctal", - "IsPassportNumber", - "IsPostalCode", - "IsSemVer", - "ValidateNested", -]; diff --git a/src/utils/typedTokenHelpers.ts b/src/utils/typedTokenHelpers.ts index 0e85546..b55903e 100644 --- a/src/utils/typedTokenHelpers.ts +++ b/src/utils/typedTokenHelpers.ts @@ -1,8 +1,9 @@ -import {AST_NODE_TYPES, TSESTree, TSESLint} from "@typescript-eslint/utils"; +import {AST_NODE_TYPES, TSESLint, TSESTree} from "@typescript-eslint/utils"; import {parse, ParserServices} from "@typescript-eslint/parser"; import ts from "typescript"; -import {unionTypeParts} from "tsutils"; import * as tsutils from "tsutils"; +import {unionTypeParts} from "tsutils"; + export const typedTokenHelpers = { decoratorsThatCouldMeanTheDevIsValidatingAnArray: [ "IsArray", @@ -109,6 +110,7 @@ export const typedTokenHelpers = { )?.name; const decoratorIdentifier = (d.expression as TSESTree.Identifier) ?.name; + return decoratorNames.includes( factoryMethodDecoratorIdentifier ?? decoratorIdentifier ?? "" ); @@ -163,4 +165,130 @@ export const typedTokenHelpers = { node.optional || isUndefinedType || false; return isOptionalPropertyValue; }, + /** + * Checks if an import is an import of the given decorator name + * @param imp + * @param decoratorName + */ + importIsDecorator( + imp: TSESTree.ImportDeclaration, + decoratorName: string + ): boolean { + const isFromClassValidator = + imp.source.value.startsWith("class-validator"); + const isDecoratorImport = imp.specifiers.some( + (specifier) => specifier.local.name === decoratorName + ); + + return isFromClassValidator && isDecoratorImport; + }, + /** + * Checks if decorator is in imports of a node + * @param imports + * @param decorator + */ + decoratorIsImportedFromClassValidator( + imports: TSESTree.ImportDeclaration[], + decorator: TSESTree.Decorator + ): boolean { + const decoratorName = this.getDecoratorName(decorator); + + if (!decoratorName) { + return false; + } + + return imports.some((imp) => + typedTokenHelpers.importIsDecorator(imp, decoratorName) + ); + }, + /** + * Checks whether a decorator is a class validator decorator + * @param program The root program node + * @param decorator The decorator node + */ + decoratorIsClassValidatorDecorator( + program: TSESTree.Program | null, + decorator: TSESTree.Decorator + ): boolean { + if (!program) { + return false; + } + + const imports = program.body.filter( + (node): node is TSESTree.ImportDeclaration => + node.type === TSESTree.AST_NODE_TYPES.ImportDeclaration + ); + + return typedTokenHelpers.decoratorIsImportedFromClassValidator( + imports, + decorator + ); + }, + /** + * Gets the root program of a node + * @param node + */ + getRootProgram(node: TSESTree.BaseNode): TSESTree.Program | null { + let root = node; + + while (root.parent) { + if (root.parent.type === TSESTree.AST_NODE_TYPES.Program) { + return root.parent; + } + + root = root.parent; + } + + return null; + }, + /** + * Gets all the decorators actually imported from class-validator lib + * @param node PropertyDefinition node + */ + getImportedClassValidatorDecorators( + node: TSESTree.PropertyDefinition + ): TSESTree.Decorator[] { + const program = typedTokenHelpers.getRootProgram(node); + + const {decorators} = node; + + return ( + decorators?.filter((decorator): decorator is TSESTree.Decorator => { + return typedTokenHelpers.decoratorIsClassValidatorDecorator( + program, + decorator + ); + }) ?? [] + ); + }, + /** + * Checks if the decorator is the IsEnum decorator + * @param decorator + */ + decoratorIsIsEnum(decorator: TSESTree.Decorator): boolean { + const decoratorName = this.getDecoratorName(decorator); + + return decoratorName === "IsEnum"; + }, + /** + * Gets the name of a decorator + * Returns null if no name is found + * @param decorator + */ + getDecoratorName(decorator: TSESTree.Decorator): string | null { + if ( + decorator.expression.type !== TSESTree.AST_NODE_TYPES.CallExpression + ) { + return null; + } + + if ( + decorator.expression.callee.type !== + TSESTree.AST_NODE_TYPES.Identifier + ) { + return null; + } + + return decorator.expression.callee.name; + }, };