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