packages/awslint/lib/linter.ts (207 lines of code) (raw):
import * as util from 'util';
import { PrimitiveType } from '@jsii/spec';
import * as reflect from 'jsii-reflect';
import { RuleFilterSet } from './rule-specs';
export interface LinterOptions {
/**
* List of rules to include.
* @default all rules
*/
includeRules?: RuleFilterSet;
/**
* List of rules to exclude (takes precedence on "include")
* @default none
*/
excludeRules?: RuleFilterSet;
}
export abstract class LinterBase {
public abstract rules: Rule[];
public abstract eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Diagnostic[];
}
export class AggregateLinter extends LinterBase {
private linters: LinterBase[];
constructor(...linters: LinterBase[]) {
super();
this.linters = linters;
}
public get rules(): Rule[] {
const ret = new Array<Rule>();
for (const linter of this.linters) {
ret.push(...linter.rules);
}
return ret;
}
public eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Diagnostic[] {
const diags = new Array<Diagnostic>();
for (const linter of this.linters) {
diags.push(...linter.eval(assembly, options));
}
return diags;
}
}
/**
* Evaluates a bunch of rules against some context.
*/
export class Linter<T> extends LinterBase {
private readonly _rules: { [name: string]: ConcreteRule<T> } = { };
constructor(private readonly init: (assembly: reflect.Assembly) => T | readonly T[] | undefined) {
super();
}
public get rules() {
return Object.values(this._rules);
}
/**
* Install another rule.
*/
public add(rule: ConcreteRule<T>) {
if (rule.code in this._rules) {
throw new Error(`rule "${rule.code}" already exists`);
}
this._rules[rule.code] = rule;
}
/**
* Evaluate all rules against the context.
*/
public eval(assembly: reflect.Assembly, options: LinterOptions | undefined): Diagnostic[] {
options = options || { };
let ctxs = this.init(assembly);
if (!ctxs) {
return []; // skip
}
if (!Array.isArray(ctxs)) {
ctxs = [ctxs] as readonly T[];
}
const diag = new Array<Diagnostic>();
for (const ctx of ctxs) {
for (const rule of Object.values(this._rules)) {
const evaluation = new Evaluation(ctx, rule, diag, options);
rule.eval(evaluation);
}
}
return diag;
}
}
/**
* Passed in to each rule during evaluation.
*/
export class Evaluation<T> {
public readonly ctx: T;
public readonly options: LinterOptions;
private readonly curr: ConcreteRule<T>;
private readonly diagnostics: Diagnostic[];
constructor(ctx: T, rule: ConcreteRule<T>, diagnostics: Diagnostic[], options: LinterOptions) {
this.ctx = ctx;
this.options = options;
this.curr = rule;
this.diagnostics = diagnostics;
}
/**
* Record a failure if `condition` is not truthy.
*
* @param condition The condition to assert.
* @param scope Used to diagnose the location in the source, and is used in the
* ignore pattern.
* @param extra Used to replace %s in the default message format string.
*/
public assert(condition: any, scope: string, extra?: string): condition is true {
// deduplicate: skip if this specific assertion ("rule:scope") was already examined
if (this.diagnostics.find(d => d.rule.code === this.curr.code && d.scope === scope)) {
return condition;
}
const include = this.shouldEvaluate(this.curr.code, scope);
const message = util.format(this.curr.message, extra || '');
// Don't add a "Success" diagnostic. It will break if we run a compound
// linter rule which consists of 3 checks with the same scope (such
// as for example `assertSignature()`). If the first check fails, we would
// add a "Success" diagnostic and all other diagnostics would be skipped because
// of the deduplication check above. Changing the scope makes it worse, since
// the scope is also the ignore pattern and they're all conceptually the same rule.
//
// Simplest solution is to not record successes -- why do we even need them?
if (include && condition) { return condition; }
let level: DiagnosticLevel;
if (!include) {
level = DiagnosticLevel.Skipped;
} else if (this.curr.warning) {
level = DiagnosticLevel.Warning;
} else {
level = DiagnosticLevel.Error;
}
const diag: Diagnostic = {
level,
rule: this.curr,
scope,
message,
};
this.diagnostics.push(diag);
return condition;
}
public assertEquals(actual: any, expected: any, scope: string) {
return this.assert(actual === expected, scope, ` (expected="${expected}",actual="${actual}")`);
}
public assertTypesEqual(ts: reflect.TypeSystem, actual: TypeSpecifier, expected: TypeSpecifier, scope: string) {
const a = typeReferenceFrom(ts, actual);
const e = typeReferenceFrom(ts, expected);
return this.assert(a.toString() === e.toString(), scope, ` (expected="${e}",actual="${a}")`);
}
public assertTypesAssignable(ts: reflect.TypeSystem, actual: TypeSpecifier, expected: TypeSpecifier, scope: string) {
const a = typeReferenceFrom(ts, actual);
const e = typeReferenceFrom(ts, expected);
return this.assert(a.toString() === e.toString() || (a.fqn && e.fqn && a.type!.extends(e.type!)), scope, ` ("${a}" not assignable to "${e}")`);
}
public assertParameterOptional(actual: boolean, expected: boolean, scope: string) {
return this.assert(actual === expected, scope, ` (${scope} should be ${expected ? 'optional' : 'mandatory'})`);
}
public assertSignature(method: reflect.Callable, expectations: MethodSignatureExpectations) {
const scope = method.parentType.fqn + '.' + method.name;
if (expectations.returns && reflect.Method.isMethod(method)) {
this.assertTypesEqual(method.system, method.returns.type, expectations.returns, scope);
}
if (expectations.parameters) {
const expectedCount = expectations.parameters.length;
const actualCount = method.parameters.length;
if (this.assertEquals(actualCount, expectedCount, scope)) {
for (let i = 0; i < expectations.parameters.length; ++i) {
const expect = expectations.parameters[i];
const actual = method.parameters[i];
const pscope = scope + `.params[${i}]`;
if (expect.name) {
const expectedName = expect.name;
const actualName = actual.name;
this.assertEquals(actualName, expectedName, pscope);
}
if (expect.type) {
if (expect.subtypeAllowed) {
this.assertTypesAssignable(method.system, actual.type, expect.type, pscope);
} else {
this.assertTypesEqual(method.system, actual.type, expect.type, pscope);
}
}
if (expect.optional !== undefined) {
this.assertParameterOptional(actual.optional, expect.optional, pscope);
}
}
}
}
}
/**
* Evaluates whether the rule should be evaluated based on the filters applied.
*/
private shouldEvaluate(code: string, scope: string) {
if (!this.options.includeRules || this.options.includeRules.isEmpty()) {
return true;
}
if (this.options.includeRules.matches(code, scope)) {
if (this.options.excludeRules?.matches(code, scope)) {
return false;
}
return true;
}
return false;
}
}
export interface Rule {
code: string,
message: string;
warning?: boolean;
}
export interface ConcreteRule<T> extends Rule {
eval(linter: Evaluation<T>): void;
}
/**
* A type constraint
*
* Be super flexible about how types can be represented. Ultimately, we will
* compare what you give to a TypeReference, because that's what's in the JSII
* Reflect model. However, if you already have a real Type, or just a string to
* a user-defined type, that's fine too. We'll Do The Right Thing.
*/
export type TypeSpecifier = reflect.TypeReference | reflect.Type | string;
export interface MethodSignatureParameterExpectation {
name?: string;
type?: TypeSpecifier;
subtypeAllowed?: boolean;
/** should this param be optional? */
optional?: boolean;
}
export interface MethodSignatureExpectations {
parameters?: MethodSignatureParameterExpectation[];
returns?: TypeSpecifier;
}
export enum DiagnosticLevel {
Skipped,
Success,
Warning,
Error,
}
export interface Diagnostic {
level: DiagnosticLevel;
rule: Rule;
scope: string;
message: string;
}
/**
* Convert a type specifier to a TypeReference
*/
function typeReferenceFrom(ts: reflect.TypeSystem, x: TypeSpecifier): reflect.TypeReference {
if (isTypeReference(x)) { return x; }
if (typeof x === 'string') {
if (x.indexOf('.') === -1) {
return new reflect.TypeReference(ts, { primitive: x as PrimitiveType });
} else {
return new reflect.TypeReference(ts, { fqn: x });
}
}
return new reflect.TypeReference(ts, x);
}
function isTypeReference(x: any): x is reflect.TypeReference {
return x instanceof reflect.TypeReference;
}