1/* 2 * Copyright (c) 2022 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'; 19 20import { 21 projectConfig, 22 systemModules 23} from '../main'; 24import { 25 processSystemApi, 26 preprocessExtend, 27 preprocessNewExtend 28} from './validate_ui_syntax'; 29import { 30 INNER_COMPONENT_MEMBER_DECORATORS, 31 COMPONENT_IF, 32 COMPONENT_DECORATORS_PARAMS, 33 COMPONENT_BUILD_FUNCTION, 34 STYLE_ADD_DOUBLE_DOLLAR, 35 $$, 36 PROPERTIES_ADD_DOUBLE_DOLLAR 37} from './pre_define'; 38import { JS_BIND_COMPONENTS } from './component_map'; 39import { getName } from './process_component_build'; 40import { INNER_COMPONENT_NAMES } from './component_map'; 41import { 42 props, 43 CacheFileName, 44 cache, 45 shouldResolvedFiles 46} from './compile_info'; 47 48function readDeaclareFiles(): string[] { 49 const declarationsFileNames: string[] = []; 50 fs.readdirSync(path.resolve(__dirname, '../declarations')) 51 .forEach((fileName: string) => { 52 if (/\.d\.ts$/.test(fileName)) { 53 declarationsFileNames.push(path.resolve(__dirname, '../declarations', fileName)); 54 } 55 }); 56 return declarationsFileNames; 57} 58 59const compilerOptions: ts.CompilerOptions = ts.readConfigFile( 60 path.resolve(__dirname, '../tsconfig.json'), ts.sys.readFile).config.compilerOptions; 61function setCompilerOptions() { 62 const allPath: Array<string> = [ 63 '*', 64 ] 65 if (!projectConfig.aceModuleJsonPath) { 66 allPath.push('../../../../../*'); 67 allPath.push('../../*'); 68 } else { 69 allPath.push('../../../../*'); 70 allPath.push('../*'); 71 } 72 Object.assign(compilerOptions, { 73 'allowJs': false, 74 'importsNotUsedAsValues': ts.ImportsNotUsedAsValues.Preserve, 75 'module': ts.ModuleKind.CommonJS, 76 'moduleResolution': ts.ModuleResolutionKind.NodeJs, 77 'noEmit': true, 78 'target': ts.ScriptTarget.ES2017, 79 'baseUrl': path.resolve(projectConfig.projectPath), 80 'paths': { 81 '*': allPath 82 }, 83 'lib': [ 84 'lib.es2020.d.ts' 85 ] 86 }); 87} 88 89export function createLanguageService(rootFileNames: string[]): ts.LanguageService { 90 setCompilerOptions(); 91 const files: ts.MapLike<{ version: number }> = {}; 92 const servicesHost: ts.LanguageServiceHost = { 93 getScriptFileNames: () => [...rootFileNames, ...readDeaclareFiles()], 94 getScriptVersion: fileName => 95 files[fileName] && files[fileName].version.toString(), 96 getScriptSnapshot: fileName => { 97 if (!fs.existsSync(fileName)) { 98 return undefined; 99 } 100 if (/(?<!\.d)\.(ets|ts)$/.test(fileName)) { 101 const content: string = processContent(fs.readFileSync(fileName).toString()); 102 checkUISyntax(content, fileName); 103 return ts.ScriptSnapshot.fromString(content); 104 } 105 return ts.ScriptSnapshot.fromString(fs.readFileSync(fileName).toString()); 106 }, 107 getCurrentDirectory: () => process.cwd(), 108 getCompilationSettings: () => compilerOptions, 109 getDefaultLibFileName: options => ts.getDefaultLibFilePath(options), 110 fileExists: ts.sys.fileExists, 111 readFile: ts.sys.readFile, 112 readDirectory: ts.sys.readDirectory, 113 resolveModuleNames: resolveModuleNames, 114 directoryExists: ts.sys.directoryExists, 115 getDirectories: ts.sys.getDirectories 116 }; 117 return ts.createLanguageService(servicesHost, ts.createDocumentRegistry()); 118} 119 120const resolvedModulesCache: Map<string, ts.ResolvedModuleFull[]> = new Map(); 121 122function resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModuleFull[] { 123 const resolvedModules: ts.ResolvedModuleFull[] = []; 124 if (![...shouldResolvedFiles].length || shouldResolvedFiles.has(path.resolve(containingFile)) || 125 !(resolvedModulesCache[path.resolve(containingFile)] && 126 resolvedModulesCache[path.resolve(containingFile)].length === moduleNames.length)) { 127 for (const moduleName of moduleNames) { 128 const result = ts.resolveModuleName(moduleName, containingFile, compilerOptions, { 129 fileExists(fileName: string): boolean { 130 return ts.sys.fileExists(fileName); 131 }, 132 readFile(fileName: string): string | undefined { 133 return ts.sys.readFile(fileName); 134 } 135 }); 136 if (result.resolvedModule) { 137 resolvedModules.push(result.resolvedModule); 138 } else if (/^@(system|ohos)/i.test(moduleName.trim())) { 139 const modulePath: string = path.resolve(__dirname, '../../../api', moduleName + '.d.ts'); 140 if (systemModules.includes(moduleName + '.d.ts') && ts.sys.fileExists(modulePath)) { 141 resolvedModules.push(getResolveModule(modulePath, '.d.ts')); 142 } else { 143 resolvedModules.push(null); 144 } 145 } else if (/\.ets$/.test(moduleName)) { 146 const modulePath: string = path.resolve(path.dirname(containingFile), moduleName); 147 if (ts.sys.fileExists(modulePath)) { 148 resolvedModules.push(getResolveModule(modulePath, '.ets')); 149 } else { 150 resolvedModules.push(null); 151 } 152 } else if (/\.ts$/.test(moduleName)) { 153 const modulePath: string = path.resolve(path.dirname(containingFile), moduleName); 154 if (ts.sys.fileExists(modulePath)) { 155 resolvedModules.push(getResolveModule(modulePath, '.ts')); 156 } else { 157 resolvedModules.push(null); 158 } 159 } else { 160 const modulePath: string = path.resolve(__dirname, '../../../api', moduleName + '.d.ts'); 161 const suffix: string = /\.js$/.test(moduleName) ? '' : '.js'; 162 const jsModulePath: string = path.resolve(__dirname, '../node_modules', moduleName + suffix); 163 if (ts.sys.fileExists(modulePath)) { 164 resolvedModules.push(getResolveModule(modulePath, '.d.ts')); 165 } else if (ts.sys.fileExists(jsModulePath)) { 166 resolvedModules.push(getResolveModule(modulePath, '.js')); 167 } else { 168 resolvedModules.push(null); 169 } 170 } 171 } 172 if (!projectConfig.xtsMode) { 173 createOrUpdateCache(resolvedModules, path.resolve(containingFile)); 174 } 175 resolvedModulesCache[path.resolve(containingFile)] = resolvedModules; 176 return resolvedModules; 177 } 178 return resolvedModulesCache[path.resolve(containingFile)]; 179} 180 181function createOrUpdateCache(resolvedModules: ts.ResolvedModuleFull[], containingFile: string): void { 182 const children: string[] = []; 183 const error: boolean = false; 184 resolvedModules.forEach(moduleObj => { 185 if (moduleObj && moduleObj.resolvedFileName && /(?<!\.d)\.(ets|ts)$/.test(moduleObj.resolvedFileName)) { 186 const file: string = path.resolve(moduleObj.resolvedFileName); 187 const mtimeMs: number = fs.statSync(file).mtimeMs; 188 children.push(file); 189 const value: CacheFileName = cache[file]; 190 if (value) { 191 value.mtimeMs = mtimeMs; 192 value.error = error; 193 value.parent = value.parent || []; 194 value.parent.push(path.resolve(containingFile)); 195 value.parent = [...new Set(value.parent)]; 196 } else { 197 cache[file] = { mtimeMs, children: [], parent: [containingFile], error }; 198 } 199 } 200 }); 201 cache[path.resolve(containingFile)] = { mtimeMs: fs.statSync(containingFile).mtimeMs, children, 202 parent: cache[path.resolve(containingFile)] && cache[path.resolve(containingFile)].parent ? 203 cache[path.resolve(containingFile)].parent : [], error }; 204} 205 206export function createWatchCompilerHost(rootFileNames: string[], 207 reportDiagnostic: ts.DiagnosticReporter, delayPrintLogCount: Function, resetErrorCount: Function, 208 isPipe: boolean = false): ts.WatchCompilerHostOfFilesAndCompilerOptions<ts.BuilderProgram> { 209 setCompilerOptions(); 210 const createProgram = ts.createSemanticDiagnosticsBuilderProgram; 211 const host = ts.createWatchCompilerHost( 212 [...rootFileNames, ...readDeaclareFiles()], compilerOptions, 213 ts.sys, createProgram, reportDiagnostic, 214 (diagnostic: ts.Diagnostic) => { 215 if ([6031, 6032].includes(diagnostic.code)) { 216 if (!isPipe) { 217 process.env.watchTs = 'start'; 218 resetErrorCount(); 219 } 220 } 221 // End of compilation in watch mode flag. 222 if ([6193, 6194].includes(diagnostic.code)) { 223 if (!isPipe) { 224 process.env.watchTs = 'end'; 225 } 226 delayPrintLogCount(); 227 } 228 }); 229 host.readFile = (fileName: string) => { 230 if (!fs.existsSync(fileName)) { 231 return undefined; 232 } 233 if (/(?<!\.d)\.(ets|ts)$/.test(fileName)) { 234 const content: string = processContent(fs.readFileSync(fileName).toString()); 235 checkUISyntax(content, fileName); 236 return content; 237 } 238 return fs.readFileSync(fileName).toString(); 239 }; 240 host.resolveModuleNames = resolveModuleNames; 241 return host; 242} 243 244function getResolveModule(modulePath: string, type): ts.ResolvedModuleFull { 245 return { 246 resolvedFileName: modulePath, 247 isExternalLibraryImport: false, 248 extension: type 249 }; 250} 251 252export const dollarCollection: Set<string> = new Set(); 253export const appComponentCollection: Set<string> = new Set(); 254export const decoratorParamsCollection: Set<string> = new Set(); 255export const extendCollection: Set<string> = new Set(); 256export const importModuleCollection: Set<string> = new Set(); 257 258function checkUISyntax(source: string, fileName: string): void { 259 if (/\.ets$/.test(fileName)) { 260 if (process.env.compileMode === 'moduleJson' || 261 path.resolve(fileName) !== path.resolve(projectConfig.projectPath, 'app.ets')) { 262 const sourceFile: ts.SourceFile = ts.createSourceFile(fileName, source, 263 ts.ScriptTarget.Latest, true, ts.ScriptKind.ETS); 264 collectComponents(sourceFile); 265 parseAllNode(sourceFile, sourceFile); 266 props.push(...dollarCollection, ...decoratorParamsCollection, ...extendCollection); 267 } 268 } 269} 270 271function collectComponents(node: ts.SourceFile): void { 272 // @ts-ignore 273 if (process.env.watchMode !== 'true' && node.identifiers && node.identifiers.size) { 274 // @ts-ignore 275 for (const key of node.identifiers.keys()) { 276 if (JS_BIND_COMPONENTS.has(key)) { 277 appComponentCollection.add(key); 278 } 279 } 280 } 281} 282 283function parseAllNode(node: ts.Node, sourceFileNode: ts.SourceFile): void { 284 if (ts.isStructDeclaration(node)) { 285 if (node.members) { 286 node.members.forEach(item => { 287 if (ts.isPropertyDeclaration(item) && ts.isIdentifier(item.name)) { 288 const propertyName: string = item.name.getText(); 289 if (item.decorators && item.decorators.length) { 290 for (let i = 0; i < item.decorators.length; i++) { 291 const decoratorName: string = item.decorators[i].getText().replace(/\(.*\)$/, '').trim(); 292 if (INNER_COMPONENT_MEMBER_DECORATORS.has(decoratorName)) { 293 dollarCollection.add('$' + propertyName); 294 } 295 if (isDecoratorCollection(item.decorators[i], decoratorName)) { 296 decoratorParamsCollection.add(item.decorators[i].expression.arguments[0].getText()); 297 } 298 } 299 } 300 } 301 }); 302 } 303 } 304 if (ts.isIfStatement(node)) { 305 appComponentCollection.add(COMPONENT_IF); 306 } 307 if (ts.isMethodDeclaration(node) && node.name.getText() === COMPONENT_BUILD_FUNCTION) { 308 if (node.body && node.body.statements && node.body.statements.length) { 309 const checkProp: ts.NodeArray<ts.Statement> = node.body.statements; 310 checkProp.forEach((item, index) => { 311 traverseBuild(item, index); 312 }); 313 } 314 } 315 node.getChildren().forEach((item: ts.Node) => parseAllNode(item, sourceFileNode)); 316} 317 318function traverseBuild(node: ts.Node, index: number): void { 319 if (ts.isExpressionStatement(node)) { 320 let parentComponentName: string = getName(node); 321 if (!INNER_COMPONENT_NAMES.has(parentComponentName) && node.parent && node.parent.statements && index >= 1 && 322 node.parent.statements[index - 1].expression && node.parent.statements[index - 1].expression.expression) { 323 parentComponentName = node.parent.statements[index - 1].expression.expression.escapedText; 324 } 325 node = node.expression; 326 if (ts.isEtsComponentExpression(node) && node.body && ts.isBlock(node.body)) { 327 node.body.statements.forEach((item, indexBlock) => { 328 traverseBuild(item, indexBlock); 329 }); 330 } else { 331 loopNodeFindDoubleDollar(node, parentComponentName); 332 } 333 } else if (ts.isIfStatement(node)) { 334 if (node.thenStatement && ts.isBlock(node.thenStatement) && node.thenStatement.statements) { 335 node.thenStatement.statements.forEach((item, indexIfBlock) => { 336 traverseBuild(item, indexIfBlock); 337 }); 338 } 339 if (node.elseStatement && ts.isBlock(node.elseStatement) && node.elseStatement.statements) { 340 node.elseStatement.statements.forEach((item, indexElseBlock) => { 341 traverseBuild(item, indexElseBlock); 342 }); 343 } 344 } 345} 346 347function loopNodeFindDoubleDollar(node: ts.Node, parentComponentName: string): void { 348 while (node) { 349 if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { 350 const argument: ts.NodeArray<ts.Node> = node.arguments; 351 const propertyName: ts.Identifier | ts.PrivateIdentifier = node.expression.name; 352 if (isCanAddDoubleDollar(propertyName.getText(), parentComponentName)) { 353 argument.forEach((item: ts.Node) => { 354 doubleDollarCollection(item); 355 }); 356 } 357 } else if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.arguments 358 && node.arguments.length) { 359 node.arguments.forEach((item: ts.Node) => { 360 if (ts.isObjectLiteralExpression(item) && item.properties && item.properties.length) { 361 item.properties.forEach((param: ts.Node) => { 362 if (isObjectPram(param, parentComponentName)) { 363 doubleDollarCollection(param.initializer); 364 } 365 }); 366 } 367 if (STYLE_ADD_DOUBLE_DOLLAR.has(node.expression.getText()) && ts.isPropertyAccessExpression(item)) { 368 doubleDollarCollection(item); 369 } 370 }); 371 } 372 node = node.expression; 373 } 374} 375 376function doubleDollarCollection(item: ts.Node): void { 377 if (item.getText().startsWith($$)) { 378 while (item.expression) { 379 item = item.expression; 380 } 381 dollarCollection.add(item.getText()); 382 } 383} 384 385function isObjectPram(param: ts.Node, parentComponentName:string): boolean { 386 return ts.isPropertyAssignment(param) && param.name && ts.isIdentifier(param.name) && 387 param.initializer && PROPERTIES_ADD_DOUBLE_DOLLAR.has(parentComponentName) && 388 PROPERTIES_ADD_DOUBLE_DOLLAR.get(parentComponentName).has(param.name.getText()); 389} 390 391function isCanAddDoubleDollar(propertyName: string, parentComponentName: string): boolean { 392 return PROPERTIES_ADD_DOUBLE_DOLLAR.has(parentComponentName) && 393 PROPERTIES_ADD_DOUBLE_DOLLAR.get(parentComponentName).has(propertyName) || 394 STYLE_ADD_DOUBLE_DOLLAR.has(propertyName); 395} 396 397function isDecoratorCollection(item: ts.Decorator, decoratorName: string): boolean { 398 return COMPONENT_DECORATORS_PARAMS.has(decoratorName) && 399 // @ts-ignore 400 item.expression.arguments && item.expression.arguments.length && 401 // @ts-ignore 402 ts.isIdentifier(item.expression.arguments[0]); 403} 404 405function processDraw(source: string): string { 406 const reg: RegExp = /new\s+\b(Circle|Ellipse|Rect|Path)\b/g; 407 return source.replace(reg, (item:string, item1: string) => { 408 return '\xa0'.repeat(item.length - item1.length) + item1; 409 }); 410} 411 412function processContent(source: string): string { 413 source = processSystemApi(source); 414 source = preprocessExtend(source, extendCollection); 415 source = preprocessNewExtend(source, extendCollection); 416 source = processDraw(source); 417 return source; 418} 419