packages/awslint/lib/rules/construct.ts (227 lines of code) (raw):

import * as reflect from 'jsii-reflect'; import { CoreTypes } from './core-types'; import { Linter, MethodSignatureParameterExpectation } from '../linter'; export const constructLinter = new Linter<ConstructReflection>(assembly => assembly.allClasses .filter(t => CoreTypes.isConstructClass(t)) .map(construct => new ConstructReflection(construct))); export class ConstructReflection { public static findAllConstructs(assembly: reflect.Assembly) { return assembly.allClasses .filter(c => CoreTypes.isConstructClass(c)) .map(c => new ConstructReflection(c)); } public static getFqnFromTypeRef(typeRef: reflect.TypeReference) { if (typeRef.arrayOfType) { return typeRef.arrayOfType.fqn; } else if (typeRef.mapOfType) { return typeRef.mapOfType.fqn; } return typeRef.fqn; } public readonly fqn: string; public readonly interfaceFqn: string; public readonly propsFqn: string; public readonly interfaceType?: reflect.InterfaceType; public readonly propsType?: reflect.InterfaceType; public readonly initializer?: reflect.Initializer; public readonly hasPropsArgument: boolean; public readonly sys: reflect.TypeSystem; public readonly core: CoreTypes; constructor(public readonly classType: reflect.ClassType) { this.fqn = classType.fqn; this.sys = classType.system; this.core = new CoreTypes(this.sys); this.interfaceFqn = `${this.typePrefix(classType)}.I${classType.name}`; this.propsFqn = `${this.typePrefix(classType)}.${classType.name}Props`; this.interfaceType = this.tryFindInterface(); this.propsType = this.tryFindProps(); this.initializer = classType.initializer; this.hasPropsArgument = this.initializer != null && this.initializer.parameters.length >= 3; } private typePrefix(classType: reflect.ClassType) { return classType.assembly.name + (classType.namespace ? `.${classType.namespace}` : ''); } private tryFindInterface() { const sys = this.classType.system; const found = sys.tryFindFqn(this.interfaceFqn); if (!found) { return undefined; } if (!found.isInterfaceType()) { throw new Error(`Expecting type ${this.interfaceFqn} to be an interface`); } return found; } private tryFindProps() { const found = this.sys.tryFindFqn(this.propsFqn); if (!found) { return undefined; } if (!found.isInterfaceType()) { throw new Error(`Expecting props struct ${this.propsFqn} to be an interface`); } return found; } } constructLinter.add({ code: 'construct-ctor', message: 'signature of all construct constructors should be "scope, id, props". ' + baseConstructAddendum(), eval: e => { // only applies to non abstract classes if (e.ctx.classType.abstract) { return; } const initializer = e.ctx.initializer; if (!e.assert(initializer, e.ctx.fqn)) { return; } const expectedParams = new Array<MethodSignatureParameterExpectation>(); expectedParams.push({ name: 'scope', type: e.ctx.core.baseConstructClassFqn, }); expectedParams.push({ name: 'id', type: 'string', }); // it's okay for a construct not to have a "props" argument so we only // assert the "props" argument if there are more than two parameters if (initializer.parameters.length > 2) { expectedParams.push({ name: 'props', }); } e.assertSignature(initializer, { parameters: expectedParams, }); }, }); constructLinter.add({ code: 'props-struct-name', message: 'all constructs must have a props struct', eval: e => { if (!e.ctx.hasPropsArgument) { return; } // abstract classes are exempt if (e.ctx.classType.abstract) { return; } e.assert(e.ctx.propsType, e.ctx.interfaceFqn); }, }); constructLinter.add({ code: 'construct-ctor-props-type', message: 'construct "props" type should use the props struct %s', eval: e => { if (!e.ctx.initializer) { return; } if (e.ctx.initializer.parameters.length < 3) { return; } e.assert( e.ctx.initializer.parameters[2].type.type === e.ctx.propsType, e.ctx.fqn, e.ctx.propsFqn); }, }); constructLinter.add({ code: 'construct-ctor-props-optional', message: 'construct "props" must be optional since all props are optional', eval: e => { if (!e.ctx.propsType) { return; } if (!e.ctx.initializer) { return; } if (!e.ctx.hasPropsArgument) { return; } // this rule applies only if all properties are optional const allOptional = e.ctx.propsType.allProperties.every(p => p.optional); if (!allOptional) { return; } e.assert(e.ctx.initializer.parameters[2].optional, e.ctx.fqn); }, }); constructLinter.add({ code: 'construct-interface-extends-iconstruct', message: 'construct interface must extend core.IConstruct', eval: e => { if (!e.ctx.interfaceType) { return; } const interfaceBase = e.ctx.sys.findInterface(e.ctx.core.baseConstructInterfaceFqn); e.assert(e.ctx.interfaceType.extends(interfaceBase), e.ctx.interfaceType.fqn); }, }); constructLinter.add({ code: 'construct-base-is-private', message: 'prefer that the construct base class is private', warning: true, eval: e => { if (!e.ctx.interfaceType) { return; } const baseFqn = `${e.ctx.classType.fqn}Base`; e.assert(!e.ctx.sys.tryFindFqn(baseFqn), baseFqn); }, }); constructLinter.add({ code: 'props-no-unions', message: 'props must not use TypeScript unions', eval: e => { if (!e.ctx.propsType) { return; } if (!e.ctx.hasPropsArgument) { return; } // this rule does not apply to L1 constructs if (CoreTypes.isCfnResource(e.ctx.classType)) { return; } for (const property of e.ctx.propsType.ownProperties) { e.assert(!property.type.unionOfTypes, `${e.ctx.propsFqn}.${property.name}`); } }, }); constructLinter.add({ code: 'props-no-arn-refs', message: 'props must use strong types instead of attributes. props should not have "arn" suffix', eval: e => { if (!e.ctx.propsType) { return; } if (!e.ctx.hasPropsArgument) { return; } // this rule does not apply to L1 constructs if (CoreTypes.isCfnResource(e.ctx.classType)) { return; } for (const property of e.ctx.propsType.ownProperties) { e.assert(!property.name.toLowerCase().endsWith('arn'), `${e.ctx.propsFqn}.${property.name}`); } }, }); constructLinter.add({ code: 'props-no-tokens', message: 'props must not use the "Token" type', eval: e => { if (!e.ctx.propsType) { return; } if (!e.ctx.hasPropsArgument) { return; } // this rule does not apply to L1 constructs if (CoreTypes.isCfnResource(e.ctx.classType)) { return; } for (const property of e.ctx.propsType.allProperties) { const fqn = ConstructReflection.getFqnFromTypeRef(property.type); const found = (fqn && e.ctx.sys.tryFindFqn(fqn)); if (found) { e.assert(!(fqn === e.ctx.core.tokenInterfaceFqn), `${e.ctx.propsFqn}.${property.name}`); } } }, }); constructLinter.add({ code: 'props-no-cfn-types', message: 'props must not expose L1 types (types which start with "Cfn")', eval: e => { if (!e.ctx.propsType) { return; } if (!e.ctx.hasPropsArgument) { return; } // this rule does not apply to L1 constructs if (CoreTypes.isCfnResource(e.ctx.classType)) { return; } for (const property of e.ctx.propsType.ownProperties) { const fqn = ConstructReflection.getFqnFromTypeRef(property.type); const found = (fqn && e.ctx.sys.tryFindFqn(fqn)); if (found) { e.assert(!found.name.toLowerCase().startsWith('cfn'), `${e.ctx.propsFqn}.${property.name}`); } } }, }); constructLinter.add({ code: 'props-no-any', message: 'props must not use Typescript "any" type', eval: e => { if (!e.ctx.propsType) { return; } if (!e.ctx.hasPropsArgument) { return; } // this rule does not apply to L1 constructs if (CoreTypes.isCfnResource(e.ctx.classType)) { return; } for (const property of e.ctx.propsType.ownProperties) { e.assert(!property.type.isAny, `${e.ctx.propsFqn}.${property.name}`); } }, }); function baseConstructAddendum(): string { if (!process.env.AWSLINT_BASE_CONSTRUCT) { return 'If the construct is using the "constructs" module, set the environment variable "AWSLINT_BASE_CONSTRUCT" and re-run'; } return ''; }