import * as camelcase from 'camelcase' ; import * as reflect from 'jsii-reflect' ; import { Linter } from '../linter' ; import { CfnResourceReflection } from './cfn-resource' ; import { ConstructReflection } from './construct' ; import { CoreTypes } from './core-types' ; import { getDocTag } from './util' ; const GRANT_RESULT_FQN = '@aws-cdk/aws-iam.Grant' ; export const resourceLinter = new Linter ( a => ResourceReflection . findAll ( a ) ) ; export interface Attribute { site : AttributeSite ; property : reflect . Property ; cfnAttributeNames : string [ ] ; } export enum AttributeSite { Interface = 'interface' , Class = 'class' } export class ResourceReflection { returns public static findAll ( assembly ) { if ( CoreTypes . hasCoreModule ( assembly ) ) { return [ ] ; } return ConstructReflection . findAllConstructs ( assembly ) . filter ( c => CoreTypes . isResourceClass ( c . classType ) ) . map ( c => new ResourceReflection ( c ) ) ; } public readonly attributes : Attribute [ ] ; public readonly fqn : string ; public readonly assembly : reflect . Assembly ; public readonly sys : reflect . TypeSystem ; public readonly cfn : CfnResourceReflection ; public readonly basename : string ; public readonly core : CoreTypes ; public readonly physicalNameProp ? : reflect . Property ; constructor ( public readonly construct ) { this . assembly = construct . classType . assembly ; this . sys = this . assembly . system ; const cfn = tryResolveCfnResource ( construct . classType ) ; if ( ! cfn ) { throw new Error ( `Cannot find L1 class for L2 ${ construct . fqn }. ` + `Is "${ guessResourceName ( construct . fqn ) }" an actual CloudFormation resource. ` + 'If not, use the "@resource" doc tag to indicate the full resource name (e.g. "@resource AWS::Route53::HostedZone")' ) ; } this . core = new CoreTypes ( this . sys ) ; this . cfn = cfn ; this . basename = construct . classType . name ; this . fqn = construct . fqn ; this . attributes = this . findAttributeProperties ( ) ; this . physicalNameProp = this . findPhysicalNameProp ( ) ; } private findPhysicalNameProp ( ) { if ( ! this . construct . propsType ) { return undefined ; } const resourceName = camelcase ( this . cfn . basename ) ; const physicalNameProp = resourceName . endsWith ( 'Name' ) ? resourceName : `${ resourceName }Name` ; return this . construct . propsType . allProperties . find ( x => x . name === physicalNameProp ) ; } /**
     * Attribute properties are all the properties that begin with the type name (e.g. bucketXxx).
     */ private findAttributeProperties ( ) { const result = new Array < Attribute > ( ) ; for ( const p of this . construct . classType . allProperties ) { if ( p . protected ) { continue ; } const basename = camelcase ( this . cfn . basename ) ; const tag = getDocTag ( p , 'attribute' ) ; if ( ! p . name . startsWith ( basename ) && ! tag ) { continue ; } let cfnAttributeNames ; if ( tag && tag !== 'true' ) { cfnAttributeNames = tag . split ( ',' ) ; } else { const name = camelcase ( p . name , { pascalCase : true } ) ; if ( this . cfn . attributeNames . includes ( name ) ) { cfnAttributeNames = [ name ] ; } else if ( p . name . startsWith ( basename ) ) { cfnAttributeNames = [ name . substring ( this . cfn . basename . length ) ] ; } else { continue ; } } const property = findDeclarationSite ( p ) ; const site = property . parentType . isInterfaceType ( ) ? AttributeSite . Interface : AttributeSite . Class ; result . push ( { site , cfnAttributeNames , property , } ) ; } return result ; } } function findDeclarationSite ( prop ) { if ( ! prop . overrides || ( ! prop . overrides . isClassType ( ) && ! prop . overrides . isInterfaceType ( ) ) ) { if ( ! prop . parentType . isClassType ( ) && ! prop . parentType . isInterfaceType ( ) ) { throw new Error ( 'invalid parent type' ) ; } return prop ; } const overridesProp = prop . overrides . allProperties . find ( p => p . name === prop . name ) ; if ( ! overridesProp ) { throw new Error ( `Cannot find property ${ prop . name } in override site ${ prop . overrides . fqn }` ) ; } return findDeclarationSite ( overridesProp ) ; } resourceLinter . add ( { code : 'resource-class-extends-resource' , message : 'resource classes must extend "cdk.Resource" directly or indirectly' , eval : e => { const resourceBase = e . ctx . sys . findClass ( e . ctx . core . resourceClass . fqn ) ; e . assert ( e . ctx . construct . classType . extends ( resourceBase ) , e . ctx . construct . fqn ) ; } , } ) ; resourceLinter . add ( { code : 'resource-interface' , warning : true , message : 'every resource must have a resource interface' , eval : e => { e . assert ( e . ctx . construct . interfaceType , e . ctx . construct . fqn ) ; } , } ) ; resourceLinter . add ( { code : 'resource-interface-extends-resource' , message : 'construct interfaces of AWS resources must extend cdk.IResource' , eval : e => { const resourceInterface = e . ctx . construct . interfaceType ; if ( ! resourceInterface ) { return ; } const resourceInterfaceFqn = e . ctx . core . resourceInterface . fqn ; const interfaceBase = e . ctx . sys . findInterface ( resourceInterfaceFqn ) ; e . assert ( resourceInterface . extends ( interfaceBase ) , resourceInterface . fqn ) ; } , } ) ; resourceLinter . add ( { code : 'resource-attribute' , message : 'resources must represent all cloudformation attributes as attribute properties. ' + '"@attribute ATTR[,ATTR]" can be used to tag non-standard attribute names. ' + 'missing property:' , eval : e => { for ( const name of e . ctx . cfn . attributeNames ) { const expected = camelcase ( name ) . startsWith ( camelcase ( e . ctx . cfn . basename ) ) ? camelcase ( name ) : camelcase ( e . ctx . cfn . basename + name ) ; const found = e . ctx . attributes . find ( a => a . cfnAttributeNames . includes ( name ) ) ; e . assert ( found , `${ e . ctx . fqn }.${ expected }` , expected ) ; } } , } ) ; resourceLinter . add ( { code : 'grant-result' , message : `"grant" method must return ${ GRANT_RESULT_FQN }` , eval : e => { const grantResultType = e . ctx . sys . tryFindFqn ( GRANT_RESULT_FQN ) ; if ( ! grantResultType ) { return ; } const grantMethods = e . ctx . construct . classType . allMethods . filter ( m => m . name . startsWith ( 'grant' ) ) ; for ( const grantMethod of grantMethods ) { e . assertSignature ( grantMethod , { returns : grantResultType , } ) ; } } , } ) ; resourceLinter . add ( { code : 'props-physical-name' , message : 'Every Resource must have a single physical name construction property, ' + 'with a name that is an ending substring of <cfnResource>Name' , eval : e => { if ( ! e . ctx . construct . propsType ) { return ; } e . assert ( e . ctx . physicalNameProp , e . ctx . construct . propsFqn ) ; } , } ) ; resourceLinter . add ( { code : 'props-physical-name-type' , message : 'The type of the physical name prop should always be a "string"' , eval : e => { if ( ! e . ctx . physicalNameProp ) { return ; } const prop = e . ctx . physicalNameProp ; e . assertTypesEqual ( e . ctx . sys , prop . type , 'string' , `${ e . ctx . construct . propsFqn }.${ prop . name }` ) ; } , } ) ; function tryResolveCfnResource ( resourceClass ) { const sys = resourceClass . system ; const tag = resourceClass . docs . customTag ( 'resource' ) ; if ( tag ) { return CfnResourceReflection . findByName ( sys , tag ) ; } const guess = guessResourceName ( resourceClass . fqn ) ; if ( guess ) { const cfn = CfnResourceReflection . findByName ( sys , guess ) ; if ( cfn ) { return cfn ; } } for ( const base of resourceClass . getAncestors ( ) ) { const ret = tryResolveCfnResource ( base ) ; if ( ret ) { return ret ; } } return undefined ; } function guessResourceName ( fqn ) { const match = /@aws-cdk\/([a-z]+)-([a-z0-9]+)\.([A-Z][a-zA-Z0-9]+)/ . exec ( fqn ) ; if ( ! match ) { return undefined ; } const [ , org , ns , rs ] = match ; if ( ! org || ! ns || ! rs ) { return undefined ; } return `${ org }::${ ns }::${ rs }` ; }