packages/awslint/lib/rules/cfn-resource.ts (72 lines of code) (raw):

import * as reflect from 'jsii-reflect'; import { CoreTypes } from './core-types'; import { ResourceReflection } from './resource'; import { pascalize } from '../case'; import { Linter } from '../linter'; const cfnResourceTagName = 'cloudformationResource'; // this linter verifies that we have L2 coverage. it finds all "Cfn" classes and verifies // that we have a corresponding L1 class for it that's identified as a resource. export const cfnResourceLinter = new Linter(a => CfnResourceReflection.findAll(a)); // Cache L1 constructs per type system. const l1ConstructCache = new Map<reflect.TypeSystem, Map<string, reflect.ClassType>>(); function cacheL1ConstructsForTypeSystem(sys: reflect.TypeSystem) { if (!l1ConstructCache.has(sys)) { l1ConstructCache.set(sys, new Map<string, reflect.ClassType>()); for (const cls of sys.classes) { const cfnResourceTag = cls.docs.customTag(cfnResourceTagName); if (cfnResourceTag) { l1ConstructCache.get(sys)?.set(cfnResourceTag?.toLocaleLowerCase()!, cls); } } } } cfnResourceLinter.add({ code: 'resource-class', message: 'every resource must have a resource class (L2), add \'@resource %s\' to its docstring', warning: true, eval: e => { const l2 = ResourceReflection.findAll(e.ctx.classType.assembly).find(r => r.cfn.fullname === e.ctx.fullname); e.assert(l2, e.ctx.fullname, e.ctx.fullname); }, }); export class CfnResourceReflection { /** * Finds a Cfn resource class by full CloudFormation resource name (e.g. `AWS::S3::Bucket`) * @param fullName first two components are case-insensitive (e.g. `aws::s3::Bucket` is equivalent to `Aws::S3::Bucket`) */ public static findByName(sys: reflect.TypeSystem, fullName: string) { if (!l1ConstructCache.has(sys)) { cacheL1ConstructsForTypeSystem(sys); } const cls = l1ConstructCache.get(sys)?.get(fullName.toLowerCase()); if (cls) { return new CfnResourceReflection(cls); } return undefined; } /** * Returns all CFN resource classes within an assembly. */ public static findAll(assembly: reflect.Assembly) { return assembly.allClasses .filter(c => CoreTypes.isCfnResource(c)) .map(c => new CfnResourceReflection(c)); } public readonly classType: reflect.ClassType; public readonly fullname: string; // AWS::S3::Bucket public readonly namespace: string; // AWS::S3 public readonly basename: string; // Bucket public readonly attributeNames: string[]; // (normalized) bucketArn, bucketName, queueUrl public readonly doc: string; // link to CloudFormation docs constructor(cls: reflect.ClassType) { this.classType = cls; this.basename = cls.name.slice('Cfn'.length); const fullname = cls.docs.customTag('cloudformationResource'); if (!fullname) { throw new Error(`Unable to extract CloudFormation resource name from initializer documentation of ${cls}`); } this.fullname = fullname; this.namespace = fullname.split('::').slice(0, 2).join('::'); this.attributeNames = cls.ownProperties .filter(p => (p.docs.docs.custom || {}).cloudformationAttribute) .map(p => p.docs.customTag('cloudformationAttribute') || '<error>') .map(p => this.attributePropertyNameFromCfnName(p)); this.doc = cls.docs.docs.see || ''; } private attributePropertyNameFromCfnName(name: string): string { // special case (someone was smart), special case copied from spec2cdk if (this.basename === 'SecurityGroup' && name === 'GroupId') { return 'Id'; } return pascalize(name); } }