1/* 2 * Copyright (c) 2022-2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as ts from 'typescript'; 17import * as path from 'path'; 18 19import { defaultCompilerOptions, getSourceFilesFromDir, compile, parseConfigFile } from './compiler/Compiler'; 20import type { DeclgenCLIOptions } from './cli/DeclgenCLI'; 21import { Logger } from '../utils/logger/Logger'; 22 23import { Transformer } from './ASTTransformer'; 24import { Autofixer } from './ASTAutofixer'; 25import { Checker } from './ASTChecker'; 26import { Extension } from './utils/Extension'; 27 28export type CheckerResult = ts.Diagnostic[]; 29 30export interface DeclgenResult { 31 emitResult: ts.EmitResult; 32 checkResult: CheckerResult; 33} 34 35export class Declgen { 36 private readonly sourceFileMap: Map<string, ts.SourceFile>; 37 private readonly hookedHost: ts.CompilerHost; 38 private readonly rootFiles: readonly string[]; 39 private readonly compilerOptions: ts.CompilerOptions; 40 41 constructor(declgenOptions: DeclgenCLIOptions) { 42 const { rootNames, options } = Declgen.parseDeclgenOptions(declgenOptions); 43 44 this.rootFiles = rootNames; 45 46 this.sourceFileMap = new Map<string, ts.SourceFile>(); 47 48 this.compilerOptions = Object.assign(options, { 49 declaration: true, 50 emitDeclarationOnly: true, 51 outDir: declgenOptions.outDir 52 }); 53 54 this.hookedHost = Declgen.createHookedCompilerHost(this.sourceFileMap, this.compilerOptions); 55 } 56 57 run(): DeclgenResult { 58 59 /** 60 * First compilation with the hooked CompilerHost: 61 * collect the SourceFiles after transformation to the hooked Map 62 */ 63 let program = this.recompile(); 64 65 const emitResult = program.emit(undefined, undefined, undefined, true, { 66 before: [], 67 after: [], 68 afterDeclarations: [ 69 (ctx: ts.TransformationContext): ts.CustomTransformer => { 70 const typeChecker = program.getTypeChecker(); 71 const autofixer = new Autofixer(typeChecker, ctx); 72 const transformer = new Transformer(ctx, this.sourceFileMap, [autofixer.fixNode.bind(autofixer)]); 73 74 return transformer.createCustomTransformer(); 75 } 76 ] 77 }); 78 79 /** 80 * Second compilation with the hooked CompilerHost: 81 * use transformed source files and perform type check upon them 82 */ 83 program = this.recompile(); 84 85 const checkResult = this.checkProgram(program); 86 87 return { 88 emitResult: emitResult, 89 checkResult: checkResult 90 }; 91 } 92 93 private recompile(): ts.Program { 94 return compile(this.rootFiles, this.compilerOptions, this.hookedHost); 95 } 96 97 private checkProgram(program: ts.Program): CheckerResult { 98 const checker = new Checker(program.getTypeChecker()); 99 100 void checker; 101 void this; 102 103 return []; 104 } 105 106 private static createHookedCompilerHost( 107 sourceFileMap: Map<string, ts.SourceFile>, 108 compilerOptions: ts.CompilerOptions 109 ): ts.CompilerHost { 110 const host = ts.createCompilerHost(compilerOptions); 111 const fallbackGetSourceFile = host.getSourceFile; 112 const fallbackWriteFile = host.writeFile; 113 114 return Object.assign(host, { 115 getSourceFile( 116 fileName: string, 117 languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, 118 onError?: (message: string) => void, 119 shouldCreateNewSourceFile?: boolean 120 ): ts.SourceFile | undefined { 121 return ( 122 sourceFileMap.get(fileName) ?? 123 fallbackGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) 124 ); 125 }, 126 writeFile( 127 fileName: string, 128 text: string, 129 writeByteOrderMark: boolean, 130 onError?: (message: string) => void, 131 sourceFiles?: readonly ts.SourceFile[], 132 data?: ts.WriteFileCallbackData 133 ) { 134 const parsedPath = path.parse(fileName); 135 fallbackWriteFile( 136 137 /* 138 * Since `.d` part of `.d.ts` extension is a part of the parsedPath.name, 139 * use `Extension.Ets` for output file name generation. 140 */ 141 path.join(parsedPath.dir, `${parsedPath.name}${Extension.STS}`), 142 text, 143 writeByteOrderMark, 144 onError, 145 sourceFiles, 146 data 147 ); 148 } 149 }); 150 } 151 152 static parseDeclgenOptions(opts: DeclgenCLIOptions): ts.CreateProgramOptions { 153 const parsedConfigFile = opts.tsconfig ? parseConfigFile(opts.tsconfig) : undefined; 154 155 if (parsedConfigFile) { 156 return { 157 rootNames: parsedConfigFile.fileNames, 158 options: parsedConfigFile.options, 159 projectReferences: parsedConfigFile.projectReferences, 160 configFileParsingDiagnostics: ts.getConfigFileParsingDiagnostics(parsedConfigFile) 161 }; 162 } 163 return { 164 rootNames: Declgen.collectInputFiles(opts), 165 options: defaultCompilerOptions() 166 }; 167 } 168 169 private static collectInputFiles(opts: DeclgenCLIOptions): string[] { 170 const inputFiles = [] as string[]; 171 172 if (opts.inputFiles) { 173 inputFiles.push(...opts.inputFiles); 174 } 175 176 if (opts.inputDirs) { 177 for (const dir of opts.inputDirs) { 178 try { 179 inputFiles.push(...getSourceFilesFromDir(dir)); 180 } catch (error) { 181 Logger.error('Failed to read folder: ' + error); 182 process.exit(-1); 183 } 184 } 185 } 186 187 return inputFiles; 188 } 189} 190