/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ts from 'typescript';
import * as path from 'path';
import {
Comment,
emptyContainer,
emptySpace,
Expression,
J,
NameTree,
Statement,
TextComment,
TrailingComma,
Type,
TypeTree,
VariableDeclarator,
} from '../java';
import {DelegatedYield, FunctionDeclaration, Generator, JS, JSX, NonNullAssertion, Optional} from '.';
import {emptyMarkers, markers, Markers, MarkersKind, ParseExceptionResult, replaceMarkerByKind} from "../markers";
import {NamedStyles} from "../style";
import {Parser, ParserInput, parserInputFile, parserInputRead, ParserOptions, Parsers, SourcePath} from "../parser";
import {randomId} from "../uuid";
import {SourceFile} from "../tree";
import {
binarySearch,
checkSyntaxErrors,
compareTextSpans,
getNextSibling,
getPreviousSibling,
hasFlowAnnotation,
isStatement,
TextSpan
} from "./parser-utils";
import {JavaScriptTypeMapping} from "./type-mapping";
import {create as produce} from "mutative";
import ComputedPropertyName = JS.ComputedPropertyName;
import Attribute = JSX.Attribute;
import SpreadAttribute = JSX.SpreadAttribute;
export interface JavaScriptParserOptions extends ParserOptions {
styles?: NamedStyles[],
sourceFileCache?: Map,
}
function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
const dotIndex = fileName.lastIndexOf('.');
if (dotIndex === -1) return ts.ScriptKind.TS;
const ext = fileName.slice(dotIndex);
switch (ext) {
case '.tsx': return ts.ScriptKind.TSX;
case '.jsx': return ts.ScriptKind.JSX;
case '.ts':
case '.mts':
case '.cts': return ts.ScriptKind.TS;
case '.js':
case '.mjs':
case '.cjs': return ts.ScriptKind.JS;
default: return ts.ScriptKind.TS;
}
}
export class JavaScriptParser extends Parser {
private readonly compilerOptions: ts.CompilerOptions;
private readonly styles?: NamedStyles[];
private oldProgram?: ts.Program;
private readonly sourceFileCache?: Map;
constructor(
{
ctx,
relativeTo,
styles,
sourceFileCache,
}: JavaScriptParserOptions = {},
) {
super({ctx, relativeTo});
this.compilerOptions = {
target: ts.ScriptTarget.Latest,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.Node10,
noEmit: true,
allowJs: true,
checkJs: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
experimentalDecorators: true,
emitDecoratorMetadata: true,
forceConsistentCasingInFileNames: false,
jsx: ts.JsxEmit.Preserve,
baseUrl: relativeTo || process.cwd()
};
this.styles = styles;
this.sourceFileCache = sourceFileCache;
}
/**
* Parses a single source file using only ts.createSourceFile(), bypassing
* the TypeScript type checker entirely. This is significantly faster than
* the full parse() method when type information is not needed.
*
* Use this method for formatting-only operations where AST structure is
* needed but type attribution is not required.
*
* @param input The parser input containing the source code
* @returns The parsed SourceFile, or a ParseExceptionResult if parsing failed
*/
async parseOnly(input: ParserInput): Promise {
const filePath = parserInputFile(input);
const sourcePath = this.relativeTo && !path.isAbsolute(filePath)
? path.join(this.relativeTo, filePath)
: filePath;
const sourceText = parserInputRead(input);
const scriptKind = getScriptKindFromFileName(sourcePath);
// Create source file directly without a Program
const sourceFileOptions: ts.CreateSourceFileOptions = {
languageVersion: ts.ScriptTarget.Latest,
jsDocParsingMode: ts.JSDocParsingMode.ParseNone
};
const tsSourceFile = ts.createSourceFile(
sourcePath,
sourceText,
sourceFileOptions,
true, // setParentNodes
scriptKind
);
// Check for parse-time syntax errors (accessible via internal parseDiagnostics)
// TypeScript stores parse errors directly on the source file
const parseDiagnostics = (tsSourceFile as any).parseDiagnostics as ts.Diagnostic[] | undefined;
if (parseDiagnostics && parseDiagnostics.length > 0) {
const errors = parseDiagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
if (errors.length > 0) {
const errorMessages = errors.map(e => {
if (e.file && e.start !== undefined) {
const { line, character } = ts.getLineAndCharacterOfPosition(e.file, e.start);
const message = ts.flattenDiagnosticMessageText(e.messageText, "\n");
return `(${line + 1},${character + 1}): ${message} [${e.code}]`;
}
return `${ts.flattenDiagnosticMessageText(e.messageText, "\n")} [${e.code}]`;
}).join('; ');
return this.error(input, new SyntaxError(`Compiler error(s): ${errorMessages}`));
}
}
try {
// Parse without type mapping (no type checker available)
const result = new JavaScriptParserVisitor(tsSourceFile, this.relativePath(input), undefined)
.visit(tsSourceFile) as SourceFile;
if (this.styles) {
const styles = this.styles;
return produce(result, draft => {
draft.markers = styles.reduce((m, s) => replaceMarkerByKind(m, s), draft.markers);
});
}
return result;
} catch (error) {
return this.error(input, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error));
}
}
// noinspection JSUnusedGlobalSymbols
reset(): this {
this.sourceFileCache && this.sourceFileCache.clear();
this.oldProgram = undefined;
return this;
}
override async* parse(...inputs: ParserInput[]): AsyncGenerator {
const inputFiles = new Map();
// Populate inputFiles map and remove from cache if necessary
for (const input of inputs) {
let sourcePath = parserInputFile(input);
// If relativeTo is set and path is not absolute, make it absolute
if (this.relativeTo && !path.isAbsolute(sourcePath)) {
sourcePath = path.join(this.relativeTo, sourcePath);
}
const normalizedSourcePath = path.normalize(sourcePath);
inputFiles.set(normalizedSourcePath, input);
// Remove from cache if previously cached
this.sourceFileCache && this.sourceFileCache.delete(normalizedSourcePath);
}
// Create a new CompilerHost within parseInputs
const host = ts.createCompilerHost(this.compilerOptions);
// Set the current directory for module resolution
if (this.relativeTo) {
const originalGetCurrentDirectory = host.getCurrentDirectory;
host.getCurrentDirectory = () => this.relativeTo || originalGetCurrentDirectory();
}
// Override getSourceFile
host.getSourceFile = (fileName, languageVersion, onError) => {
const normalizedFileName = path.normalize(fileName);
// Check if the SourceFile is in the cache
let sourceFile = this.sourceFileCache && this.sourceFileCache.get(normalizedFileName);
if (sourceFile) {
return sourceFile;
}
// Read the file content
let sourceText: string | undefined;
// For input files
const input = inputFiles.get(normalizedFileName);
if (input) {
sourceText = parserInputRead(input);
} else {
// For dependency files
sourceText = ts.sys.readFile(normalizedFileName);
}
if (sourceText !== undefined) {
// Determine script kind based on file extension
const scriptKind = getScriptKindFromFileName(normalizedFileName);
// Build CreateSourceFileOptions with jsDocParsingMode
const sourceFileOptions: ts.CreateSourceFileOptions = typeof languageVersion === 'number'
? {
languageVersion: languageVersion,
jsDocParsingMode: ts.JSDocParsingMode.ParseNone // We override this as otherwise invalid JSDoc causes parse errors
}
: {
...languageVersion,
jsDocParsingMode: ts.JSDocParsingMode.ParseNone // We override this as otherwise invalid JSDoc causes parse errors
};
sourceFile = ts.createSourceFile(normalizedFileName, sourceText, sourceFileOptions, true, scriptKind);
// Cache the SourceFile if it's a dependency
if (!input && this.sourceFileCache) {
this.sourceFileCache.set(normalizedFileName, sourceFile);
}
return sourceFile;
}
if (onError) onError(`File not found: ${normalizedFileName}`);
return undefined;
};
// Override fileExists
host.fileExists = (fileName) => {
const normalizedFileName = path.normalize(fileName);
return inputFiles.has(normalizedFileName) || ts.sys.fileExists(normalizedFileName);
};
// Override readFile
host.readFile = (fileName) => {
const normalizedFileName = path.normalize(fileName);
const input = inputFiles.get(normalizedFileName);
return input ? parserInputRead(input) : ts.sys.readFile(normalizedFileName);
};
// Custom module resolution to handle in-memory imports
// This is required because TypeScript's default module resolution looks for files on disk,
// but our source files only exist in memory (in the inputFiles map)
host.resolveModuleNameLiterals = (moduleLiterals, containingFile) => {
const resolvedModules: ts.ResolvedModuleWithFailedLookupLocations[] = [];
const normalizedFileName = path.normalize(containingFile);
const containingDir = path.dirname(normalizedFileName);
for (const moduleLiteral of moduleLiterals) {
const moduleName = moduleLiteral.text;
// For relative imports, try to find in inputFiles first
if (moduleName.startsWith('.')) {
const extensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts'];
let resolved: string | undefined;
for (const ext of extensions) {
// Try with extension
const candidate = path.join(containingDir, moduleName + ext);
if (inputFiles.has(candidate)) {
resolved = candidate;
break;
}
// Also try exact match (if moduleName already has extension)
const exactCandidate = path.join(containingDir, moduleName);
if (inputFiles.has(exactCandidate)) {
resolved = exactCandidate;
break;
}
}
if (resolved) {
resolvedModules.push({
resolvedModule: {
resolvedFileName: resolved,
extension: path.extname(resolved) as ts.Extension,
isExternalLibraryImport: false,
}
});
continue;
}
}
// Fall back to TypeScript's default resolution for node_modules and absolute paths
const result = ts.resolveModuleName(
moduleName,
normalizedFileName,
this.compilerOptions,
host
);
resolvedModules.push(result);
}
return resolvedModules;
};
// Create a new Program, passing the oldProgram for incremental parsing
const program = ts.createProgram([...inputFiles.keys()], this.compilerOptions, host, this.oldProgram);
// Update the oldProgram reference
this.oldProgram = program;
// Create a single JavaScriptTypeMapping instance to be shared across all files in this parse batch.
// This ensures that TypeScript types with the same type.id map to the same Type instance,
// preventing duplicate Type.Class, Type.Parameterized, etc. instances.
const typeChecker = program.getTypeChecker();
const typeMapping = new JavaScriptTypeMapping(typeChecker, this.relativeTo);
for (const input of inputFiles.values()) {
const filePath = parserInputFile(input);
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
yield this.error(input, new Error('Parser returned undefined'));
continue;
}
if (hasFlowAnnotation(sourceFile)) {
yield this.error(input, new FlowSyntaxNotSupportedError("Flow syntax not supported"));
continue;
}
const syntaxErrors = checkSyntaxErrors(program, sourceFile);
if (syntaxErrors.length > 0) {
let errors = syntaxErrors.map(e => `${e[0]} [${e[1]}]`).join('; ');
yield this.error(input, new SyntaxError(`Compiler error(s): ${errors}`));
continue;
}
try {
yield produce(
new JavaScriptParserVisitor(sourceFile, this.relativePath(input), typeMapping)
.visit(sourceFile) as SourceFile,
draft => {
if (this.styles) {
draft.markers = this.styles.reduce((m, s) => replaceMarkerByKind(m, s), draft.markers);
}
});
} catch (error) {
yield this.error(input, error instanceof Error ? error : new Error('Parser threw unknown error: ' + error));
}
}
}
}
// we use this instead of `ts.SyntaxKind[node.kind]` because the numeric values are not unique, and we want
// the first one rather than the last one, as the last ones are things like `FirstToken`, `LastToken`, etc.
const visitMethodMap = new Map();
for (const [key, value] of Object.entries(ts.SyntaxKind)) {
if (typeof value === 'number' && !visitMethodMap.has(value)) {
visitMethodMap.set(value, 'visit' + key);
}
}
// noinspection JSUnusedGlobalSymbols
export class JavaScriptParserVisitor {
private readonly typeMapping?: JavaScriptTypeMapping;
constructor(
private readonly sourceFile: ts.SourceFile,
private readonly sourcePath: string,
typeMapping?: JavaScriptTypeMapping) {
this.typeMapping = typeMapping;
}
visit = (node: ts.Node): any => {
const member = this[(visitMethodMap.get(node.kind) as keyof JavaScriptParserVisitor)];
if (typeof member === 'function') {
return member.bind(this)(node as any);
} else {
return this.visitUnknown(node);
}
}
convert = (node: ts.Node): T => {
return this.visit(node) as T;
}
detectBOMAndTextEncoding(content: String): { hasBom: boolean; encoding: string | undefined } {
const BOM_UTF8 = "\uFEFF"; // BOM for UTF-8
const BOM_UTF16_LE = [0xFF, 0xFE]; // BOM for UTF-16 Little Endian
// Detect BOM
const hasUtf8Bom = content.startsWith(BOM_UTF8);
const hasUtf16LeBom = content.charCodeAt(0) === BOM_UTF16_LE[0] && content.charCodeAt(1) === BOM_UTF16_LE[1];
if (hasUtf8Bom) {
return {hasBom: true, encoding: 'utf8'};
} else if (hasUtf16LeBom) {
return {hasBom: true, encoding: 'utf16le'};
}
return {hasBom: false, encoding: undefined};
}
visitSourceFile(node: ts.SourceFile): JS.CompilationUnit {
let bomAndTextEncoding = this.detectBOMAndTextEncoding(node.getFullText());
let prefix = this.prefix(node);
if (bomAndTextEncoding.hasBom) {
// If a node full text has a BOM marker, it becomes a part of the prefix, so we remove it
if (bomAndTextEncoding.encoding === 'utf8') {
prefix = produce(prefix, draft => {
draft.whitespace = prefix.whitespace!.slice(1);
});
} else if (bomAndTextEncoding.encoding === 'utf16le') {
prefix = produce(prefix, draft => {
draft.whitespace = prefix.whitespace!.slice(2);
});
}
}
let shebangStatement: J.RightPadded | undefined;
let shebangTrailingSpace: J.Space | undefined;
if (prefix.whitespace?.startsWith('#!')) {
const newlineIndex = prefix.whitespace.indexOf('\n');
const shebangText = newlineIndex === -1 ? prefix.whitespace : prefix.whitespace.slice(0, newlineIndex);
// Shebang's after only contains the newline that terminates the shebang line
// The remaining whitespace and comments go into the first statement's prefix
const afterShebangNewline = newlineIndex === -1 ? '' : '\n';
const remainingWhitespace = newlineIndex === -1 ? '' : prefix.whitespace.slice(newlineIndex + 1);
shebangStatement = this.rightPadded({
kind: JS.Kind.Shebang,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
text: shebangText
}, {kind: J.Kind.Space, whitespace: afterShebangNewline, comments: []}, emptyMarkers);
// Store the trailing whitespace and comments to prepend to first statement
if (remainingWhitespace || prefix.comments.length > 0) {
shebangTrailingSpace = {kind: J.Kind.Space, whitespace: remainingWhitespace, comments: prefix.comments};
}
// CU prefix should be empty when there's a shebang
prefix = produce(prefix, draft => {
draft.whitespace = '';
draft.comments = [];
});
}
const eof = this.prefix(node.endOfFileToken);
let statements = this.semicolonPaddedStatementList(node.statements);
// If there's trailing whitespace/comments after the shebang, prepend to first statement's prefix
if (shebangTrailingSpace && statements.length > 0) {
const firstStmt = statements[0];
statements = [
produce(firstStmt, draft => {
const existingPrefix = draft.element.prefix;
draft.element.prefix = {
kind: J.Kind.Space,
whitespace: shebangTrailingSpace!.whitespace + existingPrefix.whitespace,
comments: [...shebangTrailingSpace!.comments, ...existingPrefix.comments]
};
}),
...statements.slice(1)
];
}
return {
kind: JS.Kind.CompilationUnit,
id: randomId(),
prefix: prefix,
markers: emptyMarkers,
sourcePath: this.sourcePath,
charsetName: bomAndTextEncoding.encoding,
charsetBomMarked: bomAndTextEncoding.hasBom,
statements: shebangStatement
? [shebangStatement, ...statements]
: statements,
eof
};
}
private semicolonPaddedStatementList(statements: ts.NodeArray) {
return [...statements].map(n => {
const j: Statement = this.convert(n);
if (j.kind === J.Kind.Unknown) {
// in case of `J.Unknown` its source will already contain any `;`
return this.rightPadded(j, emptySpace, emptyMarkers);
}
let last: ts.Node | undefined = n.getChildAt(n.getChildCount(this.sourceFile) - 1, this.sourceFile)
if (last && (last.kind === ts.SyntaxKind.ExpressionStatement || last.kind == ts.SyntaxKind.DoStatement) && n.kind === ts.SyntaxKind.LabeledStatement) {
last = last.getLastToken(this.sourceFile);
}
return this.rightPadded(j, this.semicolonPrefix(n), (_ => {
return last?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers;
})?.(n));
});
}
visitUnknown(node: ts.Node): J.Unknown {
return {
kind: J.Kind.Unknown,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
source: {
kind: J.Kind.UnknownSource,
id: randomId(),
prefix: emptySpace,
markers: markers({
kind: MarkersKind.ParseExceptionResult,
id: randomId(),
parserType: "JavaScriptParser",
exceptionType: "Error",
message: "Unsupported AST element: " + node,
treeType: visitMethodMap.get(node.kind)!.substring(5)
} satisfies ParseExceptionResult as ParseExceptionResult),
text: node.getFullText()
}
};
}
private mapModifiers(node: ts.VariableDeclarationList | ts.VariableStatement | ts.ClassDeclaration | ts.PropertyDeclaration
| ts.FunctionDeclaration | ts.ParameterDeclaration | ts.MethodDeclaration | ts.EnumDeclaration | ts.InterfaceDeclaration
| ts.PropertySignature | ts.ConstructorDeclaration | ts.ModuleDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration
| ts.ArrowFunction | ts.IndexSignatureDeclaration | ts.TypeAliasDeclaration | ts.ExportDeclaration | ts.ExportAssignment | ts.FunctionExpression
| ts.ConstructorTypeNode | ts.TypeParameterDeclaration | ts.ImportDeclaration | ts.ImportEqualsDeclaration): J.Modifier[] {
if (ts.isVariableStatement(node) || ts.isModuleDeclaration(node) || ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)
|| ts.isInterfaceDeclaration(node) || ts.isPropertyDeclaration(node) || ts.isPropertySignature(node) || ts.isParameter(node)
|| ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isArrowFunction(node)
|| ts.isIndexSignatureDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isExportDeclaration(node)
|| ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isConstructorTypeNode(node) || ts.isTypeParameterDeclaration(node) || ts.isImportDeclaration(node) || ts.isImportEqualsDeclaration(node)) {
return node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : [];
} else if (ts.isExportAssignment(node)) {
const defaultModifier = this.findChildNode(node, ts.SyntaxKind.DefaultKeyword);
return [
...node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : [],
...defaultModifier && ts.isModifier(defaultModifier) ? [this.mapModifier(defaultModifier)] : []
]
} else if (ts.isVariableDeclarationList(node)) {
let modifier: string | undefined;
if ((node.flags & ts.NodeFlags.Let) !== 0) {
modifier = "let";
} else if ((node.flags & ts.NodeFlags.Const) !== 0) {
modifier = "const";
} else {
modifier = "var";
}
return modifier ? [{
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
keyword: "let",
type: J.ModifierType.LanguageExtension,
annotations: []
}] : [];
} else if (ts.isGetAccessorDeclaration(node)) {
return (node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []).concat({
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.GetKeyword)!),
markers: emptyMarkers,
keyword: 'get',
type: J.ModifierType.LanguageExtension,
annotations: []
});
} else if (ts.isSetAccessorDeclaration(node)) {
return (node.modifiers ? node.modifiers?.filter(ts.isModifier).map(this.mapModifier) : []).concat({
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.SetKeyword)!),
markers: emptyMarkers,
keyword: 'set',
type: J.ModifierType.LanguageExtension,
annotations: []
});
}
throw new Error(`Cannot get modifiers from ${node}`);
}
private mapModifier = (node: ts.Modifier | ts.ModifierLike): J.Modifier => {
let kind: J.ModifierType;
switch (node.kind) {
case ts.SyntaxKind.PublicKeyword:
kind = J.ModifierType.Public;
break;
case ts.SyntaxKind.PrivateKeyword:
kind = J.ModifierType.Private;
break;
case ts.SyntaxKind.ProtectedKeyword:
kind = J.ModifierType.Protected;
break;
case ts.SyntaxKind.StaticKeyword:
kind = J.ModifierType.Static;
break;
case ts.SyntaxKind.AbstractKeyword:
kind = J.ModifierType.Abstract;
break;
default:
kind = J.ModifierType.LanguageExtension;
}
return {
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(node!),
markers: emptyMarkers,
keyword: kind === J.ModifierType.LanguageExtension ? node.getText() : undefined,
type: kind,
annotations: []
};
}
private rightPadded(t: T, trailing: J.Space, markers?: Markers): J.RightPadded {
return {
kind: J.Kind.RightPadded,
element: t,
after: trailing ?? emptySpace,
markers: markers ?? emptyMarkers
};
}
private rightPaddedList(nodes: N[], trailing: (node: N) => J.Space, markers?: (node: N) => Markers): J.RightPadded[] {
return nodes.map(n => this.rightPadded(this.convert(n), trailing(n), markers?.(n)));
}
private rightPaddedSeparatedList(nodes: N[], markers?: (nodes: N[], i: number) => Markers): J.RightPadded[] {
if (nodes.length === 0) {
return [];
}
const ts: J.RightPadded[] = [];
for (let i = 0; i < nodes.length - 1; i += 2) {
// FIXME right padding and trailing comma
ts.push(this.rightPadded(
this.convert(nodes[i]),
this.prefix(nodes[i + 1]),
markers ? markers(nodes, i) : emptyMarkers
));
}
if ((nodes.length & 1) === 1) {
ts.push(this.rightPadded(this.convert(nodes[nodes.length - 1]), emptySpace, markers ? markers(nodes, nodes.length - 1) : emptyMarkers));
}
return ts;
}
private leftPadded(before: J.Space, t: T, markers?: Markers): J.LeftPadded {
return {
kind: J.Kind.LeftPadded,
before: before ?? emptySpace,
element: t,
markers: markers ?? emptyMarkers
};
}
private semicolonPrefix = (node: ts.Node) => {
let last: ts.Node | undefined = node.getChildren(this.sourceFile).slice(-1)[0];
if (last && (last.kind === ts.SyntaxKind.ExpressionStatement || last.kind == ts.SyntaxKind.DoStatement)) {
last = last.getLastToken(this.sourceFile);
}
return last?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(last) : emptySpace;
}
private keywordPrefix = (token: ts.PunctuationSyntaxKind, findSibling: (node: ts.Node) => ts.Node | undefined) => (node: ts.Node): J.Space => {
const last = findSibling(node);
return last?.kind === token ? this.prefix(last) : emptySpace;
}
visitClassDeclaration(node: ts.ClassDeclaration): J.ClassDeclaration {
return {
kind: J.Kind.ClassDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
classKind: {
kind: J.Kind.ClassDeclarationKind,
id: randomId(),
prefix: node.modifiers ? this.suffix(node.modifiers[node.modifiers.length - 1]) : this.prefix(node),
markers: emptyMarkers,
annotations: [],
type: J.ClassDeclaration.Kind.Type.Class
},
name: node.name ? this.convert(node.name) : this.mapIdentifier(node, ""),
typeParameters: this.mapTypeParametersAsContainer(node),
primaryConstructor: undefined, // FIXME primary constructor
extends: this.mapExtends(node),
implements: this.mapImplements(node),
body: {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace, emptyMarkers),
statements: node.members.map((ce: ts.ClassElement) => this.rightPadded(
this.convert(ce),
ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(ce.getLastToken()!) : emptySpace,
ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
)),
end: this.prefix(node.getLastToken()!)
},
type: this.mapDeclarationType(node)
};
}
private mapExtends(node: ts.ClassDeclaration | ts.ClassExpression): J.LeftPadded | undefined {
if (node.heritageClauses == undefined || node.heritageClauses.length == 0) {
return undefined;
}
for (let heritageClause of node.heritageClauses) {
if (heritageClause.token == ts.SyntaxKind.ExtendsKeyword) {
return this.leftPadded(
this.prefix(heritageClause.getFirstToken()!),
this.mapTypeTree(heritageClause.types[0])
);
}
}
return undefined;
}
private mapInterfaceExtends(node: ts.InterfaceDeclaration): J.Container | undefined {
if (node.heritageClauses == undefined || node.heritageClauses.length == 0) {
return undefined;
}
for (let heritageClause of node.heritageClauses) {
if ((heritageClause.token == ts.SyntaxKind.ExtendsKeyword)) {
const _extends: J.RightPadded[] = [];
const types = heritageClause.types;
for (let i = 0; i < types.length; i++) {
const type = types[i];
// For the last type, don't consume the suffix - it belongs to the interface body's prefix
const after = i < types.length - 1 ? this.suffix(type) : emptySpace;
_extends.push(this.rightPadded(this.visit(type), after));
}
return _extends.length > 0 ? {
kind: J.Kind.Container,
before: this.prefix(heritageClause.getFirstToken()!),
elements: _extends,
markers: emptyMarkers
} : undefined;
}
}
return undefined;
}
private mapImplements(node: ts.ClassDeclaration | ts.ClassExpression): J.Container | undefined {
if (node.heritageClauses == undefined || node.heritageClauses.length == 0) {
return undefined;
}
for (let heritageClause of node.heritageClauses) {
if (heritageClause.token == ts.SyntaxKind.ImplementsKeyword) {
const _implements: J.RightPadded[] = [];
const types = heritageClause.types;
for (let i = 0; i < types.length; i++) {
const type = types[i];
// For the last type, don't consume the suffix - it belongs to the class body's prefix
const after = i < types.length - 1 ? this.suffix(type) : emptySpace;
_implements.push(this.rightPadded(this.visit(type), after));
}
return _implements.length > 0 ? {
kind: J.Kind.Container,
before: this.prefix(heritageClause.getFirstToken()!),
elements: _implements,
markers: emptyMarkers
} : undefined;
}
}
return undefined;
}
private mapTypeTree(node: ts.LeftHandSideExpression | ts.ExpressionWithTypeArguments | ts.Expression): TypeTree {
const expression = this.visit(node);
// Check if the expression is already a TypeTree
// J.Identifier, J.FieldAccess, J.ArrayType, and J.ParameterizedType all implement TypeTree
if (expression.kind === J.Kind.Identifier ||
expression.kind === J.Kind.FieldAccess ||
expression.kind === J.Kind.ArrayType ||
expression.kind === J.Kind.ParameterizedType) {
return expression as TypeTree;
}
// For expressions that don't implement TypeTree, wrap them in JS.TypeTreeExpression
// Transfer the prefix from the expression to the wrapper
const prefix = expression.prefix;
return {
kind: JS.Kind.TypeTreeExpression,
id: randomId(),
prefix: prefix,
markers: emptyMarkers,
expression: {
...expression,
prefix: emptySpace
},
} satisfies JS.TypeTreeExpression as JS.TypeTreeExpression;
}
visitNumericLiteral(node: ts.NumericLiteral): J.Literal {
// Parse the numeric value from the text
const text = node.text;
let value: number | bigint | string;
// Check if it's a BigInt literal (ends with 'n')
if (text.endsWith('n')) {
// TODO consider adding `JS.Literal`
value = text.slice(0, -1);
} else if (text.includes('.') || text.toLowerCase().includes('e')) {
// Floating point number
value = parseFloat(text);
} else {
// Integer - but JavaScript doesn't distinguish, so use number
value = parseInt(text, text.startsWith('0x') ? 16 : text.startsWith('0o') ? 8 : text.startsWith('0b') ? 2 : 10);
}
return this.mapLiteral(node, value);
}
visitTrueKeyword(node: ts.TrueLiteral): J.Literal {
return this.mapLiteral(node, true);
}
visitNumberKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'number');
}
visitBooleanKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'boolean');
}
visitStringKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'string');
}
visitUndefinedKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'undefined');
}
visitAnyKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'any');
}
visitIntrinsicKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'intrinsic');
}
visitObjectKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'object');
}
visitUnknownKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'unknown');
}
visitVoidKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'void');
}
visitFalseKeyword(node: ts.FalseLiteral): J.Literal {
return this.mapLiteral(node, false);
}
visitNullKeyword(node: ts.NullLiteral): J.Literal {
return this.mapLiteral(node, null);
}
visitNeverKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'never');
}
visitSymbolKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'symbol');
}
visitBigIntKeyword(node: ts.Node): J.Identifier {
return this.mapIdentifier(node, 'bigint');
}
private mapLiteral(node: ts.LiteralExpression | ts.TrueLiteral | ts.FalseLiteral | ts.NullLiteral | ts.Identifier
| ts.TemplateHead | ts.TemplateMiddle | ts.TemplateTail | ts.JsxText, value: any): J.Literal {
const valueSource = node.getText();
const { cleanedSource, unicodeEscapes } = extractSurrogateEscapes(valueSource);
return {
kind: J.Kind.Literal,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
value: unicodeEscapes.length > 0 ? cleanedSource : value,
valueSource: cleanedSource,
unicodeEscapes: unicodeEscapes.length > 0 ? unicodeEscapes : undefined,
type: this.literalType(node)
};
}
/**
* Resolve a literal's primitive type from its own lexical category rather than
* from the type checker.
*
* A literal's primitive is fixed by how it is written, not by its type according to the type checker.
*/
private literalType(node: ts.Node): Type.Primitive {
switch (node.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.RegularExpressionLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.TemplateHead:
case ts.SyntaxKind.TemplateMiddle:
case ts.SyntaxKind.TemplateTail:
case ts.SyntaxKind.JsxText:
return Type.Primitive.String;
case ts.SyntaxKind.NumericLiteral:
return Type.Primitive.Double;
case ts.SyntaxKind.BigIntLiteral:
return Type.Primitive.BigInt;
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
return Type.Primitive.Boolean;
case ts.SyntaxKind.NullKeyword:
return Type.Primitive.Null;
default:
// Identifiers reaching mapLiteral (e.g. undefined) and any future
// node kinds fall back to the checker.
return this.mapPrimitiveType(node);
}
}
visitBigIntLiteral(node: ts.BigIntLiteral): J.Literal {
// Parse BigInt value, removing the 'n' suffix
const text = node.text;
// TODO consider adding `JS.Literal`
const value = text.slice(0, -1);
return this.mapLiteral(node, value);
}
visitStringLiteral(node: ts.StringLiteral): J.Literal {
return this.mapLiteral(node, node.text);
}
visitRegularExpressionLiteral(node: ts.RegularExpressionLiteral): J.Literal {
// TODO consider adding `JS.Literal`
return this.mapLiteral(node, node.text); // FIXME value not in AST
}
visitNoSubstitutionTemplateLiteral(node: ts.NoSubstitutionTemplateLiteral): J.Literal {
return this.mapLiteral(node, node.text); // FIXME value not in AST
}
visitTemplateHead(node: ts.TemplateHead): J.Literal {
return this.mapLiteral(node, node.text);
}
visitTemplateMiddle(node: ts.TemplateMiddle): J.Literal {
return this.mapLiteral(node, node.text);
}
visitTemplateTail(node: ts.TemplateTail): J.Literal {
return this.mapLiteral(node, node.text);
}
visitIdentifier(node: ts.Identifier): J.Identifier {
return this.mapIdentifier(node, node.text);
}
private mapIdentifier(node: ts.Node, name: string, withType: boolean = true): J.Identifier {
let type = withType ? this.mapType(node) : undefined;
return {
kind: J.Kind.Identifier,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
annotations: [], // FIXME decorators
simpleName: name,
type: type?.kind === Type.Kind.Variable ? (type as Type.Variable).type : type,
fieldType: type?.kind === Type.Kind.Variable ? type as Type.Variable : undefined
};
}
visitThisKeyword(node: ts.ThisExpression): J.Identifier {
return this.mapIdentifier(node, 'this');
}
visitPrivateIdentifier(node: ts.PrivateIdentifier): J.Identifier {
return this.mapIdentifier(node, node.text);
}
visitQualifiedName(node: ts.QualifiedName): J.FieldAccess {
return {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
target: this.visit(node.left),
name: this.leftPadded(this.suffix(node.left), this.convert(node.right)),
type: this.mapType(node)
};
}
visitComputedPropertyName(node: ts.ComputedPropertyName): JS.ComputedPropertyName {
return {
kind: JS.Kind.ComputedPropertyName,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.rightPadded(this.convert(node.expression), this.suffix(node.expression))
};
}
visitTypeParameter(node: ts.TypeParameterDeclaration): J.TypeParameter {
return {
kind: J.Kind.TypeParameter,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
annotations: [],
modifiers: this.mapModifiers(node),
name: this.visit(node.name),
bounds: (node.constraint || node.default) && {
kind: J.Kind.Container,
before: this.prefix(this.findChildNode(node, ts.SyntaxKind.ExtendsKeyword) ?? this.findChildNode(node, ts.SyntaxKind.EqualsToken)!),
elements: [
node.constraint ? this.rightPadded(this.visit(node.constraint), this.suffix(node.constraint)) : this.rightPadded(this.newEmpty(), emptySpace),
node.default ? this.rightPadded(this.visit(node.default), this.suffix(node.default)) : this.rightPadded(this.newEmpty(), emptySpace)
],
markers: emptyMarkers
}
};
}
visitParameter(node: ts.ParameterDeclaration): J.VariableDeclarations {
let name: VariableDeclarator = produce(this.convert(node.name), draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
});
if (node.dotDotDotToken) {
// Wrap in JS.Spread; prefix between ... and identifier stays on the inner expression
const spread: JS.Spread = {
kind: JS.Kind.Spread,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
expression: name as Expression,
type: this.mapType(node)
};
name = spread;
}
return {
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
typeExpression: this.mapTypeInfo(node),
variables: [this.rightPadded(
{
kind: J.Kind.NamedVariable,
id: randomId(),
prefix: node.dotDotDotToken ? this.prefix(node.dotDotDotToken) : this.prefix(node.name),
markers: emptyMarkers,
name: name,
dimensionsAfterName: [],
initializer: node.initializer && this.leftPadded(this.prefix(node.getChildAt(node.getChildren().indexOf(node.initializer) - 1)), this.visit(node.initializer)),
variableType: this.mapVariableType(node)
},
this.suffix(node.name)
)]
};
}
visitDecorator(node: ts.Decorator): J.Annotation | J.Unknown {
let annotationType: NameTree | TypeTree;
let _arguments: J.Container | undefined = undefined;
// The identifier/qualified name that names the decorator — the call target for
// `@Entity()` / `@Entity()`, or the bare reference for `@Entity`.
const nameNode = ts.isCallExpression(node.expression) ? node.expression.expression : node.expression;
if (ts.isCallExpression(node.expression) && node.expression.typeArguments) {
annotationType = {
kind: JS.Kind.ExpressionWithTypeArguments,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
clazz: this.convert(node.expression.expression) as Expression,
typeArguments: this.mapTypeArguments(this.suffix(node.expression.expression), node.expression.typeArguments)
} satisfies JS.ExpressionWithTypeArguments as JS.ExpressionWithTypeArguments;
_arguments = this.mapCommaSeparatedList(node.expression.getChildren(this.sourceFile).slice(-3))
} else if (ts.isCallExpression(node.expression)) {
annotationType = this.convert(node.expression.expression) as Expression;
_arguments = this.mapCommaSeparatedList(node.expression.getChildren(this.sourceFile).slice(-3))
} else if (ts.isIdentifier(node.expression)) {
annotationType = this.convert(node.expression);
} else if (ts.isPropertyAccessExpression(node.expression)) {
annotationType = this.convert(node.expression);
} else if (ts.isParenthesizedExpression(node.expression)) {
annotationType = this.mapTypeTree(node.expression);
} else {
return this.visitUnknown(node);
}
// Attribute the decorator's fully qualified type (e.g. `typeorm.Entity`) so it is matchable
// by name, like Java annotations — rather than the generic function type of the decorator
// factory that the callee identifier otherwise resolves to.
const type = this.mapAnnotationType(nameNode);
if (type) {
// Identifier, FieldAccess and ExpressionWithTypeArguments (the decorator name forms)
// all declare an optional `type`; cast through `unknown` since the union itself does not.
annotationType = {...annotationType, type} as unknown as NameTree | TypeTree;
}
return {
kind: J.Kind.Annotation,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
annotationType: annotationType,
arguments: _arguments
};
}
visitPropertySignature(node: ts.PropertySignature): J.VariableDeclarations {
// FIXME We are mapping the literals in things like type ascii = { " ": 32; "!": 33; } to
// named variables, which is not a good use of this construct.
return {
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [], // no decorators allowed
modifiers: this.mapModifiers(node),
typeExpression: this.mapTypeInfo(node),
variables: [this.rightPadded(
{
kind: J.Kind.NamedVariable,
id: randomId(),
prefix: this.prefix(node.name),
markers: emptyMarkers,
name: produce(this.convert(node.name), draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
}) as VariableDeclarator,
dimensionsAfterName: [],
variableType: this.mapVariableType(node)
},
emptySpace
)]
};
}
visitPropertyDeclaration(node: ts.PropertyDeclaration): J.VariableDeclarations {
return {
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
typeExpression: this.mapTypeInfo(node),
variables: [this.rightPadded(
{
kind: J.Kind.NamedVariable,
id: randomId(),
prefix: this.prefix(node.name),
markers: emptyMarkers,
name: produce(this.convert(node.name), draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
// This will be mutually exclusive with the optional token
if (node.exclamationToken) {
draft.markers.markers.push({
kind: JS.Markers.NonNullAssertion,
id: randomId(),
prefix: this.suffix(node.name)
} satisfies NonNullAssertion as NonNullAssertion);
}
}),
dimensionsAfterName: [],
initializer: node.initializer && this.leftPadded(this.prefix(node.getChildAt(node.getChildren().indexOf(node.initializer) - 1)), this.visit(node.initializer)),
variableType: this.mapVariableType(node)
},
emptySpace
)]
};
}
visitMethodSignature(node: ts.MethodSignature): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
const prefix = this.prefix(node);
const methodType = this.mapMethodType(node);
if (ts.isComputedPropertyName(node.name)) {
return {
kind: JS.Kind.ComputedPropertyMethodDeclaration,
id: randomId(),
prefix: prefix,
markers: emptyMarkers,
leadingAnnotations: [], // no decorators allowed
modifiers: [], // no modifiers allowed
typeParameters: this.mapTypeParametersAsObject(node),
returnTypeExpression: this.mapTypeInfo(node),
name: produce(this.convert(node.name), draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
}),
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
methodType: methodType
};
}
let name = this.mapMethodName(node, methodType) as J.Identifier;
name = produce(name, draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
});
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: prefix,
markers: emptyMarkers,
leadingAnnotations: [], // no decorators allowed
modifiers: [], // no modifiers allowed
typeParameters: this.mapTypeParametersAsObject(node),
returnTypeExpression: this.mapTypeInfo(node),
nameAnnotations: [],
name: name,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
methodType: methodType
};
}
visitMethodDeclaration(node: ts.MethodDeclaration): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
const prefix = this.prefix(node);
const methodType = this.mapMethodType(node);
const markers = produce(emptyMarkers, draft => {
if (node.asteriskToken) {
draft.markers.push({
kind: JS.Markers.Generator,
id: randomId(),
prefix: this.prefix(node.asteriskToken)
} satisfies Generator as Generator);
}
});
let name = this.mapMethodName(node, methodType);
name = produce(name, draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
});
if (ts.isComputedPropertyName(node.name)) {
return {
kind: JS.Kind.ComputedPropertyMethodDeclaration,
id: randomId(),
prefix: prefix,
markers: markers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
typeParameters: this.mapTypeParametersAsObject(node),
returnTypeExpression: this.mapTypeInfo(node),
name: name as ComputedPropertyName,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
body: node.body && this.convert(node.body),
methodType: methodType
};
}
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: prefix,
markers: markers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
typeParameters: this.mapTypeParametersAsObject(node),
returnTypeExpression: this.mapTypeInfo(node),
nameAnnotations: [],
name: name as J.Identifier,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
body: node.body && this.convert(node.body),
methodType: methodType
};
}
private mapMethodName(node: ts.NamedDeclaration, methodType?: Type.Method): J.Identifier | JS.ComputedPropertyName {
// Build the name identifier without an independent type — the containing
// MethodDeclaration's methodType is the authoritative source. We set the
// identifier's type to the same methodType object to maintain the
// referential equality invariant (name.type === methodType).
let ident: J.Identifier | JS.ComputedPropertyName;
if (!node.name) {
ident = this.mapIdentifier(node, "", false);
} else if (ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name)) {
ident = this.mapIdentifier(node.name, node.name.getText(), false);
} else if (ts.isComputedPropertyName(node.name)) {
return this.visit(node.name);
} else {
ident = this.mapIdentifier(node.name, node.name.getText(), false);
}
if (methodType && ident.kind === J.Kind.Identifier) {
return {...ident, type: methodType};
}
return ident;
}
private mapTypeInfo(node: ts.MethodDeclaration | ts.PropertyDeclaration | ts.VariableDeclaration | ts.ParameterDeclaration
| ts.PropertySignature | ts.MethodSignature | ts.ArrowFunction | ts.CallSignatureDeclaration | ts.GetAccessorDeclaration
| ts.FunctionDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionExpression | ts.NamedTupleMember): JS.TypeInfo | undefined {
return node.type && {
kind: JS.Kind.TypeInfo,
id: randomId(),
prefix: this.prefix(node.getChildAt(node.getChildren().indexOf(node.type) - 1)),
markers: emptyMarkers,
typeIdentifier: this.visit(node.type)
};
}
visitClassStaticBlockDeclaration(node: ts.ClassStaticBlockDeclaration): J.Block {
return {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
static: this.rightPadded(true, this.prefix(this.findChildNode(node.body, ts.SyntaxKind.OpenBraceToken)!), emptyMarkers),
statements: node.body.statements.map(ce => this.rightPadded(
this.convert(ce),
ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(ce.getLastToken()!) : emptySpace,
ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
)),
end: this.prefix(node.getLastToken()!)
};
}
visitConstructor(node: ts.ConstructorDeclaration): J.MethodDeclaration {
// using string literal for the following case: class A { "constructor"() {} }
const constructorKeyword = node.getChildren()
.find(n => (n.kind === ts.SyntaxKind.ConstructorKeyword) || ((n.kind === ts.SyntaxKind.StringLiteral) && (n.getText().includes("constructor"))))!;
const methodType = this.mapMethodType(node);
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
nameAnnotations: [],
name: {...this.mapIdentifier(constructorKeyword, constructorKeyword.getText(), false), type: methodType},
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
body: node.body && this.convert(node.body),
methodType: methodType
};
}
visitGetAccessor(node: ts.GetAccessorDeclaration): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
const methodType = this.mapMethodType(node);
const name = this.mapMethodName(node, methodType);
if (ts.isComputedPropertyName(node.name)) {
return {
kind: JS.Kind.ComputedPropertyMethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
returnTypeExpression: this.mapTypeInfo(node),
name: name as ComputedPropertyName,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
body: node.body && this.convert(node.body),
methodType: methodType
};
}
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
returnTypeExpression: this.mapTypeInfo(node),
nameAnnotations: [],
name: name as J.Identifier,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
body: node.body && this.convert(node.body),
methodType: methodType
};
}
visitSetAccessor(node: ts.SetAccessorDeclaration): J.MethodDeclaration | JS.ComputedPropertyMethodDeclaration {
const methodType = this.mapMethodType(node);
const name = this.mapMethodName(node, methodType);
if (ts.isComputedPropertyName(node.name)) {
return {
kind: JS.Kind.ComputedPropertyMethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
name: name as ComputedPropertyName,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
body: node.body && this.convert(node.body),
methodType: methodType
};
}
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: this.mapModifiers(node),
nameAnnotations: [],
name: name as J.Identifier,
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
body: node.body && this.convert(node.body),
methodType: methodType
};
}
visitCallSignature(node: ts.CallSignatureDeclaration): J.MethodDeclaration {
const methodType = this.mapMethodType(node);
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: [],
typeParameters: this.mapTypeParametersAsObject(node),
returnTypeExpression: this.mapTypeInfo(node),
nameAnnotations: [],
name: {
kind: J.Kind.Identifier,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
simpleName: "",
type: methodType
},
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
methodType: methodType
};
}
visitConstructSignature(node: ts.ConstructSignatureDeclaration): J.MethodDeclaration {
const methodType = this.mapMethodType(node);
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [], // no decorators allowed
modifiers: [], // no modifiers allowed
typeParameters: this.mapTypeParametersAsObject(node),
returnTypeExpression: this.mapTypeInfo(node),
nameAnnotations: [],
name: {
kind: J.Kind.Identifier,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
simpleName: 'new',
type: methodType
},
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
dimensionsAfterName: [],
methodType: methodType
};
}
visitIndexSignature(node: ts.IndexSignatureDeclaration): JS.IndexSignatureDeclaration {
return {
kind: JS.Kind.IndexSignatureDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: this.mapModifiers(node),
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node, ts.SyntaxKind.OpenBracketToken)),
typeExpression: this.leftPadded(this.prefix(node.getChildAt(node.getChildren().indexOf(node.type) - 1)), this.convert(node.type)),
type: this.mapType(node)
};
}
visitTypePredicate(node: ts.TypePredicateNode): JS.TypePredicate {
return {
kind: JS.Kind.TypePredicate,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
asserts: node.assertsModifier ? this.leftPadded(this.prefix(node.assertsModifier), true) : this.leftPadded(emptySpace, false),
parameterName: this.visit(node.parameterName),
expression: node.type && this.leftPadded(this.suffix(node.parameterName), this.convert(node.type)),
type: this.mapType(node)
};
}
visitTypeReference(node: ts.TypeReferenceNode): J.ParameterizedType {
if (node.typeArguments) {
return {
kind: J.Kind.ParameterizedType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
class: this.visit(node.typeName),
typeParameters: this.mapTypeArguments(this.suffix(node.typeName), node.typeArguments),
type: this.mapType(node)
}
}
return this.visit(node.typeName);
}
visitFunctionType(node: ts.FunctionTypeNode): JS.FunctionType {
return {
kind: JS.Kind.FunctionType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: [],
constructorType: this.leftPadded(emptySpace, false),
typeParameters: this.mapTypeParametersAsObject(node),
parameters: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(node.typeParameters ? 3 : 0)),
returnType: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.EqualsGreaterThanToken)!), this.convert(node.type))
};
}
visitConstructorType(node: ts.ConstructorTypeNode): JS.FunctionType {
return {
kind: JS.Kind.FunctionType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: this.mapModifiers(node),
constructorType: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.NewKeyword)!), true),
typeParameters: this.mapTypeParametersAsObject(node),
parameters: {
kind: J.Kind.Container,
before: this.prefix(node.getChildAt(node.getChildren().findIndex(n => n.pos === node.parameters.pos) - 1)),
elements: node.parameters.length == 0 ?
[this.rightPadded(this.newEmpty(), this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseParenToken)!))]
: node.parameters.map(p => this.rightPadded(this.visit(p), this.suffix(p)))
.concat(node.parameters.hasTrailingComma ? this.rightPadded(this.newEmpty(), this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseParenToken)!)) : []),
markers: emptyMarkers
},
returnType: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.EqualsGreaterThanToken)!), this.convert(node.type))
};
}
visitTypeQuery(node: ts.TypeQueryNode): JS.TypeQuery {
return {
kind: JS.Kind.TypeQuery,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
typeExpression: this.convert(node.exprName),
typeArguments: node.typeArguments && this.mapTypeArguments(this.suffix(node.exprName), node.typeArguments),
type: this.mapType(node)
}
}
visitTypeLiteral(node: ts.TypeLiteralNode): JS.TypeLiteral {
return {
kind: JS.Kind.TypeLiteral,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
members: {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: node.members.map(te => ({
kind: J.Kind.RightPadded,
element: this.convert(te),
after: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : emptySpace,
markers: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? markers(this.convertToken(te.getLastToken())!) : emptyMarkers
})),
end: this.prefix(node.getLastToken()!)
},
type: this.mapType(node)
};
}
visitArrayType(node: ts.ArrayTypeNode): J.ArrayType {
return {
kind: J.Kind.ArrayType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
elementType: this.convert(node.elementType),
dimension: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBracketToken)!), this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseBracketToken)!)),
type: this.mapType(node)!
}
}
visitTupleType(node: ts.TupleTypeNode): JS.Tuple {
return {
kind: JS.Kind.Tuple,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
elements: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(-3)),
type: this.mapType(node)
};
}
/**
* This can only exist in the context of a TupleTypeNode, and so printing
* of the Optional marker will be handled specially in the printer for {@link JS.Tuple}.
* @param node The optional type node.
*/
visitOptionalType(node: ts.OptionalTypeNode): JS {
const type = this.visit(node.type) as JS;
return produce(type, draft => {
draft.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: this.suffix(node.type)
} satisfies Optional as Optional);
});
}
visitRestType(node: ts.RestTypeNode): JS.Spread {
const innerExpr = this.convert(node.type);
return {
kind: JS.Kind.Spread,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: {...innerExpr, prefix: emptySpace},
type: this.mapType(node)
};
}
visitUnionType(node: ts.UnionTypeNode): JS.Union {
const initialBar = getPreviousSibling(node.types[0]);
return {
kind: JS.Kind.Union,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
types: [
...(initialBar?.kind === ts.SyntaxKind.BarToken ? [this.rightPadded(this.newEmpty(), this.prefix(initialBar))] : []),
...this.rightPaddedList([...node.types], (n) => this.keywordPrefix(ts.SyntaxKind.BarToken, getNextSibling)(n))
],
type: this.mapType(node)
};
}
visitIntersectionType(node: ts.IntersectionTypeNode): JS.Intersection {
const initialAmpersand = getPreviousSibling(node.types[0]);
return {
kind: JS.Kind.Intersection,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
types: [
...(initialAmpersand?.kind === ts.SyntaxKind.AmpersandToken ? [this.rightPadded(this.newEmpty(), this.prefix(initialAmpersand))] : []),
...this.rightPaddedList([...node.types], (n) => this.keywordPrefix(ts.SyntaxKind.AmpersandToken, getNextSibling)(n))
],
type: this.mapType(node)
};
}
visitConditionalType(node: ts.ConditionalTypeNode): JS.ConditionalType {
return {
kind: JS.Kind.ConditionalType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
checkType: this.visit(node.checkType),
condition: {
kind: J.Kind.LeftPadded,
before: this.prefix(this.findChildNode(node, ts.SyntaxKind.ExtendsKeyword)!),
element: {
kind: J.Kind.Ternary,
id: randomId(),
prefix: this.prefix(node.extendsType),
markers: emptyMarkers,
condition: this.convert(node.extendsType),
truePart: this.leftPadded(this.suffix(node.extendsType), this.convert(node.trueType)),
falsePart: this.leftPadded(this.suffix(node.trueType), this.convert(node.falseType)),
type: this.mapType(node)
},
markers: emptyMarkers
},
type: this.mapType(node)
};
}
visitInferType(node: ts.InferTypeNode): JS.InferType {
return {
kind: JS.Kind.InferType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
typeParameter: this.leftPadded(emptySpace, this.convert(node.typeParameter)),
type: this.mapType(node)
};
}
visitParenthesizedType(node: ts.ParenthesizedTypeNode): J.ParenthesizedTypeTree {
return {
kind: J.Kind.ParenthesizedTypeTree,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
annotations: [],
parenthesizedType: {
kind: J.Kind.Parentheses,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
tree: this.rightPadded(this.convert(node.type), this.prefix(node.getLastToken()!))
}
};
}
visitThisType(node: ts.ThisTypeNode): J.Identifier {
return this.mapIdentifier(node, 'this');
}
visitTypeOperator(node: ts.TypeOperatorNode): JS.TypeOperator {
function mapTypeOperator(operator: ts.SyntaxKind.KeyOfKeyword | ts.SyntaxKind.UniqueKeyword | ts.SyntaxKind.ReadonlyKeyword): JS.TypeOperator.Type | undefined {
switch (operator) {
case ts.SyntaxKind.KeyOfKeyword:
return JS.TypeOperator.Type.KeyOf;
case ts.SyntaxKind.ReadonlyKeyword:
return JS.TypeOperator.Type.ReadOnly;
case ts.SyntaxKind.UniqueKeyword:
return JS.TypeOperator.Type.Unique;
}
}
return {
kind: JS.Kind.TypeOperator,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
operator: mapTypeOperator(node.operator)!,
expression: this.leftPadded(this.prefix(node.type), this.visit(node.type))
};
}
visitIndexedAccessType(node: ts.IndexedAccessTypeNode): JS.IndexedAccessType {
return {
kind: JS.Kind.IndexedAccessType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
objectType: this.convert(node.objectType),
indexType: {
kind: JS.Kind.IndexedAccessTypeIndexType,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBracketToken)!),
markers: emptyMarkers,
element: this.rightPadded(this.convert(node.indexType), this.suffix(node.indexType)),
type: this.mapType(node.indexType)
} satisfies JS.IndexedAccessType.IndexType as JS.IndexedAccessType.IndexType,
type: this.mapType(node)
};
}
visitMappedType(node: ts.MappedTypeNode): JS.MappedType {
function hasPrefixToken(readonlyToken?: ts.ReadonlyKeyword | ts.PlusToken | ts.MinusToken): boolean {
return !!(readonlyToken && (readonlyToken.kind === ts.SyntaxKind.PlusToken || readonlyToken.kind === ts.SyntaxKind.MinusToken));
}
function hasSuffixToken(questionToken?: ts.QuestionToken | ts.PlusToken | ts.MinusToken): boolean {
return !!(questionToken && (questionToken.kind === ts.SyntaxKind.PlusToken || questionToken.kind === ts.SyntaxKind.MinusToken));
}
return {
kind: JS.Kind.MappedType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
prefixToken: hasPrefixToken(node.readonlyToken) ? this.leftPadded(
this.prefix(node.readonlyToken!),
{
kind: J.Kind.Literal,
id: randomId(),
prefix: this.prefix(node.readonlyToken!),
markers: emptyMarkers,
value: undefined, // FIXME verify
valueSource: node.readonlyToken!.getText(),
type: this.mapPrimitiveType(node.readonlyToken!)
}
) : undefined,
hasReadonly: node.readonlyToken ? this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.ReadonlyKeyword)!), true) : this.leftPadded(emptySpace, false),
keysRemapping: {
kind: JS.Kind.MappedTypeKeysRemapping,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBracketToken)!),
markers: emptyMarkers,
typeParameter: this.rightPadded(
{
kind: JS.Kind.MappedTypeParameter,
id: randomId(),
prefix: this.prefix(node.typeParameter),
markers: emptyMarkers,
name: this.visit(node.typeParameter.name),
iterateType: this.leftPadded(this.suffix(node.typeParameter.name), this.visit(node.typeParameter.constraint!)),
},
this.suffix(node.typeParameter)),
nameType: node.nameType && this.rightPadded(this.visit(node.nameType), this.suffix(node.nameType))
},
suffixToken: hasSuffixToken(node.questionToken) ? this.leftPadded(
this.prefix(node.questionToken!),
{
kind: J.Kind.Literal,
id: randomId(),
prefix: this.prefix(node.questionToken!),
markers: emptyMarkers,
value: undefined, // FIXME verify
valueSource: node.questionToken!.getText(),
type: this.mapPrimitiveType(node.questionToken!)
}
) : undefined,
hasQuestionToken: node.questionToken ? this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.QuestionToken)!), true) : this.leftPadded(emptySpace, false),
valueType: node.type ? {
kind: J.Kind.Container,
before: this.prefix(this.findChildNode(node, ts.SyntaxKind.ColonToken)!),
elements: [this.rightPadded(this.visit(node.type), this.suffix(node.type)),
this.findChildNode(node, ts.SyntaxKind.SemicolonToken) ?
this.rightPadded(this.newEmpty(emptySpace, markers({
kind: J.Markers.Semicolon,
id: randomId()
})), this.prefix(node.getLastToken()!))
: this.rightPadded(this.newEmpty(), this.prefix(node.getLastToken()!))
],
markers: emptyMarkers
} : {
kind: J.Kind.Container,
before: emptySpace,
elements: [this.findChildNode(node, ts.SyntaxKind.SemicolonToken) ?
this.rightPadded(this.newEmpty(this.prefix(this.findChildNode(node, ts.SyntaxKind.SemicolonToken)!), markers({
kind: J.Markers.Semicolon,
id: randomId()
})), this.prefix(node.getLastToken()!))
: this.rightPadded(this.newEmpty(), this.prefix(node.getLastToken()!))
],
markers: emptyMarkers
},
type: this.mapType(node)
};
}
visitLiteralType(node: ts.LiteralTypeNode): JS.LiteralType {
return {
kind: JS.Kind.LiteralType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
literal: this.visit(node.literal),
type: this.mapType(node)!,
};
}
// FIXME these should not be mapped as VariableDeclarations since the names can never be accessed
// and this would potentially just trip up flow analyses. The names exist purely for documentation purposes,
// they has no semantics. See https://stackoverflow.com/questions/63629315/what-are-named-or-labeled-tuples-in-typescript.
visitNamedTupleMember(node: ts.NamedTupleMember): J.VariableDeclarations {
let name: VariableDeclarator = produce(this.convert(node.name), draft => {
draft.markers = this.maybeAddOptionalMarker(draft, node);
});
if (node.dotDotDotToken) {
// Wrap in JS.Spread; use emptySpace since the whitespace belongs
// to VariableDeclarations.prefix, not to the spread itself
const spread: JS.Spread = {
kind: JS.Kind.Spread,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
expression: name as Expression,
type: this.mapType(node)
};
name = spread;
}
return {
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: [],
typeExpression: this.mapTypeInfo(node),
variables: [this.rightPadded(
{
kind: J.Kind.NamedVariable,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
name: name,
dimensionsAfterName: [],
variableType: this.mapVariableType(node),
},
this.suffix(node.name)
)],
};
}
visitTemplateLiteralType(node: ts.TemplateLiteralTypeNode): JS.TemplateExpression {
return {
kind: JS.Kind.TemplateExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
head: this.visit(node.head),
// Use emptySpace for the last span's after - any trailing whitespace belongs to the outer context
spans: node.templateSpans.map((s, i, arr) =>
this.rightPadded(this.visit(s), i === arr.length - 1 ? emptySpace : this.suffix(s))),
type: this.mapType(node)
}
}
visitTemplateLiteralTypeSpan(node: ts.TemplateLiteralTypeSpan): JS.TemplateExpression.Span {
return {
kind: JS.Kind.TemplateExpressionSpan,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.type),
tail: this.visit(node.literal)
}
}
visitImportType(node: ts.ImportTypeNode): JS.ImportType {
let importTypeAttributes = undefined;
if (node.attributes) {
const openBraceIndex = node.attributes.getChildren().findIndex(n => n.kind === ts.SyntaxKind.OpenBraceToken);
const attributes = this.mapCommaSeparatedList(node.attributes.getChildren(this.sourceFile).slice(openBraceIndex, openBraceIndex + 3));
importTypeAttributes = {
kind: JS.Kind.ImportTypeAttributes,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!),
markers: emptyMarkers,
token: this.rightPadded(
this.mapIdentifier(this.findChildNode(node, node.attributes.token)!,
ts.SyntaxKind.WithKeyword === node.attributes.token ? "with" : "assert"),
this.prefix(this.findChildNode(node, ts.SyntaxKind.ColonToken)!)),
elements: attributes,
end: this.suffix(node.attributes),
}
}
return {
kind: JS.Kind.ImportType,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
hasTypeof: node.isTypeOf ? this.rightPadded(true, this.suffix(this.findChildNode(node, ts.SyntaxKind.TypeOfKeyword)!)) : this.rightPadded(false, emptySpace),
argumentAndAttributes: {
kind: J.Kind.Container,
before: this.suffix(this.findChildNode(node, ts.SyntaxKind.ImportKeyword)!),
elements: [this.rightPadded(this.visit(node.argument), this.suffix(node.argument))].concat(importTypeAttributes ? [this.rightPadded(importTypeAttributes, this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseParenToken)!))] : []),
markers: emptyMarkers
},
qualifier: node.qualifier && this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.DotToken)!), this.visit(node.qualifier)),
typeArguments: node.typeArguments && this.mapTypeArguments(this.prefix(this.findChildNode(node, ts.SyntaxKind.LessThanToken)!), node.typeArguments),
type: this.mapType(node)
};
}
visitObjectBindingPattern(node: ts.ObjectBindingPattern): JS.ObjectBindingPattern {
return {
kind: JS.Kind.ObjectBindingPattern,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: [],
bindings: this.mapCommaSeparatedList(node.getChildren(this.sourceFile)),
initializer: undefined
};
}
visitArrayBindingPattern(node: ts.ArrayBindingPattern): JS.ArrayBindingPattern {
return {
kind: JS.Kind.ArrayBindingPattern,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
elements: this.mapCommaSeparatedList(node.getChildren(this.sourceFile)),
type: this.mapType(node)
};
}
visitBindingElement(node: ts.BindingElement): JS.BindingElement {
// Capture prefix before converting name, as convert may consume whitespace
const elementPrefix = this.prefix(node);
let name: Expression = this.convert(node.name);
if (node.dotDotDotToken) {
// Wrap in JS.Spread; use emptySpace for prefix since the whitespace
// belongs to BindingElement.prefix, not to the spread itself
name = {
kind: JS.Kind.Spread,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
expression: name,
type: this.mapType(node)
} satisfies JS.Spread as Expression;
}
return {
kind: JS.Kind.BindingElement,
id: randomId(),
prefix: elementPrefix,
markers: emptyMarkers,
propertyName: node.propertyName && this.rightPadded(this.convert(node.propertyName), this.suffix(node.propertyName)),
name: name,
initializer: node.initializer && this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.EqualsToken)!), this.convert(node.initializer)),
variableType: this.mapVariableType(node)
};
}
visitArrayLiteralExpression(node: ts.ArrayLiteralExpression): J.NewArray {
return {
kind: J.Kind.NewArray,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
dimensions: [],
initializer: this.mapCommaSeparatedList(node.getChildren(this.sourceFile)),
type: this.mapType(node)
};
}
visitObjectLiteralExpression(node: ts.ObjectLiteralExpression): J.NewClass {
return {
kind: J.Kind.NewClass,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
new: emptySpace,
arguments: emptyContainer(),
body: this.convertPropertyAssignments(node.getChildren(this.sourceFile).slice(-3)),
constructorType: this.mapMethodType(node)
};
}
private convertPropertyAssignments(nodes: ts.Node[]): J.Block {
const prefix = this.prefix(nodes[0]);
// Capture end whitespace before processing statements to prevent shorthand properties from consuming it
const end = this.prefix(nodes[nodes.length - 1]);
let statementList = nodes[1] as ts.SyntaxList;
const statements: J.RightPadded[] = this.rightPaddedSeparatedList(
[...statementList.getChildren(this.sourceFile)],
(nodes, i) => i == nodes.length - 2 && nodes[i + 1].kind === ts.SyntaxKind.CommaToken ? markers({
kind: J.Markers.TrailingComma,
id: randomId(),
suffix: this.prefix(nodes[i + 1])
} satisfies TrailingComma as TrailingComma) : emptyMarkers
);
return {
kind: J.Kind.Block,
id: randomId(),
prefix,
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements,
end
};
}
visitPropertyAccessExpression(node: ts.PropertyAccessExpression): J.FieldAccess {
return {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
target: produce(this.convert(node.expression), draft => {
if (node.questionDotToken) {
draft.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: this.suffix(node.expression)
} satisfies Optional as Optional);
}
}),
name: this.leftPadded(this.prefix(node.getChildAt(1, this.sourceFile)), this.convert(node.name)),
type: this.mapType(node)
};
}
visitElementAccessExpression(node: ts.ElementAccessExpression): J.ArrayAccess {
return {
kind: J.Kind.ArrayAccess,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
indexed: produce(this.convert(node.expression), draft => {
if (node.questionDotToken) {
draft.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: this.suffix(node.expression)
} satisfies Optional as Optional);
}
}),
dimension: {
kind: J.Kind.ArrayDimension,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBracketToken)!),
markers: emptyMarkers,
index: this.rightPadded(this.convert(node.argumentExpression), this.suffix(node.argumentExpression))
}
};
}
visitCallExpression(node: ts.CallExpression): J.MethodInvocation | JS.FunctionCall {
const prefix = this.prefix(node);
const ltToken = this.findChildNode(node, ts.SyntaxKind.LessThanToken);
const typeArguments = node.typeArguments && ltToken
? this.mapTypeArguments(this.prefix(ltToken), node.typeArguments)
: undefined;
let select: J.RightPadded | undefined;
let name: J.Identifier = {
kind: J.Kind.Identifier,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
simpleName: "",
type: undefined,
fieldType: undefined
};
if (ts.isIdentifier(node.expression) && !node.questionDotToken) {
select = undefined;
name = this.convert(node.expression);
} else if (node.questionDotToken) {
select = this.rightPadded(
produce(this.convert(node.expression), draft => {
draft.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: emptySpace,
} satisfies Optional as Optional)
}),
this.suffix(node.expression)
)
} else if (ts.isPropertyAccessExpression(node.expression)) {
select = this.rightPadded(this.visit(node.expression.expression), this.suffix(node.expression.expression));
if (node.expression.questionDotToken) {
select = produce(select, draft => {
draft!.element.markers.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: emptySpace
} as Optional);
});
}
name = this.visit(node.expression.name);
} else {
select = this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
}
if (name && name.simpleName.length > 0) {
const methodType = this.mapMethodType(node);
return {
kind: J.Kind.MethodInvocation,
id: randomId(),
prefix,
markers: emptyMarkers,
select,
typeParameters: typeArguments,
name: {
...name,
type: methodType && methodType.name.startsWith('<') ? {...methodType} : undefined
},
arguments: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(-3)),
methodType: methodType
}
} else {
return {
kind: JS.Kind.FunctionCall,
id: randomId(),
prefix,
markers: emptyMarkers,
function: select,
typeParameters: typeArguments,
arguments: this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(-3)),
methodType: this.mapMethodType(node)
}
}
}
visitSuperKeyword(node: ts.KeywordToken) {
return this.mapIdentifier(node, node.getText());
}
visitNewExpression(node: ts.NewExpression): J.NewClass {
return {
kind: J.Kind.NewClass,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
new: emptySpace,
class: node.typeArguments ? {
kind: J.Kind.ParameterizedType,
id: randomId(),
prefix: this.prefix(node.expression),
markers: emptyMarkers,
class: (() => {
// For parameterized types, we need to handle the prefix differently
const typeTree = this.mapTypeTree(node.expression);
// If it's a TypeTreeExpression, clear the prefix since it was already set on the ParameterizedType
if (typeTree.kind === JS.Kind.TypeTreeExpression) {
return {
...typeTree,
prefix: emptySpace
};
}
return typeTree;
})(),
typeParameters: this.mapTypeArguments(this.prefix(this.findChildNode(node, ts.SyntaxKind.LessThanToken)!), node.typeArguments),
type: undefined
} satisfies J.ParameterizedType as J.ParameterizedType : this.mapTypeTree(node.expression),
arguments: node.arguments ?
this.mapCommaSeparatedList(this.getParameterListNodes(node)) : {
...emptyContainer(),
markers: markers({
kind: J.Markers.OmitParentheses,
id: randomId()
})
},
constructorType: this.mapMethodType(node)
};
}
visitTaggedTemplateExpression(node: ts.TaggedTemplateExpression): JS.TaggedTemplateExpression {
return {
kind: JS.Kind.TaggedTemplateExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
tag: this.rightPadded(this.visit(node.tag), this.suffix(node.tag)),
typeArguments: node.typeArguments && this.mapTypeArguments(emptySpace, node.typeArguments),
templateExpression: this.convert(node.template),
type: this.mapType(node)
}
}
visitTypeAssertionExpression(node: ts.TypeAssertion): J.TypeCast {
return {
kind: J.Kind.TypeCast,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
class: {
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(node.getFirstToken()!),
markers: emptyMarkers,
tree: this.rightPadded(this.convert(node.type), this.prefix(node.getChildAt(2, this.sourceFile)))
},
expression: this.convert(node.expression),
};
}
visitParenthesizedExpression(node: ts.ParenthesizedExpression): J.Parentheses {
return {
kind: J.Kind.Parentheses,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
tree: this.rightPadded(this.convert(node.expression), this.prefix(node.getLastToken()!))
};
}
visitArrowFunction(node: ts.ArrowFunction): JS.ArrowFunction {
const openParenToken = this.findChildNode(node, ts.SyntaxKind.OpenParenToken);
const isParenthesized = openParenToken != undefined;
return {
kind: JS.Kind.ArrowFunction,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: this.mapModifiers(node),
typeParameters: node.typeParameters && this.mapTypeParametersAsObject(node),
lambda: {
kind: J.Kind.Lambda,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
parameters: {
kind: J.Kind.LambdaParameters,
id: randomId(),
prefix: isParenthesized ? this.prefix(openParenToken) : emptySpace,
markers: emptyMarkers,
parenthesized: isParenthesized,
parameters: node.parameters.length > 0 ?
node.parameters.map(p => this.rightPadded(this.convert(p), this.suffix(p)))
.concat(node.parameters.hasTrailingComma ? this.rightPadded(this.newEmpty(), this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseParenToken)!)) : []) :
isParenthesized ? [this.rightPadded(this.newEmpty(), this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseParenToken)!))] : [],
},
arrow: this.prefix(node.equalsGreaterThanToken),
body: this.convert(node.body),
type: this.mapType(node)
},
returnTypeExpression: this.mapTypeInfo(node)
};
}
visitDeleteExpression(node: ts.DeleteExpression): JS.Delete {
return {
kind: JS.Kind.Delete,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression)
};
}
visitTypeOfExpression(node: ts.TypeOfExpression): JS.TypeOf {
return {
kind: JS.Kind.TypeOf,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression),
type: this.mapType(node)
};
}
visitVoidExpression(node: ts.VoidExpression): JS.Void {
return {
kind: JS.Kind.Void,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression)
};
}
visitAwaitExpression(node: ts.AwaitExpression): JS.Await {
return {
kind: JS.Kind.Await,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression),
type: this.mapType(node)
};
}
visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression): J.Unary | J.Unknown {
let unaryOperator: J.Unary.Type | undefined;
switch (node.operator) {
case ts.SyntaxKind.PlusToken:
unaryOperator = J.Unary.Type.Positive;
break;
case ts.SyntaxKind.MinusToken:
unaryOperator = J.Unary.Type.Negative;
break;
case ts.SyntaxKind.ExclamationToken:
unaryOperator = J.Unary.Type.Not;
break;
case ts.SyntaxKind.PlusPlusToken:
unaryOperator = J.Unary.Type.PreIncrement;
break;
case ts.SyntaxKind.MinusMinusToken:
unaryOperator = J.Unary.Type.PreDecrement;
break;
case ts.SyntaxKind.TildeToken:
unaryOperator = J.Unary.Type.Complement;
}
if (unaryOperator === undefined) {
return this.visitUnknown(node);
}
return {
kind: J.Kind.Unary,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
operator: this.leftPadded(this.prefix(node.getFirstToken()!), unaryOperator),
expression: this.convert(node.operand),
type: this.mapType(node)
};
}
visitPostfixUnaryExpression(node: ts.PostfixUnaryExpression): J.Unary | J.Unknown {
let unaryOperator: J.Unary.Type | undefined;
switch (node.operator) {
case ts.SyntaxKind.PlusPlusToken:
unaryOperator = J.Unary.Type.PostIncrement;
break;
case ts.SyntaxKind.MinusMinusToken:
unaryOperator = J.Unary.Type.PostDecrement;
break;
}
if (unaryOperator === undefined) {
return this.visitUnknown(node);
}
return {
kind: J.Kind.Unary,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
operator: this.leftPadded(this.suffix(node.operand), unaryOperator),
expression: this.convert(node.operand),
type: this.mapType(node),
};
}
visitBinaryExpression(node: ts.BinaryExpression): J.Binary | J.AssignmentOperation | JS.AssignmentOperation | J.Assignment | JS.Binary | J.InstanceOf | J.Unknown {
if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
// assignment is also represented as `ts.BinaryExpression`
return {
kind: J.Kind.Assignment,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
variable: this.convert(node.left),
assignment: this.leftPadded(this.suffix(node.left), this.convert(node.right)),
type: this.mapType(node)
};
}
let binaryOperator: J.Binary.Type | JS.Binary.Type | undefined;
switch (node.operatorToken.kind) {
case ts.SyntaxKind.EqualsEqualsEqualsToken:
binaryOperator = JS.Binary.Type.IdentityEquals;
break;
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
binaryOperator = JS.Binary.Type.IdentityNotEquals;
break;
case ts.SyntaxKind.QuestionQuestionToken:
binaryOperator = JS.Binary.Type.QuestionQuestion;
break;
case ts.SyntaxKind.InKeyword:
binaryOperator = JS.Binary.Type.In;
break;
case ts.SyntaxKind.CommaToken:
binaryOperator = JS.Binary.Type.Comma;
break;
}
if (binaryOperator !== undefined) {
return {
kind: JS.Kind.Binary,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
left: this.convert(node.left),
operator: this.leftPadded(this.prefix(node.operatorToken), binaryOperator as JS.Binary.Type),
right: this.convert(node.right),
type: this.mapType(node),
};
}
if (node.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
return {
kind: J.Kind.InstanceOf,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.rightPadded(this.convert(node.left), this.prefix(node.operatorToken)),
class: this.convert(node.right),
type: this.mapType(node),
};
}
binaryOperator = this.mapBinaryOperator(node);
if (binaryOperator === undefined) {
let assignmentOperation;
switch (node.operatorToken.kind) {
case ts.SyntaxKind.QuestionQuestionEqualsToken:
assignmentOperation = JS.AssignmentOperation.Type.QuestionQuestion;
break;
case ts.SyntaxKind.AmpersandAmpersandEqualsToken:
assignmentOperation = JS.AssignmentOperation.Type.And;
break;
case ts.SyntaxKind.BarBarEqualsToken:
assignmentOperation = JS.AssignmentOperation.Type.Or;
break;
case ts.SyntaxKind.AsteriskAsteriskToken:
assignmentOperation = JS.AssignmentOperation.Type.Power;
break;
case ts.SyntaxKind.AsteriskAsteriskEqualsToken:
assignmentOperation = JS.AssignmentOperation.Type.Exp;
break;
}
if (assignmentOperation !== undefined) {
return {
kind: JS.Kind.AssignmentOperation,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
variable: this.convert(node.left),
operator: this.leftPadded(this.prefix(node.operatorToken), assignmentOperation),
assignment: this.convert(node.right),
type: this.mapType(node),
}
}
assignmentOperation = this.mapAssignmentOperation(node);
if (assignmentOperation === undefined) {
return this.visitUnknown(node);
}
return {
kind: J.Kind.AssignmentOperation,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
variable: this.convert(node.left),
operator: this.leftPadded(this.prefix(node.operatorToken), assignmentOperation),
assignment: this.convert(node.right),
type: this.mapType(node)
}
}
return {
kind: J.Kind.Binary,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
left: this.convert(node.left),
operator: this.leftPadded(this.prefix(node.operatorToken), binaryOperator),
right: this.convert(node.right),
type: this.mapType(node)
}
}
private mapBinaryOperator(node: ts.BinaryExpression): J.Binary.Type | undefined {
switch (node.operatorToken.kind) {
case ts.SyntaxKind.PlusToken:
return J.Binary.Type.Addition;
case ts.SyntaxKind.MinusToken:
return J.Binary.Type.Subtraction;
case ts.SyntaxKind.AsteriskToken:
return J.Binary.Type.Multiplication;
case ts.SyntaxKind.SlashToken:
return J.Binary.Type.Division;
case ts.SyntaxKind.PercentToken:
return J.Binary.Type.Modulo;
case ts.SyntaxKind.LessThanLessThanToken:
return J.Binary.Type.LeftShift;
case ts.SyntaxKind.GreaterThanGreaterThanToken:
return J.Binary.Type.RightShift;
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return J.Binary.Type.UnsignedRightShift;
// case ts.SyntaxKind.LessThanLessThanEqualsToken:
// return J.Binary.Type.LeftShiftEquals;
// case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken:
// return J.Binary.Type.RightShiftEquals;
case ts.SyntaxKind.AmpersandToken:
return J.Binary.Type.BitAnd;
// case ts.SyntaxKind.AmpersandEqualsToken:
// return J.Binary.Type.BitwiseAndEquals;
case ts.SyntaxKind.BarToken:
return J.Binary.Type.BitOr;
// case ts.SyntaxKind.BarEqualsToken:
// return J.Binary.Type.BitwiseOrEquals;
case ts.SyntaxKind.CaretToken:
return J.Binary.Type.BitXor;
// case ts.SyntaxKind.CaretEqualsToken:
// return J.Binary.Type.BitwiseXorEquals;
case ts.SyntaxKind.EqualsEqualsToken:
return J.Binary.Type.Equal;
// case ts.SyntaxKind.EqualsEqualsEqualsToken:
// return J.Binary.Type.StrictEquals;
case ts.SyntaxKind.ExclamationEqualsToken:
return J.Binary.Type.NotEqual;
// case ts.SyntaxKind.ExclamationEqualsEqualsToken:
// return J.Binary.Type.StrictNotEquals;
case ts.SyntaxKind.LessThanToken:
return J.Binary.Type.LessThan;
case ts.SyntaxKind.LessThanEqualsToken:
return J.Binary.Type.LessThanOrEqual;
case ts.SyntaxKind.GreaterThanToken:
return J.Binary.Type.GreaterThan;
case ts.SyntaxKind.GreaterThanEqualsToken:
return J.Binary.Type.GreaterThanOrEqual;
case ts.SyntaxKind.AmpersandAmpersandToken:
return J.Binary.Type.And;
case ts.SyntaxKind.BarBarToken:
return J.Binary.Type.Or;
// case ts.SyntaxKind.BarBarEqualsToken:
// return J.Binary.Type.OrEquals;
// case ts.SyntaxKind.AmpersandEqualsToken:
// return J.Binary.Type.AndEquals;
}
return undefined;
}
private mapAssignmentOperation(node: ts.BinaryExpression): J.AssignmentOperation.Type | undefined {
switch (node.operatorToken.kind) {
case ts.SyntaxKind.PlusEqualsToken:
return J.AssignmentOperation.Type.Addition;
case ts.SyntaxKind.MinusEqualsToken:
return J.AssignmentOperation.Type.Subtraction;
case ts.SyntaxKind.AsteriskEqualsToken:
return J.AssignmentOperation.Type.Multiplication;
case ts.SyntaxKind.SlashEqualsToken:
return J.AssignmentOperation.Type.Division;
case ts.SyntaxKind.PercentEqualsToken:
return J.AssignmentOperation.Type.Modulo;
case ts.SyntaxKind.LessThanLessThanEqualsToken:
return J.AssignmentOperation.Type.LeftShift;
case ts.SyntaxKind.GreaterThanGreaterThanEqualsToken:
return J.AssignmentOperation.Type.RightShift;
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken:
return J.AssignmentOperation.Type.UnsignedRightShift;
case ts.SyntaxKind.AmpersandEqualsToken:
return J.AssignmentOperation.Type.BitAnd;
case ts.SyntaxKind.BarEqualsToken:
return J.AssignmentOperation.Type.BitOr;
case ts.SyntaxKind.CaretEqualsToken:
return J.AssignmentOperation.Type.BitXor;
// case ts.SyntaxKind.AmpersandAmpersandEqualsToken:
// return J.AssignmentOperation.Type.And;
// case ts.SyntaxKind.BarBarEqualsToken:
// return J.AssignmentOperation.Type.Or;
// case ts.SyntaxKind.BarBarEqualsToken:
// return J.Binary.Type.OrEquals;
// case ts.SyntaxKind.AmpersandEqualsToken:
// return J.Binary.Type.AndEquals;
}
return undefined;
}
visitConditionalExpression(node: ts.ConditionalExpression): J.Ternary {
return {
kind: J.Kind.Ternary,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
condition: this.convert(node.condition),
truePart: this.leftPadded(this.suffix(node.condition), this.convert(node.whenTrue)),
falsePart: this.leftPadded(this.suffix(node.whenTrue), this.convert(node.whenFalse)),
type: this.mapType(node)
};
}
visitTemplateExpression(node: ts.TemplateExpression): JS.TemplateExpression {
return {
kind: JS.Kind.TemplateExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
head: this.visit(node.head),
// Use emptySpace for the last span's after - any trailing whitespace belongs to the outer context
spans: node.templateSpans.map((s, i, arr) =>
this.rightPadded(this.visit(s), i === arr.length - 1 ? emptySpace : this.suffix(s))),
type: this.mapType(node)
}
}
visitYieldExpression(node: ts.YieldExpression): JS.StatementExpression {
return {
kind: JS.Kind.StatementExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
statement: {
kind: J.Kind.Yield,
id: randomId(),
prefix: emptySpace,
markers: node.asteriskToken ?
markers({
kind: JS.Markers.DelegatedYield,
id: randomId(),
prefix: this.prefix(node.asteriskToken)
} satisfies DelegatedYield as DelegatedYield) : emptyMarkers,
value: node.expression && this.visit(node.expression),
implicit: false
} satisfies J.Yield as J.Yield
};
}
visitSpreadElement(node: ts.SpreadElement): JS.Spread {
return {
kind: JS.Kind.Spread,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression),
type: this.mapType(node)
};
}
visitClassExpression(node: ts.ClassExpression): JS.StatementExpression {
return {
kind: JS.Kind.StatementExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
statement: {
kind: J.Kind.ClassDeclaration,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
leadingAnnotations: this.mapDecorators(node),
modifiers: [],
classKind: {
kind: J.Kind.ClassDeclarationKind,
id: randomId(),
prefix: node.modifiers ? this.suffix(node.modifiers[node.modifiers.length - 1]) : this.prefix(node),
markers: emptyMarkers,
annotations: [],
type: J.ClassDeclaration.Kind.Type.Class
},
name: node.name ? this.convert(node.name) : this.mapIdentifier(node, ""),
typeParameters: this.mapTypeParametersAsContainer(node),
extends: this.mapExtends(node),
implements: this.mapImplements(node),
body: {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: node.members.map(ce => ({
kind: J.Kind.RightPadded,
element: this.convert(ce),
after: ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? this.prefix(ce.getLastToken()!) : emptySpace,
markers: ce.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
})),
end: this.prefix(node.getLastToken()!)
},
type: this.mapDeclarationType(node)
} satisfies J.ClassDeclaration as J.ClassDeclaration,
}
}
visitOmittedExpression(node: ts.OmittedExpression) {
return this.newEmpty(this.prefix(node));
}
visitExpressionWithTypeArguments(node: ts.ExpressionWithTypeArguments): JS.ExpressionWithTypeArguments | Expression {
if (node.typeArguments) {
return {
kind: JS.Kind.ExpressionWithTypeArguments,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
clazz: this.visit(node.expression),
typeArguments: this.mapTypeArguments(this.suffix(node.expression), node.typeArguments),
type: this.mapType(node)
}
}
return this.visit(node.expression);
}
visitAsExpression(node: ts.AsExpression): JS.As {
return {
kind: JS.Kind.As,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
left: this.rightPadded(this.convert(node.expression), this.prefix(node.getChildAt(1, this.sourceFile))),
right: this.convert(node.type),
type: this.mapType(node),
};
}
visitNonNullExpression(node: ts.NonNullExpression): Expression {
return produce(this.visit(node.expression) as Expression, draft => {
draft.markers.markers.push({
kind: JS.Markers.NonNullAssertion,
id: randomId(),
prefix: this.suffix(node.expression)
} satisfies NonNullAssertion as NonNullAssertion);
});
}
visitMetaProperty(node: ts.MetaProperty): J.FieldAccess {
return {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
target: node.keywordToken === ts.SyntaxKind.NewKeyword ? this.mapIdentifier(node, 'new') : this.mapIdentifier(node, 'import'),
name: this.leftPadded(this.prefix(node.getChildAt(1, this.sourceFile)), this.convert(node.name)),
type: this.mapType(node)
};
}
visitSyntheticExpression(node: ts.SyntheticExpression): J.Unknown {
// SyntheticExpression is a special type of node used internally by the TypeScript compiler
return this.visitUnknown(node);
}
visitSatisfiesExpression(node: ts.SatisfiesExpression): JS.SatisfiesExpression {
return {
kind: JS.Kind.SatisfiesExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.visit(node.expression),
satisfiesType: this.leftPadded(this.suffix(node.expression), this.visit(node.type)),
type: this.mapType(node)
};
}
visitTemplateSpan(node: ts.TemplateSpan): JS.TemplateExpression.Span {
return {
kind: JS.Kind.TemplateExpressionSpan,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression),
tail: this.visit(node.literal)
}
}
visitSemicolonClassElement(node: ts.SemicolonClassElement): J.Empty {
return this.newEmpty(this.semicolonPrefix(node));
}
visitBlock(node: ts.Block): J.Block {
return {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: this.semicolonPaddedStatementList(node.statements),
end: this.prefix(node.getLastToken()!)
};
}
visitEmptyStatement(node: ts.EmptyStatement): J.Empty {
return this.newEmpty(this.prefix(node));
}
visitVariableStatement(node: ts.VariableStatement): JS.ScopedVariableDeclarations | J.VariableDeclarations {
const prefix = this.prefix(node);
return produce(this.visitVariableDeclarationList(node.declarationList), draft => {
draft.prefix = prefix;
draft.modifiers = this.mapModifiers(node).concat(draft.modifiers);
});
}
visitExpressionStatement(node: ts.ExpressionStatement): Statement {
const expression: Expression = this.visit(node.expression) as Expression;
if (isStatement(expression)) {
return expression as Statement;
}
const e: Expression = expression;
return {
kind: JS.Kind.ExpressionStatement,
id: randomId(),
prefix: e.prefix,
markers: emptyMarkers,
expression: produce(e, draft => {
draft.prefix = emptySpace
})
} satisfies JS.ExpressionStatement as JS.ExpressionStatement;
}
visitIfStatement(node: ts.IfStatement): J.If {
const semicolonAfterThen = (node.thenStatement.getChildAt(node.thenStatement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken);
const semicolonAfterElse = (node.elseStatement?.getChildAt(node.elseStatement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken);
return {
kind: J.Kind.If,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
ifCondition: {
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
tree: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
},
thenPart: this.rightPadded(
this.convert(node.thenStatement),
semicolonAfterThen ? this.prefix(node.thenStatement.getLastToken()!) : emptySpace,
semicolonAfterThen ? markers({kind: J.Markers.Semicolon, id: randomId()}) : emptyMarkers
),
elsePart: node.elseStatement && {
kind: J.Kind.IfElse,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.ElseKeyword)!),
markers: emptyMarkers,
body: this.rightPadded(
this.convert(node.elseStatement),
semicolonAfterElse ? this.prefix(node.elseStatement.getLastToken()!) : emptySpace,
semicolonAfterElse ? markers({kind: J.Markers.Semicolon, id: randomId()}) : emptyMarkers
)
}
};
}
visitDoStatement(node: ts.DoStatement): J.DoWhileLoop {
return {
kind: J.Kind.DoWhileLoop,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
body: this.rightPadded(this.visit(node.statement),
this.semicolonPrefix(node.statement),
node.statement.getChildAt(node.statement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers),
whileCondition: this.leftPadded(
this.prefix(this.findChildNode(node, ts.SyntaxKind.WhileKeyword)!),
{
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
tree: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
}
)
};
}
visitWhileStatement(node: ts.WhileStatement): J.WhileLoop {
return {
kind: J.Kind.WhileLoop,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
condition: {
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
tree: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
},
body: this.rightPadded(
this.convert(node.statement),
this.semicolonPrefix(node.statement),
node.statement.getChildAt(node.statement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
),
};
}
visitForStatement(node: ts.ForStatement): J.ForLoop {
return {
kind: J.Kind.ForLoop,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
control: {
kind: J.Kind.ForLoopControl,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
init: [node.initializer ?
(ts.isVariableDeclarationList(node.initializer) ? this.rightPadded(this.visit(node.initializer), this.suffix(node.initializer)) :
this.rightPadded(ts.isStatement(node.initializer) ? this.visit(node.initializer) : {
kind: JS.Kind.ExpressionStatement,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
expression: this.visit(node.initializer)
}, this.suffix(node.initializer))) :
this.rightPadded(this.newEmpty(), this.suffix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!))],
condition: node.condition ? this.rightPadded(this.visit(node.condition), this.suffix(node.condition)) :
this.rightPadded(this.newEmpty(), this.suffix(this.findChildNode(node, ts.SyntaxKind.SemicolonToken)!)),
update: [node.incrementor ? this.rightPadded(ts.isStatement(node.incrementor) ? this.visit(node.incrementor) : {
kind: JS.Kind.ExpressionStatement,
id: randomId(),
prefix: this.prefix(node.incrementor),
markers: emptyMarkers,
expression: this.visit(node.incrementor)
}, this.suffix(node.incrementor)) :
this.rightPadded(this.newEmpty(this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseParenToken)!)), emptySpace)]
},
body: this.rightPadded(
this.convert(node.statement),
this.semicolonPrefix(node.statement),
node.statement.getChildAt(node.statement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
)
};
}
visitForInStatement(node: ts.ForInStatement): JS.ForInLoop {
return {
kind: JS.Kind.ForInLoop,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
control: {
kind: J.Kind.ForEachLoopControl,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
variable: (() => {
if (ts.isIdentifier(node.initializer)) {
const ident = this.visit(node.initializer);
return this.rightPadded({
kind: JS.Kind.ExpressionStatement,
id: randomId(),
expression: ident,
markers: emptyMarkers,
prefix: emptySpace
}, this.suffix(node.initializer));
} else {
return this.rightPadded(this.visit(node.initializer), this.suffix(node.initializer))
}
})(),
iterable: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
},
body: this.rightPadded(
this.convert(node.statement),
this.semicolonPrefix(node.statement),
node.statement.getChildAt(node.statement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
)
};
}
visitForOfStatement(node: ts.ForOfStatement): JS.ForOfLoop {
return {
kind: JS.Kind.ForOfLoop,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
await: node.awaitModifier && this.prefix(node.awaitModifier),
loop: {
kind: J.Kind.ForEachLoop,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
control: {
kind: J.Kind.ForEachLoopControl,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
variable: (() => {
if (ts.isIdentifier(node.initializer)) {
const ident = this.visit(node.initializer);
return this.rightPadded({
kind: JS.Kind.ExpressionStatement,
id: randomId(),
expression: ident,
markers: emptyMarkers,
prefix: emptySpace
}, this.suffix(node.initializer));
} else if (ts.isArrayLiteralExpression(node.initializer)) {
return this.rightPadded({
kind: JS.Kind.ExpressionStatement,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
expression: {
kind: JS.Kind.ArrayBindingPattern,
id: randomId(),
elements: {
kind: J.Kind.Container,
before: emptySpace,
elements: node.initializer.elements.map(e => this.rightPadded(this.visit(e) as Expression, this.suffix(e))),
markers: emptyMarkers
} satisfies J.Container as J.Container,
markers: emptyMarkers,
prefix: emptySpace
} satisfies JS.ArrayBindingPattern as JS.ArrayBindingPattern,
} satisfies JS.ExpressionStatement as JS.ExpressionStatement, this.suffix(node.initializer));
} else if (ts.isObjectLiteralExpression(node.initializer)) {
return this.rightPadded({
kind: JS.Kind.ObjectBindingPattern,
id: randomId(),
leadingAnnotations: [],
modifiers: [],
typeExpression: undefined,
initializer: undefined,
bindings: {
kind: J.Kind.Container,
before: emptySpace,
elements: node.initializer.properties.map(p => this.rightPadded(this.visit(p) as Expression, this.suffix(p))),
markers: emptyMarkers
},
markers: emptyMarkers,
prefix: emptySpace
}, this.suffix(node.initializer))
} else {
return this.rightPadded(this.visit(node.initializer), this.suffix(node.initializer))
}
})(),
iterable: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
},
body: this.rightPadded(
this.convert(node.statement),
this.semicolonPrefix(node.statement),
node.statement.getChildAt(node.statement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
)
}
};
}
visitContinueStatement(node: ts.ContinueStatement): J.Continue {
return {
kind: J.Kind.Continue,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
label: node.label && this.visit(node.label)
};
}
visitBreakStatement(node: ts.BreakStatement): J.Break {
return {
kind: J.Kind.Break,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
label: node.label && this.visit(node.label)
};
}
visitReturnStatement(node: ts.ReturnStatement): J.Return {
return {
kind: J.Kind.Return,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: node.expression && this.convert(node.expression)
};
}
visitWithStatement(node: ts.WithStatement): JS.WithStatement {
return {
kind: JS.Kind.WithStatement,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: {
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
tree: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
},
body: this.rightPadded(
this.convert(node.statement),
this.semicolonPrefix(node.statement),
node.statement.getChildAt(node.statement.getChildCount() - 1)?.kind === ts.SyntaxKind.SemicolonToken ? markers({
kind: J.Markers.Semicolon,
id: randomId()
}) : emptyMarkers
),
};
}
visitSwitchStatement(node: ts.SwitchStatement): J.Switch {
return {
kind: J.Kind.Switch,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
selector: {
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
tree: this.rightPadded(this.visit(node.expression), this.suffix(node.expression))
},
cases: this.visit(node.caseBlock)
};
}
visitLabeledStatement(node: ts.LabeledStatement): J.Label {
return {
kind: J.Kind.Label,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
label: this.rightPadded(this.visit(node.label), this.suffix(node.label)),
statement: this.visit(node.statement),
};
}
visitThrowStatement(node: ts.ThrowStatement): J.Throw {
return {
kind: J.Kind.Throw,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
exception: this.visit(node.expression)
};
}
visitTryStatement(node: ts.TryStatement): J.Try {
return {
kind: J.Kind.Try,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
body: this.visit(node.tryBlock),
catches: node.catchClause ? [this.visit(node.catchClause)] : [],
finally: node.finallyBlock && this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.FinallyKeyword)!), this.visit(node.finallyBlock))
};
}
visitDebuggerStatement(node: ts.DebuggerStatement): JS.ExpressionStatement {
return {
kind: JS.Kind.ExpressionStatement,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
expression: this.mapIdentifier(node, 'debugger')
};
}
visitVariableDeclaration(node: ts.VariableDeclaration): J.VariableDeclarations.NamedVariable {
return {
kind: J.Kind.NamedVariable,
id: randomId(),
prefix: this.prefix(node),
markers: produce(emptyMarkers, draft => {
if (node.exclamationToken) {
draft.markers.push({
kind: JS.Markers.NonNullAssertion,
id: randomId(),
prefix: this.suffix(node.name)
} satisfies NonNullAssertion as NonNullAssertion);
}
}),
name: this.visit(node.name),
dimensionsAfterName: [],
initializer: node.initializer && this.leftPadded(this.prefix(node.getChildAt(node.getChildCount(this.sourceFile) - 2)), this.visit(node.initializer)),
variableType: this.mapVariableType(node),
};
}
visitVariableDeclarationList(node: ts.VariableDeclarationList): J.VariableDeclarations | JS.ScopedVariableDeclarations {
let kind = node.getFirstToken();
// to parse the declaration case: await using db = ...
let modifiers: J.Modifier[] = [];
if (kind?.kind === ts.SyntaxKind.AwaitKeyword) {
modifiers.push({
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(kind),
markers: emptyMarkers,
keyword: 'await',
type: J.ModifierType.LanguageExtension,
annotations: []
});
kind = node.getChildAt(1);
}
if (kind?.kind === ts.SyntaxKind.VarKeyword ||
kind?.kind === ts.SyntaxKind.LetKeyword ||
kind?.kind === ts.SyntaxKind.ConstKeyword ||
kind?.kind === ts.SyntaxKind.UsingKeyword) {
modifiers.push({
kind: J.Kind.Modifier,
id: randomId(),
prefix: modifiers.length === 0 ? this.prefix(kind) : this.prefix(kind),
markers: emptyMarkers,
annotations: [],
keyword: kind.kind === ts.SyntaxKind.VarKeyword ? 'var' :
kind.kind === ts.SyntaxKind.LetKeyword ? 'let' :
kind.kind === ts.SyntaxKind.ConstKeyword ? 'const' : 'using',
type: kind.kind == ts.SyntaxKind.ConstKeyword ? J.ModifierType.Final : J.ModifierType.LanguageExtension
});
}
const isMulti = node.declarations.length > 1;
const varDecls = node.declarations.map(declaration => {
return this.rightPadded({
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: isMulti ? this.prefix(declaration) : emptySpace,
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: isMulti ? [] : modifiers,
typeExpression: this.mapTypeInfo(declaration),
variables: [this.rightPadded({
kind: J.Kind.NamedVariable,
id: randomId(),
prefix: isMulti ? emptySpace : this.prefix(declaration),
markers: produce(emptyMarkers, draft => {
if (declaration.exclamationToken) {
draft.markers.push({
kind: JS.Markers.NonNullAssertion,
id: randomId(),
prefix: this.suffix(declaration.name)
} satisfies NonNullAssertion as NonNullAssertion);
}
}),
name: this.visit(declaration.name),
dimensionsAfterName: [],
initializer: (() => {
if (declaration.initializer) {
const equalSign = declaration.getChildren().find(c => c.kind == ts.SyntaxKind.EqualsToken)
const prefix = equalSign && this.prefix(equalSign);
return this.leftPadded(prefix ?? emptySpace, this.visit(declaration.initializer) as Expression)
} else {
return undefined;
}
})(),
variableType: this.mapVariableType(declaration)
} satisfies J.VariableDeclarations.NamedVariable as J.VariableDeclarations.NamedVariable, emptySpace)]
} satisfies J.VariableDeclarations as J.VariableDeclarations, isMulti ? this.suffix(declaration) : emptySpace)
});
if (varDecls.length === 1) {
return varDecls[0].element;
} else {
return {
kind: JS.Kind.ScopedVariableDeclarations,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
modifiers: modifiers,
variables: varDecls
};
}
}
visitFunctionDeclaration(node: ts.FunctionDeclaration): J.MethodDeclaration {
return this.mapFunctionDeclaration(node);
}
visitFunctionExpression(node: ts.FunctionExpression): JS.StatementExpression {
const delegate = this.mapFunctionDeclaration(node);
return {
kind: JS.Kind.StatementExpression,
id: randomId(),
prefix: delegate.prefix,
markers: emptyMarkers,
statement: produce(delegate, draft => {
draft.prefix = emptySpace;
})
};
}
private mapFunctionDeclaration(node: ts.FunctionDeclaration | ts.FunctionExpression): J.MethodDeclaration {
const methodType = this.mapMethodType(node);
return {
kind: J.Kind.MethodDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: produce(emptyMarkers, draft => {
draft.markers.push({
kind: JS.Markers.FunctionDeclaration,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.FunctionKeyword)!)
} satisfies FunctionDeclaration as FunctionDeclaration);
if (node.asteriskToken) {
draft.markers.push({
kind: JS.Markers.Generator,
id: randomId(),
prefix: this.prefix(node.asteriskToken)
} satisfies Generator as Generator);
}
}),
leadingAnnotations: [],
nameAnnotations: [],
modifiers: this.mapModifiers(node),
name: this.mapMethodName(node, methodType) as J.Identifier,
typeParameters: this.mapTypeParametersAsObject(node),
parameters: this.mapCommaSeparatedList(this.getParameterListNodes(node)),
returnTypeExpression: this.mapTypeInfo(node),
dimensionsAfterName: [],
body: node.body && this.convert(node.body),
methodType: methodType
};
}
private getParameterListNodes(node: ts.SignatureDeclarationBase | ts.NewExpression, openToken: ts.SyntaxKind = ts.SyntaxKind.OpenParenToken) {
const children = node.getChildren(this.sourceFile);
for (let i = 0; i < children.length; i++) {
if (children[i].kind === openToken) {
return children.slice(i, i + 3);
}
}
return [];
}
visitInterfaceDeclaration(node: ts.InterfaceDeclaration): J.ClassDeclaration {
return {
kind: J.Kind.ClassDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: this.mapModifiers(node),
classKind: {
kind: J.Kind.ClassDeclarationKind,
id: randomId(),
prefix: node.modifiers ? this.suffix(node.modifiers[node.modifiers.length - 1]) : this.prefix(node),
markers: emptyMarkers,
annotations: [],
type: J.ClassDeclaration.Kind.Type.Interface
},
name: node.name ? this.convert(node.name) : this.mapIdentifier(node, ""),
typeParameters: this.mapTypeParametersAsContainer(node),
implements: this.mapInterfaceExtends(node),
body: {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: node.members.map(te => ({
kind: J.Kind.RightPadded,
element: this.convert(te),
after: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? this.prefix(te.getLastToken()!) : emptySpace,
markers: (te.getLastToken()?.kind === ts.SyntaxKind.SemicolonToken) || (te.getLastToken()?.kind === ts.SyntaxKind.CommaToken) ? markers(this.convertToken(te.getLastToken())!) : emptyMarkers
})),
end: this.prefix(node.getLastToken()!)
},
type: this.mapDeclarationType(node)
};
}
visitTypeAliasDeclaration(node: ts.TypeAliasDeclaration): JS.TypeDeclaration {
return {
kind: JS.Kind.TypeDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: this.mapModifiers(node),
name: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.TypeKeyword)!), this.visit(node.name)),
typeParameters: node.typeParameters && this.mapTypeParametersAsObject(node),
initializer: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.EqualsToken)!), this.convert(node.type)),
type: this.mapType(node)
};
}
visitEnumDeclaration(node: ts.EnumDeclaration): J.ClassDeclaration {
return {
kind: J.Kind.ClassDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: this.mapModifiers(node),
classKind: {
kind: J.Kind.ClassDeclarationKind,
id: randomId(),
prefix: node.modifiers ? this.suffix(node.modifiers[node.modifiers.length - 1]) : this.prefix(node),
markers: emptyMarkers,
annotations: [],
type: J.ClassDeclaration.Kind.Type.Enum
},
name: node.name ? this.convert(node.name) : this.mapIdentifier(node, ""),
body: {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenBraceToken)!),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: [this.rightPadded(
{
kind: J.Kind.EnumValueSet,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
enums: node.members.map((em, i) => {
const isLast = i === node.members.length - 1;
if (isLast && !node.members.hasTrailingComma) {
// No trailing comma - don't consume suffix, it belongs to block's end prefix
return this.rightPadded(this.visit(em), emptySpace);
}
// For non-last members, or last member with trailing comma, consume the suffix
return this.rightPadded(this.visit(em), this.suffix(em));
}),
terminatedWithSemicolon: node.members.hasTrailingComma
},
emptySpace)],
end: this.prefix(node.getLastToken()!)
},
type: this.mapDeclarationType(node) as Type.Class
};
}
visitModuleDeclaration(node: ts.ModuleDeclaration): JS.NamespaceDeclaration {
const body = node.body && this.visit(node.body as ts.Node);
let namespaceKeyword = this.findChildNode(node, ts.SyntaxKind.NamespaceKeyword) ?? this.findChildNode(node, ts.SyntaxKind.ModuleKeyword);
let keywordType: JS.NamespaceDeclaration.KeywordType;
if (namespaceKeyword == undefined) {
keywordType = JS.NamespaceDeclaration.KeywordType.Empty;
} else if (namespaceKeyword?.kind === ts.SyntaxKind.NamespaceKeyword) {
keywordType = JS.NamespaceDeclaration.KeywordType.Namespace;
} else {
keywordType = JS.NamespaceDeclaration.KeywordType.Module;
}
if (body?.kind === JS.Kind.NamespaceDeclaration) {
return {
kind: JS.Kind.NamespaceDeclaration,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
modifiers: this.mapModifiers(node),
keywordType: this.leftPadded(
namespaceKeyword ? this.prefix(namespaceKeyword) : emptySpace,
keywordType
),
name: this.rightPadded(
(body.name.element.kind === J.Kind.FieldAccess)
? this.remapFieldAccess(body.name.element, node.name)
: {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
target: this.visit(node.name),
name: {
kind: J.Kind.LeftPadded,
before: this.suffix(node.name),
element: body.name.element as J.Identifier,
markers: emptyMarkers
},
type: undefined
},
body.name.after
),
body: body.body
};
} else {
return {
kind: JS.Kind.NamespaceDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: this.mapModifiers(node),
keywordType: this.leftPadded(
namespaceKeyword ? this.prefix(namespaceKeyword) : emptySpace,
keywordType
),
name: this.rightPadded(this.convert(node.name), this.suffix(node.name)),
body
};
}
}
private remapFieldAccess(fa: J.FieldAccess, name: ts.ModuleName): J.FieldAccess {
if (fa.target.kind === J.Kind.Identifier) {
return {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
target: {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
target: this.visit(name),
name: this.leftPadded(
this.suffix(name),
fa.target as J.Identifier
),
type: undefined
} satisfies J.FieldAccess as J.FieldAccess,
name: fa.name,
type: undefined
};
}
return {
kind: J.Kind.FieldAccess,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
target: this.remapFieldAccess(fa.target as J.FieldAccess, name),
name: fa.name,
type: undefined
};
}
visitModuleBlock(node: ts.ModuleBlock): J.Block {
return {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: this.semicolonPaddedStatementList(node.statements),
end: this.prefix(node.getLastToken()!)
};
}
visitCaseBlock(node: ts.CaseBlock): J.Block {
// consume end space so it gets assigned to the block's `end`
const end = this.prefix(node.getLastToken()!);
return {
kind: J.Kind.Block,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
static: this.rightPadded(false, emptySpace),
statements: node.clauses.map(clause =>
this.rightPadded(
this.visit(clause),
emptySpace
)),
end: end
}
}
visitNamespaceExportDeclaration(node: ts.NamespaceExportDeclaration): JS.NamespaceDeclaration {
return {
kind: JS.Kind.NamespaceDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: [
{
kind: J.Kind.Modifier,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
keyword: 'export',
type: J.ModifierType.LanguageExtension,
annotations: []
},
{
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.AsKeyword)!),
markers: emptyMarkers,
keyword: 'as',
type: J.ModifierType.LanguageExtension,
annotations: []
}
],
keywordType: this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.NamespaceKeyword)!), JS.NamespaceDeclaration.KeywordType.Namespace),
name: this.rightPadded(this.convert(node.name), this.suffix(node.name)),
};
}
visitImportEqualsDeclaration(node: ts.ImportEqualsDeclaration): JS.Import {
let exportModifierSuffix: J.Space | undefined = undefined;
return {
kind: JS.Kind.Import,
id: randomId(),
modifiers: (() => {
const exportModifier = node.modifiers?.find(m => m.kind === ts.SyntaxKind.ExportKeyword);
exportModifierSuffix = exportModifier && this.suffix(exportModifier);
return exportModifier ? [{
kind: J.Kind.Modifier,
id: randomId(),
prefix: this.prefix(exportModifier),
markers: emptyMarkers,
keyword: 'export',
type: J.ModifierType.LanguageExtension,
annotations: []
}] : [];
})(),
prefix: exportModifierSuffix ? exportModifierSuffix : this.prefix(node),
importClause: {
kind: JS.Kind.ImportClause,
id: randomId(),
prefix: (() => {
if (node.isTypeOnly) {
const typeKeyword = node.getChildren().find(n => n.kind === ts.SyntaxKind.TypeKeyword);
return this.prefix(typeKeyword!);
} else {
return this.prefix(node.name);
}
})(),
markers: emptyMarkers,
typeOnly: node.isTypeOnly,
name: node.name && this.rightPadded(this.visit(node.name), this.suffix(node.name)),
namedBindings: undefined
},
markers: emptyMarkers,
moduleSpecifier: undefined,
attributes: undefined,
initializer: this.leftPadded(this.suffix(node.name), this.visit(node.moduleReference))
};
}
visitImportKeyword(node: ts.ImportExpression): J.Identifier {
// this is used for dynamic imports as in `await import('foo')`
return this.mapIdentifier(node, 'import');
}
visitImportDeclaration(node: ts.ImportDeclaration): JS.Import {
return {
kind: JS.Kind.Import,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: [],
importClause: node.importClause && this.visit(node.importClause),
moduleSpecifier: this.leftPadded(node.importClause ? this.prefix(this.findChildNode(node, ts.SyntaxKind.FromKeyword)!) : emptySpace, this.visit(node.moduleSpecifier)),
attributes: node.attributes && this.visit(node.attributes)
};
}
visitImportClause(node: ts.ImportClause): JS.ImportClause {
return {
kind: JS.Kind.ImportClause,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
typeOnly: node.isTypeOnly,
name: node.name && this.rightPadded(this.visit(node.name), this.suffix(node.name)),
namedBindings: node.namedBindings && this.visit(node.namedBindings)
};
}
visitNamespaceImport(node: ts.NamespaceImport): JS.Alias {
return {
kind: JS.Kind.Alias,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
propertyName: this.rightPadded(this.mapIdentifier(node, "*"), this.prefix(this.findChildNode(node, ts.SyntaxKind.AsKeyword)!)),
alias: this.visit(node.name)
};
}
visitNamedImports(node: ts.NamedImports): JS.NamedImports {
const children = node.getChildren(this.sourceFile);
let elements: J.Container;
if (node.elements.length === 0) {
const openBrace = children[0];
const closeBrace = children[children.length - 1];
elements = {
kind: J.Kind.Container,
before: this.prefix(openBrace),
elements: [this.rightPadded({
kind: JS.Kind.ImportSpecifier,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
importType: this.leftPadded(emptySpace, false),
specifier: this.newEmpty(this.prefix(closeBrace)),
type: undefined
}, emptySpace)],
markers: emptyMarkers
};
} else {
elements = this.mapCommaSeparatedList(children);
}
return {
kind: JS.Kind.NamedImports,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
elements,
type: undefined
};
}
visitImportSpecifier(node: ts.ImportSpecifier): JS.ImportSpecifier {
return {
kind: JS.Kind.ImportSpecifier,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
importType: this.leftPadded(
node.isTypeOnly ? this.prefix(this.findChildNode(node, ts.SyntaxKind.TypeKeyword)!) : emptySpace,
node.isTypeOnly
),
specifier: node.propertyName
? {
kind: JS.Kind.Alias,
id: randomId(),
prefix: this.prefix(node.propertyName),
markers: emptyMarkers,
propertyName: this.rightPadded(this.convert(node.propertyName), this.suffix(node.propertyName)),
alias: this.convert(node.name)
}
: this.convert(node.name) as J.Identifier,
type: this.mapType(node),
};
}
visitExportAssignment(node: ts.ExportAssignment): JS.ExportAssignment {
return {
kind: JS.Kind.ExportAssignment,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
exportEquals: !!node.isExportEquals,
expression: this.leftPadded(
this.prefix(this.findChildNode(node, node.isExportEquals ? ts.SyntaxKind.EqualsToken : ts.SyntaxKind.DefaultKeyword)!),
this.visit(node.expression)
)
};
}
visitExportDeclaration(node: ts.ExportDeclaration): JS.ExportDeclaration {
return {
kind: JS.Kind.ExportDeclaration,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
modifiers: this.mapModifiers(node),
typeOnly: this.leftPadded(node.isTypeOnly ? this.prefix(this.findChildNode(node, ts.SyntaxKind.TypeKeyword)!) : emptySpace, node.isTypeOnly),
exportClause: node.exportClause ? this.visit(node.exportClause) : this.mapIdentifier(this.findChildNode(node, ts.SyntaxKind.AsteriskToken)!, "*"),
moduleSpecifier: node.moduleSpecifier && this.leftPadded(this.prefix(this.findChildNode(node, ts.SyntaxKind.FromKeyword)!), this.visit(node.moduleSpecifier)),
attributes: node.attributes && this.visit(node.attributes)
};
}
visitNamedExports(node: ts.NamedExports): JS.NamedExports {
return {
kind: JS.Kind.NamedExports,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
elements: this.mapCommaSeparatedList(node.getChildren()),
type: this.mapType(node)
};
}
visitNamespaceExport(node: ts.NamespaceExport): JS.Alias {
return {
kind: JS.Kind.Alias,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
propertyName: this.rightPadded(this.mapIdentifier(this.findChildNode(node, ts.SyntaxKind.AsteriskToken)!, "*"), this.prefix(this.findChildNode(node, ts.SyntaxKind.AsKeyword)!)),
alias: this.visit(node.name)
}
}
visitExportSpecifier(node: ts.ExportSpecifier): JS.ExportSpecifier {
return {
kind: JS.Kind.ExportSpecifier,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
typeOnly: this.leftPadded(node.isTypeOnly ? this.prefix(this.findChildNode(node, ts.SyntaxKind.TypeKeyword)!) : emptySpace, node.isTypeOnly),
specifier: node.propertyName
? {
kind: JS.Kind.Alias,
id: randomId(),
prefix: this.prefix(node.propertyName),
markers: emptyMarkers,
propertyName: this.rightPadded(this.convert(node.propertyName), this.suffix(node.propertyName)),
alias: this.convert(node.name)
} satisfies JS.Alias as JS.Alias
: this.convert(node.name),
type: this.mapType(node)
};
}
visitMissingDeclaration(node: ts.MissingDeclaration) {
return this.visitUnknown(node);
}
visitExternalModuleReference(node: ts.ExternalModuleReference): J.MethodInvocation {
const methodType = this.mapMethodType(node);
return {
kind: J.Kind.MethodInvocation,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
name: {...this.mapIdentifier(node, "require", false), type: methodType},
arguments: {
kind: J.Kind.Container,
before: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
elements: [this.rightPadded(this.visit(node.expression), this.suffix(node.expression))],
markers: emptyMarkers
},
methodType: methodType
}
}
visitJsxText(node: ts.JsxText): J.Literal {
// For JsxText, we don't use mapLiteral because it would put whitespace into prefix.
// In JSX, the text content (including leading/trailing whitespace) should be in value/valueSource.
return {
kind: J.Kind.Literal,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
value: node.text,
valueSource: node.text,
type: Type.Primitive.String
};
}
visitJsxElement(node: ts.JsxElement): JSX.Tag {
const attrs = node.openingElement.attributes.properties;
return {
kind: JS.Kind.JsxTag,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
openName: this.leftPadded(this.prefix(node.openingElement.tagName), this.visit(node.openingElement.tagName)),
typeArguments: node.openingElement.typeArguments && this.mapTypeArguments(this.suffix(node.openingElement.tagName), node.openingElement.typeArguments),
afterName: attrs.length === 0 ?
this.prefix(this.findLastChildNode(node.openingElement, ts.SyntaxKind.GreaterThanToken)!) :
emptySpace,
attributes:
this.mapJsxAttributes(
attrs,
this.prefix(this.findLastChildNode(node.openingElement, ts.SyntaxKind.GreaterThanToken)!),
() => emptyMarkers
),
children: this.mapJsxChildren(node.children),
closingName: this.leftPadded(this.prefix(node.closingElement.tagName), this.visit(node.closingElement.tagName)),
afterClosingName: this.suffix(node.closingElement.tagName)
};
}
visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): JSX.Tag {
const attrs = node.attributes.properties;
return {
kind: JS.Kind.JsxTag,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
openName: this.leftPadded(this.prefix(node.tagName), this.visit(node.tagName)),
typeArguments: node.typeArguments && this.mapTypeArguments(this.suffix(node.tagName), node.typeArguments),
afterName: attrs.length === 0 ?
this.prefix(this.findChildNode(node, ts.SyntaxKind.GreaterThanToken)!) :
emptySpace,
attributes:
this.mapJsxAttributes(
attrs,
this.prefix(this.findChildNode(node, ts.SyntaxKind.GreaterThanToken)!),
() => emptyMarkers
),
selfClosing: this.prefix(this.findChildNode(node, ts.SyntaxKind.SlashToken)!),
};
}
visitJsxFragment(node: ts.JsxFragment): JSX.Tag {
return {
kind: JS.Kind.JsxTag,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
openName: this.leftPadded(this.prefix(node.openingFragment), this.newEmpty()),
afterName: this.prefix(this.findChildNode(node.openingFragment, ts.SyntaxKind.GreaterThanToken)!),
attributes: [],
children: this.mapJsxChildren(node.children),
closingName: this.leftPadded(emptySpace, this.newEmpty()),
afterClosingName: emptySpace
};
}
visitJsxAttribute(node: ts.JsxAttribute): JSX.Attribute {
return {
kind: JS.Kind.JsxAttribute,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
key: this.visit(node.name),
value: node.initializer
? this.leftPadded(
this.prefix(this.findChildNode(node, ts.SyntaxKind.EqualsToken)!),
this.visit(node.initializer)
)
: undefined
};
}
visitJsxSpreadAttribute(node: ts.JsxSpreadAttribute): JSX.SpreadAttribute {
return {
kind: JS.Kind.JsxSpreadAttribute,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
dots: this.prefix(this.findChildNode(node, ts.SyntaxKind.DotDotDotToken)!),
expression: this.rightPadded(this.visit(node.expression),
this.suffix(node.expression))
};
}
visitJsxExpression(node: ts.JsxExpression): JSX.EmbeddedExpression {
let expr: Expression;
if (node.expression) {
if (node.dotDotDotToken) {
// Wrap in JS.Spread
const innerExpr = this.convert(node.expression);
expr = {
kind: JS.Kind.Spread,
id: randomId(),
prefix: this.prefix(node.dotDotDotToken!),
markers: emptyMarkers,
expression: innerExpr,
type: this.mapType(node.expression)
} satisfies JS.Spread as Expression;
} else {
expr = this.convert(node.expression);
}
} else {
expr = this.newEmpty();
}
return {
kind: JS.Kind.JsxEmbeddedExpression,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.rightPadded(
expr,
this.prefix(this.findChildNode(node, ts.SyntaxKind.CloseBraceToken)!)
)
};
}
visitJsxNamespacedName(node: ts.JsxNamespacedName): JSX.NamespacedName {
return {
kind: JS.Kind.JsxNamespacedName,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
namespace: this.mapIdentifier(node.namespace, node.namespace.getText()),
name: this.leftPadded(
this.prefix(this.findChildNode(node, ts.SyntaxKind.ColonToken)!),
this.mapIdentifier(node.name, node.name.getText())
)
};
}
visitCaseClause(node: ts.CaseClause): J.Case {
return {
kind: J.Kind.Case,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
type: J.Case.Type.Statement,
caseLabels: {
kind: J.Kind.Container,
before: this.prefix(node.expression),
elements: [this.rightPadded(
this.visit(node.expression),
this.suffix(node.expression)
)],
markers: emptyMarkers
},
statements: {
kind: J.Kind.Container,
before: this.prefix(node),
elements: this.semicolonPaddedStatementList(node.statements),
markers: emptyMarkers
},
};
}
visitDefaultClause(node: ts.DefaultClause): J.Case {
return {
kind: J.Kind.Case,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
type: J.Case.Type.Statement,
caseLabels: {
kind: J.Kind.Container,
before: this.prefix(node),
elements: [this.rightPadded(this.mapIdentifier(node, 'default'), this.suffix(this.findChildNode(node, ts.SyntaxKind.DefaultKeyword)!))],
markers: emptyMarkers
},
statements: {
kind: J.Kind.Container,
before: this.prefix(node),
elements: this.semicolonPaddedStatementList(node.statements),
markers: emptyMarkers
},
};
}
visitHeritageClause(node: ts.HeritageClause) {
return this.convert(node.types[0]);
}
visitCatchClause(node: ts.CatchClause): J.Try.Catch {
return {
kind: J.Kind.TryCatch,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
parameter: node.variableDeclaration ?
{
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.OpenParenToken)!),
markers: emptyMarkers,
tree: this.rightPadded(
{
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: this.prefix(node.variableDeclaration),
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: [],
typeExpression: this.mapTypeInfo(node.variableDeclaration),
variables: [this.rightPadded(this.visit(node.variableDeclaration), emptySpace)]
},
this.suffix(node.variableDeclaration))
} :
// should return empty variables list to handle: try { } catch { }
{
kind: J.Kind.ControlParentheses,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
tree: this.rightPadded({
kind: J.Kind.VariableDeclarations,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
leadingAnnotations: [],
modifiers: [],
variables: []
}, emptySpace)
},
body: this.visit(node.block),
}
}
visitImportAttributes(node: ts.ImportAttributes): JS.ImportAttributes {
const openBraceIndex = node.getChildren().findIndex(n => n.kind === ts.SyntaxKind.OpenBraceToken);
const elements = this.mapCommaSeparatedList(node.getChildren(this.sourceFile).slice(openBraceIndex, openBraceIndex + 3));
return {
kind: JS.Kind.ImportAttributes,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
token: ts.SyntaxKind.WithKeyword === node.token ? JS.ImportAttributes.Token.With : JS.ImportAttributes.Token.Assert,
elements: elements,
};
}
visitImportAttribute(node: ts.ImportAttribute): JS.ImportAttribute {
return {
kind: JS.Kind.ImportAttribute,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
name: this.visit(node.name),
value: this.leftPadded(this.suffix(node.name), this.visit(node.value)),
};
}
visitPropertyAssignment(node: ts.PropertyAssignment): JS.PropertyAssignment {
return {
kind: JS.Kind.PropertyAssignment,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
name: this.rightPadded(this.visit(node.name), this.suffix(node.name)),
assigmentToken: JS.PropertyAssignment.Token.Colon,
initializer: this.visit(node.initializer)
};
}
visitShorthandPropertyAssignment(node: ts.ShorthandPropertyAssignment): JS.PropertyAssignment {
return {
kind: JS.Kind.PropertyAssignment,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
name: this.rightPadded(this.visit(node.name), this.suffix(node.name)),
assigmentToken: JS.PropertyAssignment.Token.Equals,
initializer: node.objectAssignmentInitializer && this.visit(node.objectAssignmentInitializer)
};
}
visitSpreadAssignment(node: ts.SpreadAssignment): JS.Spread {
return {
kind: JS.Kind.Spread,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
expression: this.convert(node.expression),
type: this.mapType(node)
};
}
visitEnumMember(node: ts.EnumMember): J.EnumValue {
return {
kind: J.Kind.EnumValue,
id: randomId(),
prefix: this.prefix(node),
markers: emptyMarkers,
annotations: [],
name: node.name ? ts.isStringLiteral(node.name) ? this.mapIdentifier(node.name, node.name.getText()) : this.convert(node.name) : this.mapIdentifier(node, ""),
initializer: node.initializer && {
kind: J.Kind.NewClass,
id: randomId(),
prefix: this.suffix(node.name),
markers: emptyMarkers,
new: emptySpace,
arguments: {
kind: J.Kind.Container,
before: emptySpace,
elements: [this.rightPadded(this.visit(node.initializer), emptySpace)],
markers: emptyMarkers
},
constructorType: this.mapMethodType(node)
}
}
}
visitBundle(node: ts.Bundle) {
return this.visitUnknown(node);
}
visitJSDocTypeExpression(node: ts.JSDocTypeExpression) {
return this.visitUnknown(node);
}
visitJSDocNameReference(node: ts.JSDocNameReference) {
return this.visitUnknown(node);
}
visitJSDocMemberName(node: ts.JSDocMemberName) {
return this.visitUnknown(node);
}
visitJSDocAllType(node: ts.JSDocAllType) {
return this.visitUnknown(node);
}
visitJSDocUnknownType(node: ts.JSDocUnknownType) {
return this.visitUnknown(node);
}
visitJSDocNullableType(node: ts.JSDocNullableType) {
return this.visitUnknown(node);
}
visitJSDocNonNullableType(node: ts.JSDocNonNullableType) {
return this.visitUnknown(node);
}
visitJSDocOptionalType(node: ts.JSDocOptionalType) {
return this.visitUnknown(node);
}
visitJSDocFunctionType(node: ts.JSDocFunctionType) {
return this.visitUnknown(node);
}
visitJSDocVariadicType(node: ts.JSDocVariadicType) {
return this.visitUnknown(node);
}
visitJSDocNamepathType(node: ts.JSDocNamepathType) {
return this.visitUnknown(node);
}
visitJSDoc(node: ts.JSDoc) {
return this.visitUnknown(node);
}
visitJSDocType(node: ts.JSDocType) {
return this.visitUnknown(node);
}
visitJSDocText(node: ts.JSDocText) {
return this.visitUnknown(node);
}
visitJSDocTypeLiteral(node: ts.JSDocTypeLiteral) {
return this.visitUnknown(node);
}
visitJSDocSignature(node: ts.JSDocSignature) {
return this.visitUnknown(node);
}
visitJSDocLink(node: ts.JSDocLink) {
return this.visitUnknown(node);
}
visitJSDocLinkCode(node: ts.JSDocLinkCode) {
return this.visitUnknown(node);
}
visitJSDocLinkPlain(node: ts.JSDocLinkPlain) {
return this.visitUnknown(node);
}
visitJSDocTag(node: ts.JSDocTag) {
return this.visitUnknown(node);
}
visitJSDocAugmentsTag(node: ts.JSDocAugmentsTag) {
return this.visitUnknown(node);
}
visitJSDocImplementsTag(node: ts.JSDocImplementsTag) {
return this.visitUnknown(node);
}
visitJSDocAuthorTag(node: ts.JSDocAuthorTag) {
return this.visitUnknown(node);
}
visitJSDocDeprecatedTag(node: ts.JSDocDeprecatedTag) {
return this.visitUnknown(node);
}
visitJSDocClassTag(node: ts.JSDocClassTag) {
return this.visitUnknown(node);
}
visitJSDocPublicTag(node: ts.JSDocPublicTag) {
return this.visitUnknown(node);
}
visitJSDocPrivateTag(node: ts.JSDocPrivateTag) {
return this.visitUnknown(node);
}
visitJSDocProtectedTag(node: ts.JSDocProtectedTag) {
return this.visitUnknown(node);
}
visitJSDocReadonlyTag(node: ts.JSDocReadonlyTag) {
return this.visitUnknown(node);
}
visitJSDocOverrideTag(node: ts.JSDocOverrideTag) {
return this.visitUnknown(node);
}
visitJSDocCallbackTag(node: ts.JSDocCallbackTag) {
return this.visitUnknown(node);
}
visitJSDocOverloadTag(node: ts.JSDocOverloadTag) {
return this.visitUnknown(node);
}
visitJSDocEnumTag(node: ts.JSDocEnumTag) {
return this.visitUnknown(node);
}
visitJSDocParameterTag(node: ts.JSDocParameterTag) {
return this.visitUnknown(node);
}
visitJSDocReturnTag(node: ts.JSDocReturnTag) {
return this.visitUnknown(node);
}
visitJSDocThisTag(node: ts.JSDocThisTag) {
return this.visitUnknown(node);
}
visitJSDocTypeTag(node: ts.JSDocTypeTag) {
return this.visitUnknown(node);
}
visitJSDocTemplateTag(node: ts.JSDocTemplateTag) {
return this.visitUnknown(node);
}
visitJSDocTypedefTag(node: ts.JSDocTypedefTag) {
return this.visitUnknown(node);
}
visitJSDocSeeTag(node: ts.JSDocSeeTag) {
return this.visitUnknown(node);
}
visitJSDocPropertyTag(node: ts.JSDocPropertyTag) {
return this.visitUnknown(node);
}
visitJSDocThrowsTag(node: ts.JSDocThrowsTag) {
return this.visitUnknown(node);
}
visitJSDocSatisfiesTag(node: ts.JSDocSatisfiesTag) {
return this.visitUnknown(node);
}
visitJSDocImportTag(node: ts.JSDocImportTag) {
return this.visitUnknown(node);
}
visitSyntaxList(node: ts.SyntaxList) {
return this.visitUnknown(node);
}
visitNotEmittedStatement(node: ts.NotEmittedStatement) {
return this.visitUnknown(node);
}
visitPartiallyEmittedExpression(node: ts.PartiallyEmittedExpression) {
return this.visitUnknown(node);
}
visitCommaListExpression(node: ts.CommaListExpression) {
return this.visitUnknown(node);
}
visitSyntheticReferenceExpression(node: ts.Node) {
return this.visitUnknown(node);
}
private _seenTriviaSpans: TextSpan[] = [];
private prefix(node: ts.Node, consume: boolean = true): J.Space {
if (node.getFullStart() == node.getStart()) {
return emptySpace;
}
if (consume) {
const nodeStart = node.getFullStart();
const span: TextSpan = [nodeStart, node.getStart()];
let idx = binarySearch(this._seenTriviaSpans, span, compareTextSpans);
if (idx >= 0)
return emptySpace;
idx = ~idx;
if (idx > 0 && this._seenTriviaSpans[idx - 1][1] > span[0])
return emptySpace;
this._seenTriviaSpans.splice(idx, 0, span);
}
return prefixFromNode(node, this.sourceFile);
}
private suffix = (node: ts.Node, consume: boolean = true): J.Space => {
return this.prefix(getNextSibling(node)!, consume);
}
private mapType(node: ts.Node): Type | undefined {
return this.typeMapping?.type(node);
}
private mapDeclarationType(node: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.EnumDeclaration): Type.FullyQualified | undefined {
return this.typeMapping?.declarationType(node);
}
private mapPrimitiveType(node: ts.Node): Type.Primitive {
return this.typeMapping?.primitiveType(node) ?? Type.Primitive.None;
}
private mapVariableType(node: ts.NamedDeclaration): Type.Variable | undefined {
return this.typeMapping?.variableType(node);
}
private mapMethodType(node: ts.Node): Type.Method | undefined {
return this.typeMapping?.methodType(node);
}
private mapAnnotationType(node: ts.Node): Type.FullyQualified | undefined {
return this.typeMapping?.annotationType(node);
}
private mapCommaSeparatedList(nodes: readonly ts.Node[]): J.Container {
return this.mapToContainer(nodes, this.trailingComma(nodes));
}
private mapTypeArguments(prefix: J.Space, nodes: readonly ts.Node[]): J.Container {
if (nodes.length === 0) {
return emptyContainer();
}
const args = nodes.map(node =>
this.rightPadded(
this.visit(node),
this.suffix(node),
emptyMarkers
))
return {
kind: J.Kind.Container,
before: prefix,
elements: args,
markers: emptyMarkers
};
}
private trailingComma = (nodes: readonly ts.Node[]) => (ns: readonly ts.Node[], i: number) => {
const last = i === ns.length - 2;
return last ? markers({
kind: J.Markers.TrailingComma,
id: randomId(),
suffix: this.prefix(nodes[2], false)
} satisfies TrailingComma as TrailingComma) : emptyMarkers;
}
private mapToContainer(nodes: readonly ts.Node[], markers?: (ns: readonly ts.Node[], i: number) => Markers): J.Container {
if (nodes.length === 0) {
return emptyContainer();
}
const prefix = this.prefix(nodes[0]);
const args: J.RightPadded[] = this.mapToRightPaddedList(nodes[1].getChildren(this.sourceFile), this.prefix(nodes[2]), markers);
return {
kind: J.Kind.Container,
before: prefix,
elements: args,
markers: emptyMarkers
};
}
private mapToRightPaddedList(elementList: readonly ts.Node[], lastAfter: J.Space, markers?: (ns: readonly ts.Node[], i: number) => Markers): J.RightPadded[] {
let childCount = elementList.length;
const args: J.RightPadded[] = [];
if (childCount === 0) {
args.push(this.rightPadded(
this.newEmpty() as T,
lastAfter,
emptyMarkers
));
} else {
for (let i = 0; i < childCount - 1; i += 2) {
// FIXME right padding and trailing comma
// const last = i === childCount - 2;
args.push(this.rightPadded(
this.visit(elementList[i]),
this.prefix(elementList[i + 1]),
markers ? markers(elementList, i) : emptyMarkers
));
}
if ((childCount & 1) === 1) {
args.push(this.rightPadded(this.visit(elementList[childCount - 1]), lastAfter));
}
}
return args;
}
private mapJsxChildren(elementList: readonly ts.Node[]): T[] {
let childCount = elementList.length;
const args: T[] = [];
if (childCount === 0) {
args.push(this.newEmpty() as T);
} else {
for (let i = 0; i < childCount; i++) {
args.push(this.visit(elementList[i]));
}
}
return args;
}
private mapJsxAttributes(elementList: readonly ts.Node[], lastAfter: J.Space, markers?: (ns: readonly ts.Node[], i: number) => Markers): J.RightPadded[] {
let childCount = elementList.length;
if (childCount === 0) {
return [];
} else {
const args: J.RightPadded[] = [];
for (let i = 0; i < childCount; i++) {
const node = elementList[i];
const isLast = i === childCount - 1;
args.push(this.rightPadded(
this.visit(node),
isLast ? lastAfter : emptySpace,
markers ? markers(elementList, i) : emptyMarkers
));
}
return args;
}
}
private mapDecorators(node: ts.ClassDeclaration | ts.FunctionDeclaration | ts.MethodDeclaration | ts.ConstructorDeclaration | ts.ParameterDeclaration | ts.PropertyDeclaration | ts.SetAccessorDeclaration | ts.GetAccessorDeclaration | ts.ClassExpression): J.Annotation[] {
return node.modifiers?.filter(ts.isDecorator)?.map(this.convert) ?? [];
}
private mapTypeParametersAsContainer(node: ts.ClassDeclaration | ts.InterfaceDeclaration | ts.ClassExpression): J.Container | undefined {
return node.typeParameters &&
{
kind: J.Kind.Container,
before: this.prefix(this.findChildNode(node, ts.SyntaxKind.LessThanToken)!),
elements: this.mapTypeParametersList(node.typeParameters)
.concat(node.typeParameters.hasTrailingComma ? this.rightPadded(
{
kind: J.Kind.TypeParameter,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
modifiers: [],
name: this.newEmpty(),
},
this.prefix(this.findChildNode(node, ts.SyntaxKind.GreaterThanToken)!)) : []),
markers: emptyMarkers
};
}
private mapTypeParametersAsObject(node: ts.MethodDeclaration | ts.MethodSignature | ts.FunctionDeclaration
| ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.TypeAliasDeclaration | ts.FunctionTypeNode | ts.ConstructorTypeNode): J.TypeParameters | undefined {
const typeParameters = node.typeParameters;
if (!typeParameters) return undefined;
return {
kind: J.Kind.TypeParameters,
id: randomId(),
prefix: this.prefix(this.findChildNode(node, ts.SyntaxKind.LessThanToken)!),
markers: emptyMarkers,
annotations: [],
typeParameters: typeParameters.length == 0 ?
[this.rightPadded({
kind: J.Kind.TypeParameter,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
modifiers: [],
name: this.newEmpty(),
}, this.prefix(this.findChildNode(node, ts.SyntaxKind.GreaterThanToken)!))]
: typeParameters.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp)))
.concat(typeParameters.hasTrailingComma ? this.rightPadded(
{
kind: J.Kind.TypeParameter,
id: randomId(),
prefix: emptySpace,
markers: emptyMarkers,
annotations: [],
modifiers: [],
name: this.newEmpty(),
},
this.prefix(this.findChildNode(node, ts.SyntaxKind.GreaterThanToken)!)) : []),
};
}
private mapTypeParametersList(typeParamsNodeArray: ts.NodeArray): J.RightPadded[] {
return typeParamsNodeArray.map(tp => this.rightPadded(this.visit(tp), this.suffix(tp)));
}
private findChildNode(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined {
for (let i = 0; i < node.getChildCount(this.sourceFile); i++) {
if (node.getChildAt(i, this.sourceFile).kind === kind) {
return node.getChildAt(i, this.sourceFile);
}
}
return undefined;
}
private findLastChildNode(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined {
for (let i = node.getChildCount(this.sourceFile) - 1; i >= 0; i--) {
if (node.getChildAt(i, this.sourceFile).kind === kind) {
return node.getChildAt(i, this.sourceFile);
}
}
return undefined;
}
private convertToken(token?: ts.Node) {
if (token?.kind === ts.SyntaxKind.CommaToken) {
return {
kind: J.Markers.TrailingComma,
id: randomId(),
suffix: emptySpace
};
}
if (token?.kind === ts.SyntaxKind.SemicolonToken) return {kind: J.Markers.Semicolon, id: randomId()};
return undefined;
}
private newEmpty(prefix: J.Space = emptySpace, markers?: Markers): J.Empty {
return {kind: J.Kind.Empty, id: randomId(), prefix: prefix, markers: markers ?? emptyMarkers};
}
private maybeAddOptionalMarker(t: {
markers: Markers
}, node: ts.MethodSignature | ts.MethodDeclaration | ts.ParameterDeclaration | ts.PropertySignature | ts.PropertyDeclaration | ts.NamedTupleMember): Markers {
return produce(t.markers, draft => {
if (node.questionToken) {
draft.markers.push({
kind: JS.Markers.Optional,
id: randomId(),
prefix: this.suffix(node.name)
} satisfies Optional as Optional);
}
});
}
}
function prefixFromNode(node: ts.Node, sourceFile: ts.SourceFile): J.Space {
const comments: Comment[] = [];
const text = sourceFile.getFullText();
const nodeStart = node.getFullStart();
// FIXME merge with whitespace from previous sibling
// let previousSibling = getPreviousSibling(node);
let leadingWhitespacePos = node.getStart();
// Step 1: Get all comments
const commentRanges = [
...(ts.getTrailingCommentRanges(text, nodeStart) || []),
...(ts.getLeadingCommentRanges(text, nodeStart) || []),
].filter(range => range.pos < node.getStart())
.sort((a, b) => a.pos - b.pos)
// filter out duplicate ranges
.filter((range, index, self) => index === 0 || range.pos !== self[index - 1].pos);
commentRanges.forEach(range => {
const pos = range.pos;
const end = range.end;
const kind = range.kind;
leadingWhitespacePos = Math.min(leadingWhitespacePos, pos);
const isMultiline = kind === ts.SyntaxKind.MultiLineCommentTrivia;
const commentStart = pos + 2; // Skip `/*` or `//`
const commentEnd = isMultiline ? end - 2 : end; // Exclude closing `*/` or nothing for `//`
// Step 2: Capture suffix (whitespace after the comment)
let suffixEnd = end;
while (suffixEnd < text.length && (text[suffixEnd] === ' ' || text[suffixEnd] === '\t' || text[suffixEnd] === '\n' || text[suffixEnd] === '\r')) {
suffixEnd++;
}
const commentBody = text.slice(commentStart, commentEnd); // Extract comment body
const suffix = text.slice(end, suffixEnd); // Extract suffix (whitespace after comment)
comments.push({
kind: J.Kind.TextComment,
multiline: isMultiline,
text: commentBody,
suffix: suffix,
markers: emptyMarkers
} satisfies TextComment as TextComment);
});
// Step 3: Extract leading whitespace (before the first comment)
let whitespace = '';
if (leadingWhitespacePos > nodeStart) {
whitespace = text.slice(nodeStart, leadingWhitespacePos);
}
return {kind: J.Kind.Space, comments: comments, whitespace: whitespace};
}
class FlowSyntaxNotSupportedError extends SyntaxError {
constructor(message: string = "Flow syntax is not supported") {
super(message);
this.name = "FlowSyntaxNotSupportedError";
}
}
const SURR_FIRST = 0xD800;
const SURR_LAST = 0xDFFF;
/**
* Extracts invalid UTF-16 surrogate pairs from a string literal's value source.
* Unmatched UTF-16 surrogate pairs (composed of two escape and code point pairs) are unserializable
* by technologies like Jackson. So we separate and store the code point off and reconstruct
* the escape sequence when printing later.
* We only escape unicode characters that are part of UTF-16 surrogate pairs. Others are generally
* treated well by tools like Jackson.
*
* Handles both:
* 1. Unicode escape sequences (\uXXXX) where XXXX is in the surrogate range
* 2. Raw surrogate characters in the source (character codes 0xD800-0xDFFF)
*/
function extractSurrogateEscapes(valueSource: string): { cleanedSource: string, unicodeEscapes: J.LiteralUnicodeEscape[] } {
const unicodeEscapes: J.LiteralUnicodeEscape[] = [];
let cleanedSource = '';
let cleanedIndex = 0;
for (let j = 0; j < valueSource.length; j++) {
const c = valueSource.charAt(j);
const charCode = valueSource.charCodeAt(j);
// Check for unicode escape sequence: \uXXXX
// Ensure we're not escaped (previous char is not \) or we're at the start
if (c === '\\' && j < valueSource.length - 1 && (j === 0 || valueSource.charAt(j - 1) !== '\\')) {
if (valueSource.charAt(j + 1) === 'u' && j < valueSource.length - 5) {
const codePoint = valueSource.substring(j + 2, j + 6);
const codePointNumeric = parseInt(codePoint, 16);
if (!isNaN(codePointNumeric) && codePointNumeric >= SURR_FIRST && codePointNumeric <= SURR_LAST) {
unicodeEscapes.push({ valueSourceIndex: cleanedIndex, codePoint });
j += 5; // Skip the \uXXXX sequence (we're already at \, skip u and 4 hex digits)
continue;
}
}
}
// Check for raw surrogate characters in the source
if (charCode >= SURR_FIRST && charCode <= SURR_LAST) {
const codePoint = charCode.toString(16).toUpperCase().padStart(4, '0');
unicodeEscapes.push({ valueSourceIndex: cleanedIndex, codePoint });
continue;
}
cleanedSource += c;
cleanedIndex++;
}
return { cleanedSource, unicodeEscapes };
}
Parsers.registerParser("javascript", JavaScriptParser);