* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as arkts from '@koalaui/libarkts';
import { PluginContext, PluginHandler, Plugins } from '../common/plugin-context';
import {
CheckedUISyntaxLinterTransformer,
ParsedUISyntaxLinterTransformer,
} from './transformers/ui-syntax-linter-transformer';
import { createUISyntaxRuleProcessor, UISyntaxRuleProcessor } from './processor';
import { UISyntaxLinterVisitor } from './transformers/ui-syntax-linter-visitor';
import rules from './rules';
import { getConsistentResourceMap, getMainPages, getUIComponents, matchPrefix } from '../common/arkts-utils';
import { EXCLUDE_EXTERNAL_SOURCE_PREFIXES, tracePerformance } from './utils';
import { Debugger, debugLog, getDumpFileName } from '../common/debug';
import { UIVisitor } from '../collectors/ui-collectors/ui-visitor';
import { MemoVisitor } from '../collectors/memo-collectors/memo-visitor';
import { Collector } from '../collectors/collector';
import { ProgramVisitor } from '../common/program-visitor';
import { EXTERNAL_SOURCE_PREFIX_NAMES, NodeCacheNames } from '../common/predefines';
import { ProgramSkipper } from '../common/program-skipper';
import { MetaDataCollector } from '../common/metadata-collector';
import { NodeCacheFactory } from '../common/node-cache';
export function uiSyntaxLinterTransform(): Plugins {
return {
name: 'ui-syntax-plugin',
checked: collectAndLint,
clean(): void {
ProgramSkipper.clear();
NodeCacheFactory.getInstance().clear();
visitedPrograms.clear();
visitedExternalSources.clear();
},
};
}
function collectAndLint(this: PluginContext): arkts.ETSModule | undefined {
Debugger.getInstance().phasesDebugLog('[UI LINTER PLUGIN] AFTER CHECKED ENTER');
const contextPtr = this.getContextPtr() ?? arkts.arktsGlobal.compilerContext?.peer;
const isCoding = this.isCoding?.() ?? false;
if (!!contextPtr) {
let program = arkts.getOrUpdateGlobalContext(contextPtr).program;
let script = program.ast as arkts.ETSModule;
debugLog('[BEFORE LINTER SCRIPT] script: ', script);
arkts.Performance.getInstance().createEvent('ui-linter');
program = checkedProgramVisit(program, this, isCoding);
script = program.ast as arkts.ETSModule;
arkts.Performance.getInstance().stopEvent('ui-linter', true);
debugLog('[AFTER LINTER SCRIPT] script: ', script);
this.setArkTSAst(script);
arkts.Performance.getInstance().logDetailedEventInfos(true);
Debugger.getInstance().phasesDebugLog('[UI LINTER PLUGIN] AFTER CHECKED EXIT');
return script;
}
Debugger.getInstance().phasesDebugLog('[UI LINTER PLUGIN] AFTER CHECKED EXIT WITH NO TRANSFORM');
return undefined;
}
function checkedProgramVisit(
program: arkts.Program,
context: PluginContext,
isCoding: boolean = false
): arkts.Program {
if (isCoding) {
NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).shouldCollect(false);
NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).shouldCollect(false);
} else {
NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).shouldCollectUpdate(true);
NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).shouldCollectUpdate(true);
}
const projectConfig = context.getProjectConfig();
arkts.Performance.getInstance().createDetailedEvent('[UI LINTER PLUGIN] MetadataCollector init');
MetaDataCollector.getInstance()
.setProjectConfig(projectConfig)
.setComponentsInfo(getUIComponents(projectConfig, isCoding))
.setConsistentResourceMap(getConsistentResourceMap())
.setMainPages(getMainPages(projectConfig));
arkts.Performance.getInstance().stopDetailedEvent('[UI LINTER PLUGIN] MetadataCollector init');
const collector = new Collector({
shouldCollectUI: true,
shouldCollectMemo: true,
shouldCheckUISyntax: true
});
const programVisitor = new ProgramVisitor({
pluginName: uiSyntaxLinterTransform.name,
state: arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED,
visitors: [collector],
skipPrefixNames: EXTERNAL_SOURCE_PREFIX_NAMES,
pluginContext: context,
shouldVisitExternal: !isCoding,
});
program = programVisitor.programVisitor(program);
MetaDataCollector.getInstance()
.setComponentsInfo(undefined)
.setConsistentResourceMap(undefined)
.setMainPages(undefined);
if (!isCoding) {
NodeCacheFactory.getInstance().perfLog(NodeCacheNames.UI, true);
NodeCacheFactory.getInstance().perfLog(NodeCacheNames.MEMO, true);
}
return program;
}
function createTransformer(
phase: string,
processor: UISyntaxRuleProcessor,
transformer: UISyntaxLinterVisitor
): PluginHandler {
const visitedPrograms: Set<arkts.KNativePointer> = new Set();
const visitedExternalSources: Set<arkts.KNativePointer> = new Set();
return tracePerformance(`UISyntaxPlugin::${phase}`, function (this: PluginContext): arkts.ETSModule | undefined {
const contextPtr = this.getContextPtr() ?? arkts.arktsGlobal.compilerContext?.peer;
if (!contextPtr) {
return undefined;
}
const projectConfig = this.getProjectConfig();
if (!projectConfig) {
return undefined;
}
processor.setProjectConfig(projectConfig);
if (projectConfig.frameworkMode) {
return undefined;
}
const program = arkts.getOrUpdateGlobalContext(contextPtr).program;
if (visitedPrograms.has(program.peer) || isHeaderFile(program.absoluteName)) {
return undefined;
}
const isCoding = this.isCoding?.() ?? false;
processor.setComponentsInfo(projectConfig, isCoding);
if (isCoding) {
const codingFilePath = this.getCodingFilePath();
if (program.absoluteName === codingFilePath) {
return transformProgram.call(this, transformer, program);
}
} else {
transformExternalSources.call(this, program, visitedExternalSources, visitedPrograms, transformer);
if (program.absoluteName) {
return transformProgram.call(this, transformer, program);
}
}
visitedPrograms.add(program.peer);
return undefined;
});
}
function transformExternalSources(
this: PluginContext,
program: arkts.Program,
visitedExternalSources: Set<arkts.KNativePointer>,
visitedPrograms: Set<arkts.KNativePointer>,
transformer: UISyntaxLinterVisitor
): void {
const externalSources = program.getExternalSources();
for (const externalSource of externalSources) {
if (matchPrefix(EXCLUDE_EXTERNAL_SOURCE_PREFIXES, externalSource.getName())) {
continue;
}
if (visitedExternalSources.has(externalSource.peer)) {
continue;
}
const programs = externalSource.programs;
for (const program of programs) {
if (!program.isBuiltSimultaneously || visitedPrograms.has(program.peer) || isHeaderFile(program.absoluteName)) {
continue;
}
const script = transformer.transform(program.ast) as arkts.ETSModule;
this.setArkTSAst(script);
}
visitedExternalSources.add(externalSource.peer);
}
}
const visitedPrograms: Set<any> = new Set();
const visitedExternalSources: Set<any> = new Set();
function parsedTransform(this: PluginContext): arkts.ETSModule | undefined {
const isCoding = this.isCoding?.() ?? false;
arkts.Performance.getInstance().createEvent(`ui-syntax::parsed`);
const processor = createUISyntaxRuleProcessor(rules);
const transformer = new ParsedUISyntaxLinterTransformer(processor);
const contextPtr = this.getContextPtr() ?? arkts.arktsGlobal.compilerContext?.peer;
if (!contextPtr) {
return undefined;
}
const projectConfig = this.getProjectConfig();
if (!projectConfig) {
return undefined;
}
processor.setProjectConfig(projectConfig);
if (projectConfig.frameworkMode) {
return undefined;
}
const program = arkts.getOrUpdateGlobalContext(contextPtr).program;
if (visitedPrograms.has(program.peer) || isHeaderFile(program.absoluteName)) {
return undefined;
}
processor.setComponentsInfo(projectConfig, isCoding);
if (isCoding) {
const codingFilePath = this.getCodingFilePath();
if (program.absoluteName === codingFilePath) {
return transformProgram.call(this, transformer, program);
}
} else {
transformExternalSources.call(this, program, visitedExternalSources, visitedPrograms, transformer);
if (program.absoluteName) {
return transformProgram.call(this, transformer, program);
}
}
visitedPrograms.add(program.peer);
arkts.Performance.getInstance().stopEvent(`ui-syntax::parsed`, true);
return undefined;
}
function transformProgram(
this: PluginContext,
transformer: UISyntaxLinterVisitor,
program: arkts.Program
): arkts.ETSModule {
const script = transformer.transform(program.ast) as arkts.ETSModule;
this.setArkTSAst(script);
return script;
}
function isHeaderFile(fileName: string): boolean {
return fileName.endsWith('.d.ets');
}