Last active
July 2, 2019 09:36
-
-
Save spion/b89d1d2958f3d3142b2fe64fea5e4c32 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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