• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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