import fs from "fs" ; import { DiagnosticCategory , Node , Project , TypeChecker } from "ts-morph" ; import { PackageDetails } from "./packageJson" ; import { ClassData , decomposeClassDeclaration } from "./classDecomposition" ; import { generateTypeDataForProject , getFullTypeName , PackageAndTypeData , TypeData , } from "./typeData" ; import { DecompositionResult , GenericsInfo } from "./typeDecomposition" ; import { log } from "./validatorUtils" ; export enum BreakingIncrement { none = 0 , minor = 1 , major = minor << 1 | minor } ; export type BrokenTypes = Map < string , BreakingIncrement > ; export interface PackageResult { increment : BreakingIncrement ; brokenTypes : BrokenTypes ; } interface DecompositionTypeData extends TypeData { classData ? : ClassData ; } param packageDetails param packageDir param brokenTypes returns export function validatePackage ( packageDetails , packageDir , brokenTypes ) { let pkgIncrement = BreakingIncrement . none ; const pkgBrokenTypes = new Map ( ) ; if ( ! fs . existsSync ( `${ packageDir }/tsconfig.json` ) || packageDetails . oldVersions === undefined || packageDetails . oldVersions . length === 0 ) { return { increment : pkgIncrement , brokenTypes : pkgBrokenTypes } ; } const oldVersion = packageDetails . oldVersions [ packageDetails . oldVersions . length - 1 ] ; const newDetails = generateTypeDataForProject ( packageDir , undefined ) ; const oldDetails = generateTypeDataForProject ( packageDir , oldVersion ) ; const newTypeMap = new Map < string , TypeData > ( newDetails . typeData . map ( ( v ) => [ getFullTypeName ( v ) , v ] ) ) ; const oldTypeMap = new Map < string , TypeData > ( oldDetails . typeData . map ( ( v ) => [ getFullTypeName ( v ) , v ] ) ) ; const project = newDetails . project ; for ( const oldTypeData of oldDetails . typeData ) { oldTypeData as DecompositionTypeData ; const newTypeData = newTypeMap . get ( getFullTypeName ( oldTypeData ) ) as DecompositionTypeData | undefined ; log ( `Validating type ${ oldTypeData . name }` ) ; if ( newTypeData === undefined ) { log ( "Type was removed" ) ; pkgIncrement |= BreakingIncrement . major ; } else { tryDecomposeTypeData ( oldDetails . project . getTypeChecker ( ) , oldTypeData ) ; tryDecomposeTypeData ( newDetails . project . getTypeChecker ( ) , newTypeData ) ; const typeIncrement = checkMajorIncrement ( project , packageDir , oldTypeData , newTypeData ) ; if ( typeIncrement !== BreakingIncrement . none ) { log ( "Check found major increment" ) ; pkgIncrement |= typeIncrement ; pkgBrokenTypes . set ( oldTypeData . name , typeIncrement ) ; } else if ( checkMinorIncrement ( project , packageDir , oldTypeData , newTypeData ) ) { log ( "Check found minor increment" ) ; pkgIncrement |= BreakingIncrement . minor ; pkgBrokenTypes . set ( oldTypeData . name , BreakingIncrement . minor ) ; } else { log ( "Check did not find increment" ) ; } } newTypeMap . delete ( getFullTypeName ( oldTypeData ) ) ; } newTypeMap . forEach ( ( value , key ) => { log ( `New type added ${ key }` ) ; pkgIncrement |= BreakingIncrement . minor ; pkgBrokenTypes . set ( key , BreakingIncrement . minor ) ; } ) ; return { increment : pkgIncrement , brokenTypes : pkgBrokenTypes } ; } function tryDecomposeTypeData ( typeChecker , typeData ) { if ( typeData . classData !== undefined ) { return true ; } else if ( Node . isClassDeclaration ( typeData . node ) ) { typeData . classData = decomposeClassDeclaration ( typeChecker , typeData . node ) ; } else { return false ; } return true ; } function checkMajorIncrement ( project , pkgDir , oldTypeData , newTypeData ) { let testFile = "" ; if ( oldTypeData . classData !== undefined && newTypeData . classData !== undefined ) { testFile = buildClassTestFileMajor ( `old${ getFullTypeName ( oldTypeData ) }` , oldTypeData . classData , `new${ getFullTypeName ( newTypeData ) }` , newTypeData . classData ) ; log ( testFile ) ; } const sourcePath = `${ pkgDir }/src/test/typeValidation.spec.ts` ; const sourceFile = project . createSourceFile ( sourcePath , testFile ) ; const diagnostics = sourceFile . getPreEmitDiagnostics ( ) ; for ( const diagnostic of diagnostics ) { if ( diagnostic . getCategory ( ) === DiagnosticCategory . Error ) { log ( diagnostic . getMessageText ( ) . toString ( ) ) ; } else { log ( `non-error diagnostic found: ${ diagnostic . getMessageText ( ) . toString ( ) }` ) ; } } project . removeSourceFile ( sourceFile ) ; if ( diagnostics . length > 0 ) { return BreakingIncrement . major ; } return BreakingIncrement . none ; } function checkMinorIncrement ( project , pkgDir , oldTypeData , newTypeData ) { let testFile = "" ; if ( oldTypeData . classData !== undefined && newTypeData . classData !== undefined ) { testFile = buildClassTestFileMinor ( `old${ getFullTypeName ( oldTypeData ) }` , oldTypeData . classData , `new${ getFullTypeName ( newTypeData ) }` , newTypeData . classData ) ; log ( testFile ) ; } const sourcePath = `${ pkgDir }/src/test/typeValidation.spec.ts` ; const sourceFile = project . createSourceFile ( sourcePath , testFile ) ; const diagnostics = sourceFile . getPreEmitDiagnostics ( ) ; for ( const diagnostic of diagnostics ) { if ( diagnostic . getCategory ( ) === DiagnosticCategory . Error ) { log ( diagnostic . getMessageText ( ) . toString ( ) ) ; } else { log ( `non-error diagnostic found: ${ diagnostic . getMessageText ( ) . toString ( ) }` ) ; } } project . removeSourceFile ( sourceFile ) ; if ( diagnostics . length > 0 ) { return BreakingIncrement . minor ; } return BreakingIncrement . none ; } function buildClassTestFileMajor ( oldClassName , oldClassData , newClassName , newClassData ) { const fileLines = [ ] ; const requiredGenerics = new GenericsInfo ( oldClassData . requiredGenerics ) ; requiredGenerics . merge ( newClassData . requiredGenerics ) ; for ( const [ generic , paramCount ] of requiredGenerics ) { const numberArray = Array . from ( Array ( paramCount ) . keys ( ) ) ; const typeParams = numberArray . map ( ( n ) => `T${ n } = any` ) . join ( ", " ) ; const typedProperties = numberArray . map ( ( n ) => `myVar${ n }: T${ n };` ) . join ( "\n" ) ; fileLines . push ( `interface ${ generic }<${ typeParams }> {` ) ; fileLines . push ( typedProperties ) ; fileLines . push ( `};` ) ; } let oldTypeParameters = oldClassData . typeParameters . join ( ", " ) ; oldTypeParameters = oldTypeParameters === "" ? oldTypeParameters : `<${ oldTypeParameters }>` ; fileLines . push ( `declare class ${ oldClassName }${ oldTypeParameters } {` ) ; fileLines . push ( ... oldClassData . properties ) ; fileLines . push ( "}" ) ; let newTypeParameters = newClassData . typeParameters . join ( ", " ) ; newTypeParameters = newTypeParameters === "" ? newTypeParameters : `<${ newTypeParameters }>` ; fileLines . push ( `declare class ${ newClassName }${ newTypeParameters } {` ) ; fileLines . push ( ... newClassData . properties ) ; fileLines . push ( "}" ) ; const oldTypeArgs = oldClassData . typeParameters . map ( ( ) => "any" ) . join ( ", " ) ; const oldClassType = oldTypeArgs === "" ? oldClassName : `${ oldClassName }<${ oldTypeArgs }>` ; const newTypeArgs = newClassData . typeParameters . map ( ( ) => "any" ) . join ( ", " ) ; const newClassType = newTypeArgs === "" ? newClassName : `${ newClassName }<${ newTypeArgs }>` ; fileLines . push ( `const oldToNew: ${ newClassType } = undefined as any as ${ oldClassType }` ) ; fileLines . push ( `const newToOld: ${ oldClassType } = undefined as any as ${ newClassType }` ) ; const declaration = fileLines . join ( "\n" ) ; return declaration ; } function buildClassTestFileMinor ( oldClassName , oldClassData , newClassName , newClassData ) { const fileLines = [ ] ; fileLines . push ( `type Equals<X, Y> = (<T>() => (T extends X ? 1 : 2)) extends` ) ; fileLines . push ( `    (<T>() => (T extends Y ? 1 : 2)) ? true : false;` ) ; fileLines . push ( `let trueVal: true = true;` ) ; const requiredGenerics = new GenericsInfo ( oldClassData . requiredGenerics ) ; requiredGenerics . merge ( newClassData . requiredGenerics ) ; for ( const [ generic , paramCount ] of requiredGenerics ) { const numberArray = Array . from ( Array ( paramCount ) . keys ( ) ) ; const typeParams = numberArray . map ( ( n ) => `T${ n } = any` ) . join ( ", " ) ; const typedProperties = numberArray . map ( ( n ) => `myVar${ n }: T${ n };` ) . join ( "\n" ) ; fileLines . push ( `interface ${ generic }<${ typeParams }> {` ) ; fileLines . push ( typedProperties ) ; fileLines . push ( `};` ) ; } let oldTypeParameters = oldClassData . typeParameters . join ( ", " ) ; oldTypeParameters = oldTypeParameters === "" ? oldTypeParameters : `<${ oldTypeParameters }>` ; fileLines . push ( `declare class ${ oldClassName }${ oldTypeParameters } {` ) ; fileLines . push ( ... oldClassData . properties ) ; fileLines . push ( "}" ) ; let newTypeParameters = newClassData . typeParameters . join ( ", " ) ; newTypeParameters = newTypeParameters === "" ? newTypeParameters : `<${ newTypeParameters }>` ; fileLines . push ( `declare class ${ newClassName }${ newTypeParameters } {` ) ; fileLines . push ( ... newClassData . properties ) ; fileLines . push ( "}" ) ; const oldTypeArgs = oldClassData . typeParameters . map ( ( ) => "any" ) . join ( ", " ) ; const oldClassType = oldTypeArgs === "" ? oldClassName : `${ oldClassName }<${ oldTypeArgs }>` ; const newTypeArgs = newClassData . typeParameters . map ( ( ) => "any" ) . join ( ", " ) ; const newClassType = newTypeArgs === "" ? newClassName : `${ newClassName }<${ newTypeArgs }>` ; fileLines . push ( `trueVal = undefined as any as Equals<${ newClassType }, ${ oldClassType }>;` ) ; const declaration = fileLines . join ( "\n" ) ; return declaration ; }