packages/awslint/lib/rules/docs.ts (105 lines of code) (raw):
import { Stability } from '@jsii/spec';
import * as reflect from 'jsii-reflect';
import { CoreTypes } from './core-types';
import { Linter } from '../linter';
type DocsLinterContext = {
readonly assembly: reflect.Assembly;
readonly errorKey: string;
} & ({ readonly kind: 'type'; documentable: reflect.Type }
| { readonly kind: 'interface-property'; containingType: reflect.InterfaceType; documentable: reflect.Property }
| { readonly kind: 'class-property'; containingType: reflect.ClassType; documentable: reflect.Property }
| { readonly kind: 'method'; containingType: reflect.ReferenceType; documentable: reflect.Method }
| { readonly kind: 'enum-member'; containingType: reflect.EnumType; documentable: reflect.EnumMember }
);
export const docsLinter = new Linter<DocsLinterContext>(assembly => {
return [
...flatMap(assembly.allClasses, classType => [
{ assembly, kind: 'type', documentable: classType, errorKey: classType.fqn },
...classType.ownProperties.map(property => ({ assembly, kind: 'class-property', containingType: classType, documentable: property, errorKey: `${classType.fqn}.${property.name}` })),
...classType.ownMethods.map(method => ({ assembly, kind: 'method', containingType: classType, documentable: method, errorKey: `${classType.fqn}.${method.name}` })),
]),
...flatMap(assembly.allInterfaces, interfaceType => [
{ assembly, kind: 'type', documentable: interfaceType, errorKey: interfaceType.fqn },
...interfaceType.ownProperties.map(property => ({ assembly, kind: 'interface-property', containingType: interfaceType, documentable: property, errorKey: `${interfaceType.fqn}.${property.name}` })),
...interfaceType.ownMethods.map(method => ({ assembly, kind: 'method', containingType: interfaceType, documentable: method, errorKey: `${interfaceType.fqn}.${method.name}` })),
]),
...flatMap(assembly.allEnums, enumType => [
{ assembly, kind: 'type', documentable: enumType, errorKey: enumType.fqn },
...enumType.members.map(member => ({ assembly, kind: 'enum-member', containingType: enumType, documentable: member, errorKey: `${enumType.fqn}.${member.name}` })),
]),
] as DocsLinterContext[];
});
docsLinter.add({
code: 'docs-public-apis',
message: 'Public API element must have a docstring',
eval: e => {
if (!isPublic(e.ctx)) { return; }
// this rule does not apply to L1 constructs
if (isCfnType(e.ctx)) { return; }
if (!e.ctx.documentable.docs.summary) {
e.assert(e.ctx.documentable.docs.summary, e.ctx.errorKey);
}
},
});
docsLinter.add({
code: 'props-default-doc',
message: 'Optional property must have @default documentation',
eval: e => {
if (e.ctx.kind !== 'interface-property') { return; }
if (!e.ctx.containingType.isDataType()) { return; }
// this rule does not apply to L1 constructs
if (CoreTypes.isCfnType(e.ctx.containingType) || CoreTypes.isCfnNestedType(e.ctx.containingType)) {
return;
}
const property = e.ctx.documentable;
e.assert(!property.optional || property.docs.docs.default !== undefined, e.ctx.errorKey);
},
});
docsLinter.add({
code: 'props-no-undefined-default',
message: '\'@default undefined\' is not helpful. Users will know the VALUE is literally \'undefined\' if they don\'t specify it, but what is the BEHAVIOR if they do so?',
eval: e => {
if (e.ctx.kind !== 'interface-property') { return; }
if (!e.ctx.containingType.isDataType()) { return; }
const property = e.ctx.documentable;
e.assert(property.docs.docs.default !== 'undefined', e.ctx.errorKey);
},
});
docsLinter.add({
code: 'no-experimental-apis',
message: 'The use of @experimental in not allowed',
eval: e => {
if (!isPublic(e.ctx)) { return; }
// technically we should ban the use of @experimental in the codebase. Since jsii marks all symbols
// of experimental modules as experimental we can't.
if (isModuleExperimental(e.ctx.assembly)) {
return;
}
const sym = e.ctx.documentable;
e.assert(sym.docs.docs.stability !== Stability.Experimental, e.ctx.errorKey);
},
});
function isPublic(ctx: DocsLinterContext) {
switch (ctx.kind) {
case 'class-property':
case 'interface-property':
case 'method':
return !ctx.documentable.protected;
case 'enum-member':
case 'type':
return true;
}
}
function isCfnType(ctx: DocsLinterContext) {
switch (ctx.kind) {
case 'class-property':
case 'interface-property':
case 'method':
case 'enum-member':
return CoreTypes.isCfnType(ctx.containingType);
case 'type':
return CoreTypes.isCfnType(ctx.documentable);
}
}
function isModuleExperimental(assembly: reflect.Assembly) {
return assembly.spec.docs?.stability === Stability.Experimental;
}
function flatMap<T, U>(array: readonly T[], callbackfn: (value: T, index: number, array: readonly T[]) => U[]): U[] {
return Array.prototype.concat(...array.map(callbackfn));
}