dry-ts
Version:
Find candidate duplicate TypeScript code by comparing normalized AST structure.
106 lines (105 loc) • 3.63 kB
JavaScript
import ts from "typescript";
import { NormalizedNode } from "./NormalizedNode.js";
export class TypeScriptNormalizer {
normalize(node, memo) {
if (memo) {
const cached = memo.get(node);
if (cached) {
return cached;
}
}
const children = [];
for (const marker of this.markers(node)) {
children.push(new NormalizedNode(marker, []));
}
node.forEachChild((child) => {
if (this.keepsStructuralChild(child)) {
children.push(this.normalize(child, memo));
}
});
const result = new NormalizedNode(this.tag(node), children);
if (memo) {
memo.set(node, result);
}
return result;
}
tag(node) {
return ts.SyntaxKind[node.kind] ?? `SyntaxKind${node.kind}`;
}
keepsStructuralChild(child) {
return !this.isName(child) && !this.isLiteral(child) && child.kind !== ts.SyntaxKind.JSDocComment;
}
isName(node) {
return ts.isIdentifier(node) || node.kind === ts.SyntaxKind.PrivateIdentifier;
}
isLiteral(node) {
return (ts.isStringLiteral(node) ||
ts.isNumericLiteral(node) ||
ts.isNoSubstitutionTemplateLiteral(node) ||
ts.isRegularExpressionLiteral(node) ||
node.kind === ts.SyntaxKind.BigIntLiteral ||
node.kind === ts.SyntaxKind.TrueKeyword ||
node.kind === ts.SyntaxKind.FalseKeyword ||
node.kind === ts.SyntaxKind.NullKeyword);
}
markers(node) {
const markers = [];
this.addDecoratorMarkers(node, markers);
this.addModifierMarkers(node, markers);
this.addOperatorMarkers(node, markers);
this.addTypeScriptShapeMarkers(node, markers);
markers.sort();
return markers;
}
addDecoratorMarkers(node, markers) {
if (ts.canHaveDecorators(node)) {
for (const _decorator of ts.getDecorators(node) ?? []) {
markers.push("decorator");
}
}
}
addModifierMarkers(node, markers) {
if (!ts.canHaveModifiers(node)) {
return;
}
for (const modifier of ts.getModifiers(node) ?? []) {
markers.push(`modifier:${this.tag(modifier)}`);
}
}
addOperatorMarkers(node, markers) {
if (ts.isBinaryExpression(node)) {
markers.push(`operator:${this.tag(node.operatorToken)}`);
}
else if (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) {
markers.push(`operator:${ts.SyntaxKind[node.operator]}`);
}
}
addTypeScriptShapeMarkers(node, markers) {
if (ts.isVariableDeclarationList(node)) {
if ((node.flags & ts.NodeFlags.Const) !== 0) {
markers.push("variable:const");
}
else if ((node.flags & ts.NodeFlags.Let) !== 0) {
markers.push("variable:let");
}
else {
markers.push("variable:var");
}
}
if (ts.isHeritageClause(node)) {
markers.push(`heritage:${ts.SyntaxKind[node.token]}`);
}
if (hasQuestionToken(node)) {
markers.push("optional");
}
if (hasExclamationToken(node)) {
markers.push("definite");
}
}
}
function hasQuestionToken(node) {
return "questionToken" in node && Boolean(node.questionToken);
}
function hasExclamationToken(node) {
return "exclamationToken" in node && Boolean(node.exclamationToken);
}