packages/awslint/lib/rules/api.ts (106 lines of code) (raw):
import * as reflect from 'jsii-reflect';
import { ConstructReflection } from './construct';
import { CoreTypes } from './core-types';
import { Linter } from '../linter';
const EXCLUDE_ANNOTATION_REF_VIA_INTERFACE = '[disable-awslint:ref-via-interface]';
// lint all constructs that are not L1 resources
export const apiLinter = new Linter(a => ConstructReflection
.findAllConstructs(a)
.filter(c => !CoreTypes.isCfnResource(c.classType)));
apiLinter.add({
code: 'ref-via-interface',
message: 'API should use interface and not the concrete class (%s). ' +
`If this is intentional, add "${EXCLUDE_ANNOTATION_REF_VIA_INTERFACE}" to element's jsdoc`,
eval: e => {
const cls = e.ctx.classType;
const visited = new Set<string>();
assertClass(cls);
function assertClass(type: reflect.ClassType): void {
if (visited.has(type.fqn)) { return; }
visited.add(type.fqn);
for (const method of type.allMethods) {
assertMethod(method);
}
if (type.initializer) {
assertMethod(type.initializer);
}
}
function assertDataType(type: reflect.InterfaceType): void {
for (const property of type.allProperties) {
assertProperty(property);
}
}
function assertInterface(type: reflect.InterfaceType): void {
if (visited.has(type.fqn)) { return; }
visited.add(type.fqn);
if (type.datatype) {
assertDataType(type);
}
for (const method of type.allMethods) {
assertMethod(method);
}
}
function assertProperty(property: reflect.Property) {
if (property.protected) {
return;
}
const site = property.overrides ? property.overrides : property.parentType;
assertType(property.type, property.docs, `${site.fqn}.${property.name}`);
}
function assertMethod(method: reflect.Callable) {
if (method.protected) {
return;
}
const site = method.overrides ? method.overrides : method.parentType;
const scope = `${site.fqn}.${method.name}`;
let firstMethod: reflect.Callable | undefined = site.isClassType() || site.isInterfaceType()
? site.allMethods.find(m => m.name === method.name)
: undefined;
if (!firstMethod) {
firstMethod = method;
}
for (const param of firstMethod.parameters) {
assertType(param.type, param.docs, `${scope}.${param.name}`);
}
// note that we do not require that return values will use an interface
}
function assertType(type: reflect.TypeReference, docs: reflect.Docs, scope: string): void {
if (type.primitive) {
return;
}
if (type.void) {
return;
}
if (type.arrayOfType) {
return assertType(type.arrayOfType, docs, scope);
}
if (type.mapOfType) {
return assertType(type.mapOfType, docs, scope);
}
if (type.unionOfTypes) {
for (const t of type.unionOfTypes) {
assertType(t, docs, scope);
}
return;
}
// interfaces are okay
if (type.type && type.type.isInterfaceType()) {
return assertInterface(type.type);
}
// enums are okay
if (type.type && type.type.isEnumType()) {
return;
}
// classes are okay as long as they are not resource constructs
if (type.type && type.type.isClassType()) {
if (!CoreTypes.isResourceClass(type.type)) {
return;
}
if (type.type.fqn === e.ctx.core.baseConstructClassFqn) {
return;
}
// allow exclusion of this rule
if (docs.summary.includes(EXCLUDE_ANNOTATION_REF_VIA_INTERFACE) || docs.remarks.includes(EXCLUDE_ANNOTATION_REF_VIA_INTERFACE)) {
return;
}
e.assert(false, scope, type.type.fqn);
return;
}
throw new Error(`invalid type reference: ${type.toString()}`);
}
},
});