Skip to content

Instantly share code, notes, and snippets.

@spion
Last active July 2, 2019 09:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save spion/b89d1d2958f3d3142b2fe64fea5e4c32 to your computer and use it in GitHub Desktop.
Save spion/b89d1d2958f3d3142b2fe64fea5e4c32 to your computer and use it in GitHub Desktop.
import * as Lint from 'tslint';
import * as ts from 'typescript';
import { inspect } from 'util';
/**
* This rule will ensure that methods marked with the "@expose" decorator must be declared in at
* least one interface implemented by the class. It will also ensure that the return type of this
* method has no excess properties compared to those specified in the interface
*/
export class Rule extends Lint.Rules.TypedRule {
public applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, new Set(), program.getTypeChecker());
}
}
function insp(o: any) {
return inspect(o, undefined, 0);
}
function walk(ctx: Lint.WalkContext<Set<string>>, tc: ts.TypeChecker) {
function typesAreEqualSerialized(tDeclared: ts.Type, tActual: ts.Type): boolean {
if (tDeclared === tActual) {
return true;
}
if (
tDeclared.symbol &&
tDeclared.symbol.name === 'Array' &&
tActual.symbol &&
tActual.symbol.name === 'Array'
) {
let arg1 =
tDeclared && (tDeclared as any).typeArguments && (tDeclared as any).typeArguments[0];
let arg2 = tActual && (tActual as any).typeArguments && (tActual as any).typeArguments[0];
// compare array type parameters
return typesAreEqualSerialized(arg1, arg2);
}
let actProps = tActual.getProperties();
for (let actProp of actProps) {
let declProp = tDeclared.getProperty(actProp.name);
if (!declProp) {
// Dont check methods, they're not serialized
if (
actProp.valueDeclaration.kind == ts.SyntaxKind.MethodSignature ||
actProp.valueDeclaration.kind === ts.SyntaxKind.MethodDeclaration
)
continue;
ctx.addFailureAtNode(
currentNode,
`Exposed method returns more properties than its interface: ${actProp.name}`
);
return false;
}
let declPropType = tc.getTypeAtLocation(declProp.valueDeclaration),
actPropType = tc.getTypeAtLocation(actProp.valueDeclaration);
if (!typesAreEqualSerialized(declPropType, actPropType)) return false;
}
// TODO: understand unions
return true;
}
let currentNode: ts.Node = null!;
return ts.forEachChild(ctx.sourceFile, cb);
function cb(anyNode: ts.Node) {
currentNode = anyNode;
// Methods only
if (anyNode.kind == ts.SyntaxKind.MethodDeclaration) {
let node = anyNode as ts.MethodDeclaration;
let actualRtPromise = tc.getReturnTypeOfSignature(tc.getSignatureFromDeclaration(node)!);
let actualReturn =
actualRtPromise &&
(actualRtPromise as any).typeArguments &&
(actualRtPromise as any).typeArguments[0];
// Ignore non exposed methods
if (!node.decorators || !node.decorators.some(d => d.getText().includes('@expose'))) return;
let p = node.parent;
if (p.kind !== ts.SyntaxKind.ClassDeclaration) return;
if (!p.heritageClauses || !p.heritageClauses.length) return;
let wasDeclared = false;
p.heritageClauses.forEach(hc => {
hc.types.forEach(child => {
let ifaceType = tc.getTypeFromTypeNode(child);
if (ifaceType.isClassOrInterface()) {
let iface = ifaceType as ts.InterfaceTypeWithDeclaredMembers;
let propLikeThis = ifaceType.getProperty(node.name.getFullText().trim());
if (!propLikeThis) return;
let t = tc.getTypeAtLocation(propLikeThis.declarations[0]);
let callSigs = t.getCallSignatures();
if (callSigs.length < 0) return;
let rt = callSigs[0].getReturnType();
let declReturn = rt && (rt as any).typeArguments && (rt as any).typeArguments[0];
wasDeclared = true;
typesAreEqualSerialized(declReturn, actualReturn);
}
});
});
if (!wasDeclared) {
ctx.addFailureAtNode(currentNode, 'Method is exposed but not declared in any interface');
}
} else {
ts.forEachChild(anyNode, cb);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment