• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 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 fs from 'fs';
17import path from 'path';
18import * as ts from 'typescript';
19import { projectConfig } from '../main';
20import {
21  toUnixPath,
22  getRollupCacheStoreKey,
23  getRollupCacheKey
24} from './utils';
25import {
26  resolveModuleNames,
27  resolveTypeReferenceDirectives,
28  fileHashScriptVersion,
29  LanguageServiceCache,
30} from './ets_checker';
31import { ARKTS_LINTER_BUILD_INFO_SUFFIX } from './pre_define';
32
33const arkTSDir: string = 'ArkTS';
34const arkTSLinterOutputFileName: string = 'ArkTSLinter_output.json';
35const spaceNumBeforeJsonLine = 2;
36
37interface OutputInfo {
38  categoryInfo: string | undefined;
39  fileName: string | undefined;
40  line: number | undefined;
41  character: number | undefined;
42  messageText: string | ts.DiagnosticMessageChain;
43}
44
45export enum ArkTSLinterMode {
46  NOT_USE = 0,
47  COMPATIBLE_MODE = 1,
48  STANDARD_MODE = 2
49}
50
51export enum ArkTSVersion {
52  ArkTS_1_0,
53  ArkTS_1_1,
54}
55
56export interface ArkTSProgram {
57  builderProgram: ts.BuilderProgram,
58  wasStrict: boolean
59}
60
61export type ProcessDiagnosticsFunc = (diagnostics: ts.Diagnostic) => void;
62
63export function doArkTSLinter(arkTSVersion: ArkTSVersion, arkTSMode: ArkTSLinterMode,
64  builderProgram: ArkTSProgram, reverseStrictProgram: ArkTSProgram,
65  printDiagnostic: ProcessDiagnosticsFunc, shouldWriteFile: boolean = true,
66  buildInfoWriteFile?: ts.WriteFileCallback): ts.Diagnostic[] {
67  if (arkTSMode === ArkTSLinterMode.NOT_USE) {
68    return [];
69  }
70
71  let diagnostics: ts.Diagnostic[] = [];
72
73  if (arkTSVersion === ArkTSVersion.ArkTS_1_0) {
74    diagnostics = ts.ArkTSLinter_1_0.runArkTSLinter(builderProgram, reverseStrictProgram,
75      /*srcFile*/ undefined, buildInfoWriteFile);
76  } else {
77    diagnostics = ts.ArkTSLinter_1_1.runArkTSLinter(builderProgram, reverseStrictProgram,
78      /*srcFile*/ undefined, buildInfoWriteFile);
79  }
80
81  removeOutputFile();
82  if (diagnostics.length === 0) {
83    return [];
84  }
85
86  if (arkTSMode === ArkTSLinterMode.COMPATIBLE_MODE) {
87    processArkTSLinterReportAsWarning(diagnostics, printDiagnostic, shouldWriteFile);
88  } else {
89    processArkTSLinterReportAsError(diagnostics, printDiagnostic);
90  }
91
92  return diagnostics;
93}
94
95function processArkTSLinterReportAsError(diagnostics: ts.Diagnostic[], printDiagnostic: ProcessDiagnosticsFunc): void {
96  diagnostics.forEach((diagnostic: ts.Diagnostic) => {
97    printDiagnostic(diagnostic);
98  });
99  printArkTSLinterFAQ(diagnostics, printDiagnostic);
100}
101
102function processArkTSLinterReportAsWarning(diagnostics: ts.Diagnostic[], printDiagnostic: ProcessDiagnosticsFunc,
103  shouldWriteFile: boolean): void {
104  const filePath = shouldWriteFile ? writeOutputFile(diagnostics) : undefined;
105  if (filePath === undefined) {
106    diagnostics.forEach((diagnostic: ts.Diagnostic) => {
107      const originalCategory = diagnostic.category;
108      diagnostic.category = ts.DiagnosticCategory.Warning;
109      printDiagnostic(diagnostic);
110      diagnostic.category = originalCategory;
111    });
112    printArkTSLinterFAQ(diagnostics, printDiagnostic);
113    return;
114  }
115  const logMessage = `Has ${diagnostics.length} ArkTS Linter Error. You can get the output in ${filePath}`;
116  const arkTSDiagnostic: ts.Diagnostic = {
117    file: undefined,
118    start: undefined,
119    length: undefined,
120    messageText: logMessage,
121    category: ts.DiagnosticCategory.Warning,
122    code: -1,
123    reportsUnnecessary: undefined,
124    reportsDeprecated: undefined
125  };
126  printDiagnostic(arkTSDiagnostic);
127
128  printArkTSLinterFAQ(diagnostics, printDiagnostic);
129}
130
131function writeOutputFile(diagnostics: ts.Diagnostic[]): string | undefined {
132  let filePath: string = toUnixPath(projectConfig.cachePath);
133  if (!fs.existsSync(filePath)) {
134    return undefined;
135  }
136  filePath = toUnixPath(path.join(filePath, arkTSDir));
137  if (!fs.existsSync(filePath)) {
138    fs.mkdirSync(filePath);
139  }
140  filePath = toUnixPath((path.join(filePath, arkTSLinterOutputFileName)));
141  const outputInfo: OutputInfo[] = [];
142  diagnostics.forEach((diagnostic: ts.Diagnostic) => {
143    const { line, character }: ts.LineAndCharacter =
144      diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
145    outputInfo.push({
146      categoryInfo: diagnostic.category === ts.DiagnosticCategory.Error ? 'Error' : 'Warning',
147      fileName: diagnostic.file?.fileName,
148      line: line + 1,
149      character: character + 1,
150      messageText: diagnostic.messageText
151    });
152  });
153  let output: string | undefined = filePath;
154  try {
155    fs.writeFileSync(filePath, JSON.stringify(outputInfo, undefined, spaceNumBeforeJsonLine));
156  } catch {
157    output = undefined;
158  }
159  return output;
160}
161
162function removeOutputFile(): void {
163  let filePath: string = toUnixPath(projectConfig.cachePath);
164  if (!fs.existsSync(filePath)) {
165    return;
166  }
167  filePath = toUnixPath(path.join(filePath, arkTSDir));
168  if (!fs.existsSync(filePath)) {
169    return;
170  }
171  filePath = toUnixPath((path.join(filePath, arkTSLinterOutputFileName)));
172  if (fs.existsSync(filePath)) {
173    fs.rmSync(filePath);
174  }
175}
176
177function printArkTSLinterFAQ(diagnostics: ts.Diagnostic[], printDiagnostic: ProcessDiagnosticsFunc): void {
178  if (diagnostics === undefined || diagnostics.length === undefined || diagnostics.length <= 0) {
179    return;
180  }
181
182  const logMessageFAQ = 'For details about ArkTS syntax errors, see FAQs';
183  const arkTSFAQDiagnostic: ts.Diagnostic = {
184    file: undefined,
185    start: undefined,
186    length: undefined,
187    messageText: logMessageFAQ,
188    category: ts.DiagnosticCategory.Warning,
189    code: -1,
190    reportsUnnecessary: undefined,
191    reportsDeprecated: undefined
192  };
193  printDiagnostic(arkTSFAQDiagnostic);
194}
195
196function setCompilerOptions(originProgram: ts.Program, wasStrict: boolean): ts.CompilerOptions {
197  const compilerOptions: ts.CompilerOptions = { ...originProgram.getCompilerOptions() };
198  const inversedOptions = getStrictOptions(wasStrict);
199
200  Object.assign(compilerOptions, inversedOptions);
201  compilerOptions.allowJs = true;
202  compilerOptions.checkJs = true;
203  compilerOptions.tsBuildInfoFile = path.resolve(projectConfig.cachePath, '..', ARKTS_LINTER_BUILD_INFO_SUFFIX);
204
205  return compilerOptions;
206}
207
208export function getReverseStrictBuilderProgram(rollupShareObject: any, originProgram: ts.Program,
209  wasStrict: boolean): ts.BuilderProgram {
210  let cacheManagerKey: string = getRollupCacheStoreKey(projectConfig);
211  let cacheServiceKey: string = getRollupCacheKey(projectConfig) + '#' + 'linter_service';
212
213  let cache: LanguageServiceCache | undefined =
214    rollupShareObject?.cacheStoreManager?.mount(cacheManagerKey).getCache(cacheServiceKey);
215  let service: ts.LanguageService | undefined = cache?.service;
216  const currentHash: string | undefined = rollupShareObject?.projectConfig?.pkgJsonFileHash;
217  const lastHash: string | undefined= cache?.pkgJsonFileHash;
218  const shouldRebuild: boolean | undefined = currentHash && lastHash && currentHash !== lastHash;
219  if (!service || shouldRebuild) {
220    // Create language service for linter
221    // Revert strict options for linter program
222    const compilerOptions: ts.CompilerOptions = setCompilerOptions(originProgram, !wasStrict);
223    const servicesHost: ts.LanguageServiceHost = {
224      getScriptFileNames: () => [...originProgram.getRootFileNames()],
225      getScriptVersion: fileHashScriptVersion,
226      getScriptSnapshot: fileName => {
227        if (!fs.existsSync(fileName)) {
228          return undefined;
229        }
230        return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString());
231      },
232      getCurrentDirectory: () => process.cwd(),
233      getCompilationSettings: () => compilerOptions,
234      getDefaultLibFileName: options => ts.getDefaultLibFilePath(options),
235      fileExists: ts.sys.fileExists,
236      readFile: ts.sys.readFile,
237      readDirectory: ts.sys.readDirectory,
238      resolveModuleNames: resolveModuleNames,
239      resolveTypeReferenceDirectives: resolveTypeReferenceDirectives,
240      directoryExists: ts.sys.directoryExists,
241      getDirectories: ts.sys.getDirectories,
242      getFileCheckedModuleInfo: (containFilePath: string) => {
243        return {
244          fileNeedCheck: true,
245          checkPayload: undefined,
246          currentFileName: containFilePath,
247        };
248      }
249    };
250
251    service = ts.createLanguageService(servicesHost, ts.createDocumentRegistry());
252  }
253
254  service.updateRootFiles([...originProgram.getRootFileNames()]);
255  const newCache: LanguageServiceCache = {service: service, pkgJsonFileHash: currentHash};
256  rollupShareObject?.cacheStoreManager?.mount(cacheManagerKey).setCache(cacheServiceKey, newCache);
257
258  return service.getBuilderProgram();
259}
260
261function getStrictOptions(strict = true): object {
262  return {
263    strictNullChecks: strict,
264    strictFunctionTypes: strict,
265    strictPropertyInitialization: strict,
266    noImplicitReturns: strict,
267  };
268}
269
270/**
271 * Returns true if options were initially strict
272 */
273export function wasOptionsStrict(compilerOptions: ts.CompilerOptions): boolean {
274  const strictOptions = getStrictOptions();
275  let wasStrict = false;
276  Object.keys(strictOptions).forEach(x => {
277    wasStrict = wasStrict || !!compilerOptions[x];
278  });
279  // wasStrict evaluates true if any of the strict options was set
280  return wasStrict;
281}
282