diff --git a/src/cli/base.ts b/src/cli/base.ts index 2ca3b2672..276e68471 100644 --- a/src/cli/base.ts +++ b/src/cli/base.ts @@ -12,7 +12,7 @@ import ConsoleWriter from "@ui5/logger/writers/Console"; export interface LinterArg { coverage: boolean; - filePaths: string[]; + filePaths?: string[]; details: boolean; format: string; } @@ -120,9 +120,9 @@ async function handleLint(argv: ArgumentsCamelCase) { const res = await lintProject({ rootDir: path.join(process.cwd()), - filePaths: filePaths?.map((filePath) => path.resolve(process.cwd(), filePath)), + pathsToLint: filePaths?.map((filePath) => path.resolve(process.cwd(), filePath)), reportCoverage, - messageDetails: details, + includeMessageDetails: details, }); if (reportCoverage) { diff --git a/src/detectors/AbstractDetector.ts b/src/detectors/AbstractDetector.ts deleted file mode 100644 index e95fd1de5..000000000 --- a/src/detectors/AbstractDetector.ts +++ /dev/null @@ -1,69 +0,0 @@ -import {Project} from "@ui5/project"; - -// Data types are structured very similar to the ESLint types for better compatibility into existing integrations: -// https://eslint.org/docs/latest/integrate/nodejs-api#-lintresult-type -export interface LintResult { - filePath: string; - messages: LintMessage[]; - coverageInfo: CoverageInfo[]; - errorCount: number; // includes fatal errors - fatalErrorCount: number; - warningCount: number; -} - -export enum LintMessageSeverity { - Warning = 1, - Error = 2, -} - -export interface LintMessage { - ruleId: string; - severity: LintMessageSeverity; - message: string; - messageDetails?: string; - fatal?: boolean | undefined; // e.g. parsing error - line?: number | undefined; // 1 based to be aligned with most IDEs - column?: number | undefined; // 1 based to be aligned with most IDEs - endLine?: number | undefined; - endColumn?: number | undefined; -} - -export enum CoverageCategory { - CallExpressionUnknownType = 1, -} - -export interface CoverageInfo { - category: CoverageCategory; - message: string; - messageDetails?: string; - line?: number | undefined; // 1 based to be aligned with most IDEs - column?: number | undefined; // 1 based to be aligned with most IDEs - endLine?: number | undefined; - endColumn?: number | undefined; -} - -// export interface DetectorCapabilities { -// // TODO: Expose supported file types and names -// } - -abstract class AbstractDetector { - abstract createReports(filePaths: string[]): Promise; - // abstract getCapabilities(): DetectorCapabilities -} -export abstract class FileBasedDetector extends AbstractDetector { - rootDir: string; - - constructor(rootDir: string) { - super(); - this.rootDir = rootDir; - } -} - -export abstract class ProjectBasedDetector extends AbstractDetector { - project: Project; - - constructor(project: Project) { - super(); - this.project = project; - } -} diff --git a/src/detectors/BaseReporter.ts b/src/detectors/BaseReporter.ts deleted file mode 100644 index 7682fa1e7..000000000 --- a/src/detectors/BaseReporter.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type ts from "typescript"; -import type {LintMessage, CoverageInfo, LintResult} from "../detectors/AbstractDetector.js"; -import type {Tag as SaxTag} from "sax-wasm"; - -export interface BaseReporter { - addMessage(args: ReporterMessage): void; - addCoverageInfo(args: ReporterCoverageInfo): void; - getReport(): LintResult; -} - -export interface ReporterMessage { - node?: ts.Node | SaxTag | string; - message: LintMessage["message"]; - messageDetails?: LintMessage["messageDetails"]; - severity: LintMessage["severity"]; - ruleId: LintMessage["ruleId"]; - fatal?: LintMessage["fatal"]; -} - -export interface PositionInfo { - line: number; - column: number; -} - -export interface PositionRange { - start: PositionInfo; - end?: PositionInfo; -} - -export interface ReporterCoverageInfo { - node: ts.Node | string; - message: CoverageInfo["message"]; - messageDetails?: CoverageInfo["messageDetails"]; - category: CoverageInfo["category"]; -} diff --git a/src/detectors/transpilers/AbstractTranspiler.ts b/src/detectors/transpilers/AbstractTranspiler.ts deleted file mode 100644 index 179df8e35..000000000 --- a/src/detectors/transpilers/AbstractTranspiler.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {LintMessage} from "../AbstractDetector.js"; - -export interface TranspileResult { - source: string; - map: string; - messages?: LintMessage[]; -} diff --git a/src/detectors/transpilers/amd/transpiler.ts b/src/detectors/transpilers/amd/transpiler.ts deleted file mode 100644 index c2d56f9ac..000000000 --- a/src/detectors/transpilers/amd/transpiler.ts +++ /dev/null @@ -1,153 +0,0 @@ -import ts from "typescript"; -import {getLogger} from "@ui5/logger"; -import path from "node:path/posix"; -import {taskStart} from "../../util/perf.js"; -import {TranspileResult} from "../AbstractTranspiler.js"; -import {createTransformer} from "./TsTransformer.js"; -import {UnsupportedModuleError} from "./util.js"; - -const log = getLogger("transpilers:amd:transpiler"); - -export function amdToEsm(moduleId: string, content: string, strict?: boolean): TranspileResult { - try { - const taskEnd = taskStart("Transform JS", moduleId, true); - const esmContent = transpile(moduleId, content, strict); - taskEnd(); - if (!esmContent.source) { - log.verbose(`ESM transpiler returned no result for ${moduleId}`); - return {source: content, map: ""}; - } - return esmContent; - } catch (err) { - if (err instanceof Error) { - throw new Error(`Failed to transpile module ${moduleId}: ${err.message}`, { - cause: err, - }); - } else { - throw err; - } - } -} - -function transpile(resourcePath: string, content: string, strict?: boolean): TranspileResult { - // This is heavily inspired by the TypesScript "transpileModule" API, - // which sadly does not expose the program instance, which we need to access the type checker - const moduleName = path.basename(resourcePath, ".js"); - const inputFileName = `${moduleName}.js`; - const outputFileName = `${moduleName}.ts`; - const sourceFile = ts.createSourceFile( - inputFileName, - content, - { - languageVersion: ts.ScriptTarget.ES2022, - jsDocParsingMode: ts.JSDocParsingMode.ParseNone, - } - ); - - // Output - let outputText: string | undefined; - let sourceMapText: string | undefined; - let sourceMapName: string | undefined; - - const compilerOptions = { - moduleResolution: ts.ModuleResolutionKind.NodeNext, - checkJs: true, - allowJs: true, - skipLibCheck: true, - - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.ES2022, - isolatedModules: true, - sourceMap: true, - suppressOutputPathCheck: true, - noLib: true, - noResolve: true, - allowNonTsExtensions: true, - }; - - // TODO: Investigate whether it would be faster to create one host + program to transpile many files in - // one batch - const compilerHost: ts.CompilerHost = { - getSourceFile: (fileName) => fileName === inputFileName ? sourceFile : undefined, - writeFile: (name, text) => { - if (name.endsWith(".map")) { - if (sourceMapText) { - throw new Error(`Unexpected multiple source maps for module ${resourcePath}`); - } - sourceMapText = text; - sourceMapName = name; - } else { - if (outputText) { - throw new Error(`Unexpected multiple outputs for module ${resourcePath}`); - } - outputText = text; - } - }, - getDefaultLibFileName: () => "lib.d.ts", - useCaseSensitiveFileNames: () => false, - getCanonicalFileName: (fileName) => fileName, - getCurrentDirectory: () => "", - getNewLine: () => "\n", - fileExists: (fileName): boolean => fileName === inputFileName, - readFile: () => "", - directoryExists: () => true, - getDirectories: () => [], - }; - const program = ts.createProgram([inputFileName], compilerOptions, compilerHost); - - const transformers: ts.CustomTransformers = { - before: [createTransformer(resourcePath, program)], - }; - - try { - // ts.setEmitFlags(sourceFile, ts.EmitFlags.NoTrailingSourceMap); - // TODO: Investigate whether we can retrieve a source file that can be fed directly into the typeChecker - program.emit( - /* targetSourceFile */ undefined, /* writeFile */ undefined, - /* cancellationToken */ undefined, /* emitOnlyDtsFiles */ undefined, - transformers); - - /* tsc currently does not provide an API to emit TypeScript *with* a source map - (see https://github.com/microsoft/TypeScript/issues/51329) - - The below can be used to emit TypeScript without a source map: - - const result = ts.transform( - sourceFile, - [createTransformer(resourcePath, program)], compilerOptions); - - const printer = ts.createPrinter(); - const printed = printer.printNode(ts.EmitHint.SourceFile, result.transformed[0], sourceFile); - const printed = printer.printFile(result.transformed[0]); - outputText = printed; - */ - } catch (err) { - if (strict) { - throw err; - } - if (err instanceof UnsupportedModuleError) { - log.verbose(`Failed to transform module ${resourcePath}: ${err.message}`); - if (err.stack && log.isLevelEnabled("verbose")) { - log.verbose(`Stack trace:`); - log.verbose(err.stack); - } - return {source: content, map: ""}; - } else if (err instanceof Error && err.message.startsWith("Debug Failure")) { - // We probably failed to create a valid AST - log.verbose(`AST transformation failed for module ${resourcePath}: ${err.message}`); - if (err.stack && log.isLevelEnabled("verbose")) { - log.verbose(`Stack trace:`); - log.verbose(err.stack); - } - return {source: content, map: ""}; - } - throw err; - } - - if (outputText === undefined) throw new Error(`Transpiling yielded no result for ${resourcePath}`); - - // Convert sourceMappingURL ending with ".js" to ".ts" - outputText = outputText - .replace(`//# sourceMappingURL=${sourceMapName}`, `//# sourceMappingURL=${outputFileName}.map`); - return {source: outputText, map: sourceMapText!}; -} diff --git a/src/detectors/typeChecker/index.ts b/src/detectors/typeChecker/index.ts deleted file mode 100644 index 16f8e98cc..000000000 --- a/src/detectors/typeChecker/index.ts +++ /dev/null @@ -1,367 +0,0 @@ -import ts from "typescript"; -import path from "node:path"; -import fs from "node:fs"; -import {createAdapter, createResource} from "@ui5/fs/resourceFactory"; -import {createVirtualCompilerHost} from "./host.js"; -import FileLinter from "./FileLinter.js"; -import Reporter from "../Reporter.js"; -import {taskStart} from "../util/perf.js"; -import {amdToEsm} from "../transpilers/amd/transpiler.js"; -import {xmlToJs} from "../transpilers/xml/transpiler.js"; -import {lintManifest} from "../../linter/json/linter.js"; -import {lintHtml} from "../../linter/html/linter.js"; -import { - FileBasedDetector, LintMessage, LintMessageSeverity, LintResult, ProjectBasedDetector, -} from "../AbstractDetector.js"; -import {Project} from "@ui5/project"; -import {Resource} from "@ui5/fs"; - -const DEFAULT_OPTIONS: ts.CompilerOptions = { - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.ES2022, - moduleResolution: ts.ModuleResolutionKind.NodeNext, - // Skip lib check to speed up linting. Libs should generally be fine, - // we might want to add a unit test doing the check during development - skipLibCheck: true, - // Include standard typescript libraries for ES2022 and DOM support - lib: ["lib.es2022.d.ts", "lib.dom.d.ts"], - // Allow and check JavaScript files since this is everything we'll do here - allowJs: true, - checkJs: true, - strict: true, - noImplicitAny: false, - strictNullChecks: false, - strictPropertyInitialization: false, - rootDir: "/", - // Library modules (e.g. sap/ui/core/library.js) do not have a default export but - // instead have named exports e.g. for enums defined in the module. - // However, in current JavaScript code (UI5 AMD) the whole object is exported, as there are no - // named exports outside of ES Modules / TypeScript. - // This property compensates this gap and tries to all usage of default imports where actually - // no default export is defined. - // NOTE: This setting should not be used when analyzing TypeScript code, as it would allow - // using an default import on library modules, which is not intended. - // A better solution: - // During transpilation, for every library module (where no default export exists), - // an "import * as ABC" instead of a default import is created. - // This logic needs to be in sync with the generator for UI5 TypeScript definitions. - allowSyntheticDefaultImports: true, -}; - -export class TsProjectDetector extends ProjectBasedDetector { - compilerOptions: ts.CompilerOptions; - #projectBasePath: string; - - constructor(project: Project) { - super(project); - this.compilerOptions = {...DEFAULT_OPTIONS}; - - const namespace = project.getNamespace(); - this.#projectBasePath = `/resources/${namespace}/`; - this.compilerOptions.paths = { - [`${namespace}/*`]: [`${this.#projectBasePath}*`], - }; - } - - private async writeTransformedSources(fsBasePath: string, originalResourcePath: string, - source: string, map: string | undefined) { - const transformedWriter = createAdapter({ - fsBasePath, - virBasePath: "/", - }); - - await transformedWriter.write( - createResource({ - path: originalResourcePath + ".ui5lint.transformed.js", - string: source, - }) - ); - - if (map) { - await transformedWriter.write( - createResource({ - path: originalResourcePath + ".ui5lint.transformed.js.map", - string: JSON.stringify(JSON.parse(map), null, "\t"), - }) - ); - } - } - - private getModuleId(resourcePath: string) { - let resourcePathHeader = "/resources/"; - if (resourcePath.startsWith("/test-resources/")) { - resourcePathHeader = "/test-resources/"; - } - return resourcePath.substring(resourcePathHeader.length); - } - - private async analyzeFiles(all: Resource[], resources: Map, sourceMaps: Map, - resourceMessages: Map, vfsToFsPaths: Map, result: LintResult[]) { - return Promise.all(all.map(async (resource: Resource) => { - const originalResourcePath = resource.getPath(); - const fsPath = resource.getSourceMetadata().fsPath; - - let source: string, map: string | undefined, messages: LintMessage[] | undefined; - let resourcePath = originalResourcePath; - try { - if (resourcePath.endsWith(".xml")) { - resourcePath = resourcePath.replace(/\.xml$/, ".js"); - const resourceContent = resource.getStream(); - ({source, map, messages} = - await xmlToJs(path.basename(originalResourcePath), resourceContent)); - } else if (resourcePath.endsWith(".js")) { - const resourceContent = await resource.getString(); - const id = this.getModuleId(resourcePath); - ({source, map} = amdToEsm(id, resourceContent)); - } else if (resourcePath.endsWith(".json")) { - resourcePath = resourcePath.replace(/\.json$/, ".js"); - const resourceContent = await resource.getString(); - ({source, messages} = await lintManifest(resourcePath, resourceContent)); - } else if (resourcePath.endsWith(".html")) { - resourcePath = resourcePath.replace(/\.html$/, ".jsx"); - // TODO: Enable when implement script extraction and parse - // Details: TS treats HTML as JSX, but parsing results are not consistent. https://github.com/SAP/ui5-linter/pull/48#discussion_r1551412367 - // source = await resource.getString(); - source = ""; - ({messages} = await lintHtml(resourcePath, resource.getStream())); - } else { - throw new Error(`Unsupported file type for ${resourcePath}`); - } - } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - const errorLinterReporter = new Reporter(this.project.getRootPath(), fsPath); - errorLinterReporter.addMessage({ - severity: LintMessageSeverity.Error, - message, - ruleId: "ui5-linter-parsing-error", - fatal: true, - }); - result.push(errorLinterReporter.getReport()); - return; - } - vfsToFsPaths.set(resourcePath, fsPath); - - resources.set(resourcePath, source); - if (messages?.length) { - resourceMessages.set(resourcePath, messages); - } - if (map) { - sourceMaps.set(resourcePath, map); - } - if (process.env.UI5LINT_WRITE_TRANSFORMED_SOURCES) { - await this.writeTransformedSources(process.env.UI5LINT_WRITE_TRANSFORMED_SOURCES, - originalResourcePath, source, map); - } - })); - } - - async createReports( - filePaths: string[], - reportCoverage: boolean | undefined = false, - messageDetails: boolean | undefined = false - ) { - const result: LintResult[] = []; - const reader = this.project.getReader({ - style: "buildtime", - }); - - // Read all resources and test-resources and their content since tsc works completely synchronous - const globEnd = taskStart("Locating Resources"); - const fileTypes = "{*.js,*.view.xml,*.fragment.xml,manifest.json,*.html}"; - const allResources = await reader.byGlob("/resources/**/" + fileTypes); - const allTestResources = await reader.byGlob("/test-resources/**/" + fileTypes); - globEnd(); - const resources = new Map(); - const sourceMaps = new Map(); - const resourceMessages = new Map(); - const vfsToFsPaths = new Map(); - - const transpileTaskEnd = taskStart("Transpiling Resources", `${allResources.length} Resources`); - await this.analyzeFiles(allResources, resources, sourceMaps, resourceMessages, vfsToFsPaths, result); - transpileTaskEnd(); - - const transpileTestResourcesTaskEnd = taskStart("Transpiling Test-Resources", - `${allTestResources.length} Resources`); - await this.analyzeFiles(allTestResources, resources, sourceMaps, resourceMessages, vfsToFsPaths, result); - transpileTestResourcesTaskEnd(); - - /* Handle filePaths parameter: - - Project path will always be absolute. e.g. '/home/user/projects/com.ui5.troublesome.app/' - - filePaths (if provided) can either be absolute or relative - - Absolute example: - '/home/user/projects/com.ui5.troublesome.app/webapp/model/formatter.js/webapp/controller/BaseController.js' - '/home/user/projects/com.ui5.troublesome.app/webapp/model/formatter.js/webapp/model/formatter.js' - - Check: Absolute paths must be located within projectPath - - Relative example: - 'webapp/controller/BaseController.js' - 'webapp/model/formatter.js' - - Task: Always resolve relative paths to the projectPath - (and check the resulting path is within the projectPath) - */ - let resourcePaths: (string | undefined)[]; - if (filePaths?.length) { - const absoluteFilePaths = filePaths.map((filePath) => { - if (!path.isAbsolute(filePath)) { - // Resolve relative filePaths - filePath = path.join(this.project.getRootPath(), filePath); - } - // Ensure file path is located within project root - if (!filePath.startsWith(this.project.getRootPath())) { - throw new Error( - `File ${filePath} is not located within project root ${this.project.getRootPath()}`); - } - return filePath; - }); - - // Rewrite fs-paths to virtual paths - resourcePaths = [...allResources, ...allTestResources].map((res: Resource) => { - if (!absoluteFilePaths.includes(res.getSourceMetadata().fsPath)) { - return; - } - - let resPath = res.getPath(); - if (resPath.endsWith(".html")) { - resPath = resPath.replace(/\.[a-z]+$/, ".jsx"); - } else if (!resPath.endsWith(".js")) { - resPath = resPath.replace(/\.[a-z]+$/, ".js"); - } - return resPath; - }) - .filter(($: string | undefined) => $); - } else { - resourcePaths = Array.from(resources.keys()); - } - resourcePaths.sort(); - - const host = await createVirtualCompilerHost(this.compilerOptions, resources); - const program = ts.createProgram(resourcePaths as string[], this.compilerOptions, host); - const checker = program.getTypeChecker(); - - const typeCheckDone = taskStart("Linting all transpiled resources"); - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile && resourcePaths.includes(sourceFile.fileName)) { - const filePath = vfsToFsPaths.get(sourceFile.fileName); - if (!filePath) { - throw new Error(`Failed to get FS path for ${sourceFile.fileName}`); - } - const linterDone = taskStart("Lint resource", filePath, true); - const linter = new FileLinter( - this.project.getRootPath(), - filePath, sourceFile, sourceMaps.get(sourceFile.fileName), checker, reportCoverage, messageDetails - ); - const report = await linter.getReport(); - if (resourceMessages.has(sourceFile.fileName)) { - report.messages.push(...resourceMessages.get(sourceFile.fileName)!); - report.errorCount = report.messages - .filter((message) => message.severity === LintMessageSeverity.Error).length; - report.warningCount = report.messages - .filter((message) => message.severity === LintMessageSeverity.Warning).length; - report.fatalErrorCount = report.messages.filter((message) => message.fatal).length; - } - result.push(report); - linterDone(); - } - } - typeCheckDone(); - return result; - } -} - -export class TsFileDetector extends FileBasedDetector { - async createReports( - filePaths: string[], reportCoverage: boolean | undefined = false, messageDetails: boolean | undefined = false - ) { - const options: ts.CompilerOptions = { - ...DEFAULT_OPTIONS, - rootDir: this.rootDir, - }; - - const resources = new Map(); - const sourceMaps = new Map(); - const resourceMessages = new Map(); - const internalToFsFilePaths = new Map(); - const internalfilePaths = await Promise.all(filePaths.map(async (filePath: string) => { - let transformationResult; - filePath = path.join(this.rootDir, filePath); - let internalfilePath = filePath.replace(/\\/g, "/"); - if (filePath.endsWith(".js")) { - const fileContent = ts.sys.readFile(filePath); - if (!fileContent) { - throw new Error(`Failed to read file ${filePath}`); - } - transformationResult = amdToEsm(path.basename(filePath, ".js"), fileContent); - } else if (filePath.endsWith(".xml")) { - const fileStream = fs.createReadStream(filePath); - internalfilePath = internalfilePath.replace(/\.xml$/, ".js"); - transformationResult = await xmlToJs(path.basename(filePath), fileStream); - } else if (filePath.endsWith(".json")) { - const fileContent = ts.sys.readFile(filePath); - if (!fileContent) { - throw new Error(`Failed to read file ${filePath}`); - } - internalfilePath = internalfilePath.replace(/\.json$/, ".js"); - transformationResult = await lintManifest(filePath.replace(/\.json$/, ".js"), fileContent); - } else if (filePath.endsWith(".html")) { - // TODO: Enable when implement script extraction and parse - // Details: TS treats HTML as JSX, but parsing results are not consistent. https://github.com/SAP/ui5-linter/pull/48#discussion_r1551412367 - // const fileContent = ts.sys.readFile(filePath); - // if (!fileContent) { - // throw new Error(`Failed to read file ${filePath}`); - // } - internalfilePath = internalfilePath.replace(/\.html$/, ".jsx"); - transformationResult = await lintHtml(path.basename(filePath), fs.createReadStream(filePath)); - // transformationResult.source = fileContent; - transformationResult.source = ""; - } else { - throw new Error(`Unsupported file type for ${filePath}`); - } - const {source, map} = transformationResult; - resources.set(internalfilePath, source); - if (map) { - sourceMaps.set(internalfilePath, map); - } - if (transformationResult.messages?.length) { - resourceMessages.set(internalfilePath, transformationResult.messages); - } - - internalToFsFilePaths.set(internalfilePath, filePath); - return internalfilePath; - })); - - const host = await createVirtualCompilerHost(options, resources); - const program = ts.createProgram(internalfilePaths, options, host); - const checker = program.getTypeChecker(); - - const result: LintResult[] = []; - - for (const sourceFile of program.getSourceFiles()) { - if (!sourceFile.isDeclarationFile) { - const filePath = internalToFsFilePaths.get(sourceFile.fileName); - if (!filePath) { - throw new Error(`Failed to get FS path for ${sourceFile.fileName}`); - } - const linter = new FileLinter( - this.rootDir, filePath, sourceFile, - sourceMaps.get(sourceFile.fileName), checker, reportCoverage, messageDetails - ); - const report = await linter.getReport(); - if (resourceMessages.has(sourceFile.fileName)) { - report.messages.push(...resourceMessages.get(sourceFile.fileName)!); - report.errorCount = report.messages - .filter((message) => message.severity === LintMessageSeverity.Error).length; - report.warningCount = report.messages - .filter((message) => message.severity === LintMessageSeverity.Warning).length; - report.fatalErrorCount = report.messages.filter((message) => message.fatal).length; - } - result.push(report); - } - } - return result; - } -} diff --git a/src/formatter/coverage.ts b/src/formatter/coverage.ts index 3021d3eaa..0ac8200a3 100644 --- a/src/formatter/coverage.ts +++ b/src/formatter/coverage.ts @@ -4,7 +4,7 @@ import { LintMessageSeverity, CoverageInfo, CoverageCategory, -} from "../detectors/AbstractDetector.js"; +} from "../linter/LinterContext.js"; import {readFile} from "fs/promises"; const visualizedSpace = "\u00b7"; diff --git a/src/formatter/json.ts b/src/formatter/json.ts index 6dc5e7550..b4b5a9f07 100644 --- a/src/formatter/json.ts +++ b/src/formatter/json.ts @@ -1,4 +1,4 @@ -import {LintMessage, LintResult} from "../detectors/AbstractDetector.js"; +import {LintMessage, LintResult} from "../linter/LinterContext.js"; export class Json { format(lintResults: LintResult[], showDetails: boolean) { diff --git a/src/formatter/text.ts b/src/formatter/text.ts index 511065673..c2459f071 100644 --- a/src/formatter/text.ts +++ b/src/formatter/text.ts @@ -1,6 +1,6 @@ import chalk from "chalk"; import path from "node:path"; -import {LintMessageSeverity, LintResult, LintMessage} from "../detectors/AbstractDetector.js"; +import {LintMessageSeverity, LintResult, LintMessage} from "../linter/LinterContext.js"; function formatSeverity(severity: LintMessageSeverity) { if (severity === LintMessageSeverity.Error) { diff --git a/src/linter/LinterContext.ts b/src/linter/LinterContext.ts new file mode 100644 index 000000000..4a0bcf6b3 --- /dev/null +++ b/src/linter/LinterContext.ts @@ -0,0 +1,217 @@ +import {AbstractAdapter, AbstractReader} from "@ui5/fs"; +import {createReader} from "@ui5/fs/resourceFactory"; + +export type FilePath = string; // Platform-dependent path +export type ResourcePath = string; // Always POSIX + +// Data types are structured very similar to the ESLint types for better compatibility into existing integrations: +// https://eslint.org/docs/latest/integrate/nodejs-api#-lintresult-type +export interface LintResult { + filePath: FilePath; + messages: LintMessage[]; + coverageInfo: CoverageInfo[]; + errorCount: number; // includes fatal errors + fatalErrorCount: number; + warningCount: number; +} + +export enum LintMessageSeverity { + Warning = 1, + Error = 2, +} + +export interface LintMessage { + ruleId: string; + severity: LintMessageSeverity; + message: string; + messageDetails?: string; + fatal?: boolean | undefined; // e.g. parsing error + line?: number | undefined; // 1 based to be aligned with most IDEs + column?: number | undefined; // 1 based to be aligned with most IDEs + endLine?: number | undefined; + endColumn?: number | undefined; +} + +export enum CoverageCategory { + CallExpressionUnknownType = 1, +} + +export interface CoverageInfo { + category: CoverageCategory; + message: string; + messageDetails?: string; + line?: number | undefined; // 1 based to be aligned with most IDEs + column?: number | undefined; // 1 based to be aligned with most IDEs + endLine?: number | undefined; + endColumn?: number | undefined; +} + +export interface TranspileResult { + source: string; + map: string; +} + +export interface LinterOptions { + rootDir: string; + namespace?: string; + pathsToLint?: FilePath[]; + reportCoverage?: boolean; + includeMessageDetails?: boolean; +} + +export interface LinterParameters { + workspace: AbstractAdapter; + context: LinterContext; +} + +export interface PositionInfo { + line: number; + column: number; +} + +export interface PositionRange { + start: PositionInfo; + end?: PositionInfo; +} + +export interface LintMetadata { + // TODO: Use this to store information shared across linters, + // such as the async flag state in manifest.json which might be relevant + // when parsing the Component.js + _todo: string; +} + +export default class LinterContext { + #rootDir: string; + #namespace: string | undefined; + #messages = new Map(); + #coverageInfo = new Map(); + #metadata = new Map(); + #rootReader: AbstractReader | undefined; + + #resourcePathsToLint: ResourcePath[] | undefined; + // Mapping original resource paths to aliases, such as the paths of transpiled resources + #resourcePathAliases = new Map(); + + #reportCoverage: boolean; + #includeMessageDetails: boolean; + + constructor(options: LinterOptions) { + this.#rootDir = options.rootDir; + this.#namespace = options.namespace; + this.#resourcePathsToLint = options.pathsToLint ? [...options.pathsToLint] : undefined; + this.#reportCoverage = !!options.reportCoverage; + this.#includeMessageDetails = !!options.includeMessageDetails; + } + + getRootDir(): string { + return this.#rootDir; + } + + getRootReader(): AbstractReader { + if (this.#rootReader) { + return this.#rootReader; + } + this.#rootReader = createReader({ + fsBasePath: this.#rootDir, + virBasePath: "/", + }); + return this.#rootReader; + } + + getNamespace(): string | undefined { + return this.#namespace; + } + + getPathsToLint(): ResourcePath[] | undefined { + return this.#resourcePathsToLint; + } + + getReportCoverage(): boolean { + return this.#reportCoverage; + } + + getIncludeMessageDetails(): boolean { + return this.#includeMessageDetails; + } + + getMetadata(resourcePath: ResourcePath): LintMetadata { + let metadata = this.#metadata.get(resourcePath); + if (!metadata) { + metadata = {} as LintMetadata; + this.#metadata.set(resourcePath, metadata); + } + return metadata; + } + + addPathToLint(resourcePath: ResourcePath) { + this.#resourcePathsToLint?.push(resourcePath); + } + + getLintingMessages(resourcePath: ResourcePath): LintMessage[] { + let messages = this.#messages.get(resourcePath); + if (!messages) { + messages = []; + this.#messages.set(resourcePath, messages); + } + return messages; + } + + addLintingMessage(resourcePath: ResourcePath, message: LintMessage) { + this.getLintingMessages(resourcePath).push(message); + } + + getCoverageInfo(resourcePath: ResourcePath): CoverageInfo[] { + let coverageInfo = this.#coverageInfo.get(resourcePath); + if (!coverageInfo) { + coverageInfo = []; + this.#coverageInfo.set(resourcePath, coverageInfo); + } + return coverageInfo; + } + + addCoverageInfo(resourcePath: ResourcePath, coverageInfo: CoverageInfo) { + this.getCoverageInfo(resourcePath).push(coverageInfo); + } + + generateLintResult(resourcePath: ResourcePath): LintResult { + const messages = this.#messages.get(resourcePath) ?? []; + const coverageInfo = this.#coverageInfo.get(resourcePath) ?? []; + let errorCount = 0; + let warningCount = 0; + let fatalErrorCount = 0; + for (const message of messages) { + if (message.severity === LintMessageSeverity.Error) { + errorCount++; + if (message.fatal) { + fatalErrorCount++; + } + } else { + warningCount++; + } + } + + return { + filePath: resourcePath, + messages, + coverageInfo, + errorCount, + warningCount, + fatalErrorCount, + }; + } + + generateLintResults(): LintResult[] { + const lintResults: LintResult[] = []; + let resourcePaths; + if (this.#reportCoverage) { + resourcePaths = new Set([...this.#messages.keys(), ...this.#coverageInfo.keys()]).values(); + } else { + resourcePaths = this.#messages.keys(); + } + for (const resourcePath of resourcePaths) { + lintResults.push(this.generateLintResult(resourcePath)); + } + return lintResults; + } +} diff --git a/src/linter/html/HtmlReporter.ts b/src/linter/html/HtmlReporter.ts index a9705999b..ab43e224b 100644 --- a/src/linter/html/HtmlReporter.ts +++ b/src/linter/html/HtmlReporter.ts @@ -1,16 +1,22 @@ -import type {BaseReporter, ReporterMessage, ReporterCoverageInfo} from "../../detectors/BaseReporter.js"; -import type {LintMessage} from "../../detectors/AbstractDetector.js"; import {Tag as SaxTag} from "sax-wasm"; -import {LintMessageSeverity, CoverageInfo} from "../../detectors/AbstractDetector.js"; +import LinterContext, {CoverageInfo, LintMessage, LintMessageSeverity, ResourcePath} from "../LinterContext.js"; import {resolveLinks} from "../../formatter/lib/resolveLinks.js"; -export default class HtmlReporter implements BaseReporter { - #filePath: string; - #messages: LintMessage[] = []; - #coverageInfo: CoverageInfo[] = []; +interface ReporterMessage extends LintMessage { + node: SaxTag; +} + +interface ReporterCoverageInfo extends CoverageInfo { + node: SaxTag; +} - constructor(filePath: string) { - this.#filePath = filePath; +export default class HtmlReporter { + #resourcePath: string; + #context: LinterContext; + + constructor(resourcePath: ResourcePath, context: LinterContext) { + this.#resourcePath = resourcePath; + this.#context = context; } addMessage({node, message, messageDetails, severity, ruleId, fatal = undefined}: ReporterMessage) { @@ -23,20 +29,15 @@ export default class HtmlReporter implements BaseReporter { ({line, character: column} = node.openStart); } - const msg: LintMessage = { + this.#context.addLintingMessage(this.#resourcePath, { ruleId, severity, fatal, line: line + 1, column: column + 1, message, - }; - - if (messageDetails) { - msg.messageDetails = resolveLinks(messageDetails); - } - - this.#messages.push(msg); + messageDetails: messageDetails ? resolveLinks(messageDetails) : undefined, + }); } addCoverageInfo({node, message, category}: ReporterCoverageInfo) { @@ -46,7 +47,7 @@ export default class HtmlReporter implements BaseReporter { ({line: endLine, character: endColumn} = node.closeEnd); } - this.#coverageInfo.push({ + this.#context.addCoverageInfo(this.#resourcePath, { category, // One-based to be aligned with most IDEs line: line + 1, @@ -56,29 +57,4 @@ export default class HtmlReporter implements BaseReporter { message, }); } - - getReport() { - let errorCount = 0; - let warningCount = 0; - let fatalErrorCount = 0; - for (const {severity, fatal} of this.#messages) { - if (severity === LintMessageSeverity.Error) { - errorCount++; - if (fatal) { - fatalErrorCount++; - } - } else { - warningCount++; - } - } - - return { - filePath: this.#filePath, - messages: this.#messages, - coverageInfo: this.#coverageInfo, - errorCount, - warningCount, - fatalErrorCount, - }; - } } diff --git a/src/linter/html/linter.ts b/src/linter/html/linter.ts index c402c301c..109faee00 100644 --- a/src/linter/html/linter.ts +++ b/src/linter/html/linter.ts @@ -1,35 +1,27 @@ -import {taskStart} from "../../detectors/util/perf.js"; -import {extractJSScriptTags} from "../../detectors/transpilers/html/parser.js"; -import {LintMessageSeverity} from "../../detectors/AbstractDetector.js"; -import HtmlReporter from "./HtmlReporter.js"; +import {LinterParameters} from "../LinterContext.js"; +import transpileHtml from "./transpiler.js"; +import {Resource} from "@ui5/fs"; -import type {TranspileResult} from "../../detectors/transpilers/AbstractTranspiler.js"; -import type {ReadStream} from "node:fs"; +export default async function lintHtml({workspace, context}: LinterParameters) { + let htmlResources: Resource[]; + const pathsToLint = context.getPathsToLint(); + if (pathsToLint?.length) { + htmlResources = []; + await Promise.all(pathsToLint.map(async (resourcePath) => { + if (!resourcePath.endsWith(".html")) { + return; + } + const resource = await workspace.byPath(resourcePath); + if (!resource) { + throw new Error(`Resource not found: ${resourcePath}`); + } + htmlResources.push(resource); + })); + } else { + htmlResources = await workspace.byGlob("**/{*.html}"); + } -export async function lintHtml(resourceName: string, contentStream: ReadStream): Promise { - const taskLintEnd = taskStart("Linting HTML", resourceName); - const report = new HtmlReporter(resourceName); - const jsScriptTags = await extractJSScriptTags(contentStream); - - jsScriptTags.forEach((tag) => { - // Tags with src attribute do not parse and run inline code - const hasSrc = tag.attributes.some((attr) => { - return attr.name.value.toLowerCase() === "src"; - }); - - if (!hasSrc && tag.textNodes?.length > 0) { - report.addMessage({ - node: tag, - severity: LintMessageSeverity.Warning, - ruleId: "ui5-linter-csp-unsafe-inline-script", - message: `Use of unsafe inline script`, - messageDetails: "{@link topic:fe1a6dba940e479fb7c3bc753f92b28c Content Security Policy}", - }); - } - }); - - taskLintEnd(); - - const {messages} = report.getReport(); - return {messages, source: "", map: ""}; + await Promise.all(htmlResources.map(async (resource: Resource) => { + return transpileHtml(resource.getPath(), resource.getStream(), context); + })); } diff --git a/src/detectors/transpilers/html/parser.ts b/src/linter/html/parser.ts similarity index 100% rename from src/detectors/transpilers/html/parser.ts rename to src/linter/html/parser.ts diff --git a/src/linter/html/transpiler.ts b/src/linter/html/transpiler.ts new file mode 100644 index 000000000..e2c54b45b --- /dev/null +++ b/src/linter/html/transpiler.ts @@ -0,0 +1,43 @@ +import {ReadStream} from "node:fs"; +import {extractJSScriptTags} from "./parser.js"; +import HtmlReporter from "./HtmlReporter.js"; +import LinterContext, {LintMessageSeverity, ResourcePath, TranspileResult} from "../LinterContext.js"; +import {taskStart} from "../../util/perf.js"; + +export default async function transpileHtml( + resourcePath: ResourcePath, contentStream: ReadStream, context: LinterContext +): Promise { + try { + const taskEnd = taskStart("Transpile XML", resourcePath, true); + const report = new HtmlReporter(resourcePath, context); + const jsScriptTags = await extractJSScriptTags(contentStream); + + jsScriptTags.forEach((tag) => { + // Tags with src attribute do not parse and run inline code + const hasSrc = tag.attributes.some((attr) => { + return attr.name.value.toLowerCase() === "src"; + }); + + if (!hasSrc && tag.textNodes?.length > 0) { + report.addMessage({ + node: tag, + severity: LintMessageSeverity.Warning, + ruleId: "ui5-linter-csp-unsafe-inline-script", + message: `Use of unsafe inline script`, + messageDetails: "{@link topic:fe1a6dba940e479fb7c3bc753f92b28c Content Security Policy}", + }); + } + }); + + taskEnd(); + return {source: "", map: ""}; + } catch (err) { + if (err instanceof Error) { + throw new Error(`Failed to transpile resource ${resourcePath}: ${err.message}`, { + cause: err, + }); + } else { + throw err; + } + } +} diff --git a/src/linter/json/ManifestReporter.ts b/src/linter/json/ManifestReporter.ts deleted file mode 100644 index 0ffec046b..000000000 --- a/src/linter/json/ManifestReporter.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type {BaseReporter, ReporterMessage, ReporterCoverageInfo, PositionInfo} from "../../detectors/BaseReporter.js"; -import type {LintMessage} from "../../detectors/AbstractDetector.js"; -import type {jsonSourceMapType, jsonMapPointers} from "./ManifestLinter.js"; -import {LintMessageSeverity, CoverageInfo} from "../../detectors/AbstractDetector.js"; - -export default class ManifestReporter implements BaseReporter { - #filePath: string; - #pointers: jsonMapPointers; - #messages: LintMessage[] = []; - #coverageInfo: CoverageInfo[] = []; - - constructor(filePath: string, manifest: jsonSourceMapType) { - this.#filePath = filePath; - this.#pointers = manifest.pointers; - } - - addMessage({node, message, severity, ruleId, fatal = undefined}: ReporterMessage) { - if (fatal && severity !== LintMessageSeverity.Error) { - throw new Error(`Reports flagged as "fatal" must be of severity "Error"`); - } - - const {line, column} = this.#getPosition((node as string)); - - this.#messages.push({ - ruleId, - severity, - fatal, - line, - column, - message, - }); - } - - addCoverageInfo({node, message, category}: ReporterCoverageInfo) { - const location = this.#getPositionsForNode((node as string)); - this.#coverageInfo.push({ - category, - // One-based to be aligned with most IDEs - line: location.key.line, - column: location.key.column, - endLine: location.valueEnd.line, - endColumn: location.valueEnd.column, - message, - }); - } - - #getPositionsForNode(path: string) { - const location = this.#pointers[path]; - - return location || {key: 1, keyEnd: 1, value: 1, valueEnd: 1}; - } - - #getPosition(path: string): PositionInfo { - let line = 1; - let column = 1; - - const location = this.#pointers[path]; - - if (location) { - line = location.key.line + 1; - column = location.key.column + 1; - } - - return { - line, - column, - }; - } - - getReport() { - let errorCount = 0; - let warningCount = 0; - let fatalErrorCount = 0; - for (const {severity, fatal} of this.#messages) { - if (severity === LintMessageSeverity.Error) { - errorCount++; - if (fatal) { - fatalErrorCount++; - } - } else { - warningCount++; - } - } - - return { - filePath: this.#filePath, - messages: this.#messages, - coverageInfo: this.#coverageInfo, - errorCount, - warningCount, - fatalErrorCount, - }; - } -} diff --git a/src/linter/json/linter.ts b/src/linter/json/linter.ts deleted file mode 100644 index 399cc3926..000000000 --- a/src/linter/json/linter.ts +++ /dev/null @@ -1,13 +0,0 @@ -import ManifestLinter from "./ManifestLinter.js"; -import {taskStart} from "../../detectors/util/perf.js"; - -import type {TranspileResult} from "../../detectors/transpilers/AbstractTranspiler.js"; - -export async function lintManifest(resourceName: string, content: string): Promise { - const taskLintEnd = taskStart("Static lint", resourceName); - const linter = new ManifestLinter(content, resourceName); - const {messages} = await linter.getReport(); - taskLintEnd(); - - return {messages, source: content, map: ""}; -} diff --git a/src/linter/lintWorkspace.ts b/src/linter/lintWorkspace.ts new file mode 100644 index 000000000..61decabeb --- /dev/null +++ b/src/linter/lintWorkspace.ts @@ -0,0 +1,29 @@ +import {AbstractAdapter} from "@ui5/fs"; +import lintXml from "./xmlTemplate/linter.js"; +import lintJson from "./manifestJson/linter.js"; +import lintHtml from "./html/linter.js"; +import {taskStart} from "../util/perf.js"; +import TypeLinter from "./ui5Types/TypeLinter.js"; +import LinterContext, {LintResult, LinterParameters, LinterOptions} from "./LinterContext.js"; + +export default async function lintWorkspace( + workspace: AbstractAdapter, options: LinterOptions +): Promise { + const done = taskStart("Linting Workspace"); + + const context = new LinterContext(options); + const params: LinterParameters = { + workspace, context, + }; + + await Promise.all([ + lintXml(params), + lintJson(params), + lintHtml(params), + ]); + + const typeLinter = new TypeLinter(params); + await typeLinter.lint(); + done(); + return context.generateLintResults(); +} diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 86e4ef2fc..090d0c29d 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -1,41 +1,115 @@ import {graphFromObject} from "@ui5/project/graph"; -import {LintResult} from "../detectors/AbstractDetector.js"; -import {TsFileDetector, TsProjectDetector} from "../detectors/typeChecker/index.js"; -import {taskStart} from "../detectors/util/perf.js"; +import {createReader, createWorkspace, createReaderCollection} from "@ui5/fs/resourceFactory"; +import {FilePath, LinterOptions, LintResult} from "./LinterContext.js"; +import lintWorkspace from "./lintWorkspace.js"; +import {taskStart} from "../util/perf.js"; import path from "node:path"; +import posixPath from "node:path/posix"; import {stat} from "node:fs/promises"; import {ProjectGraph} from "@ui5/project"; +import {AbstractReader} from "@ui5/fs"; -export interface LinterOptions { - rootDir: string; - filePaths: string[]; - reportCoverage?: boolean; - messageDetails?: boolean; +async function lint( + resourceReader: AbstractReader, options: LinterOptions +): Promise { + const lintEnd = taskStart("Linting"); + + const workspace = createWorkspace({ + reader: resourceReader, + }); + + const res = await lintWorkspace(workspace, options); + lintEnd(); + return res; } -async function fsStat(fsPath: string) { - try { - return await stat(fsPath); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - // "File or directory does not exist" - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (err.code === "ENOENT") { - return false; - } else { - throw err; +export async function lintProject({ + rootDir, pathsToLint, reportCoverage, includeMessageDetails, +}: LinterOptions): Promise { + const projectGraphDone = taskStart("Project Graph creation"); + const graph = await getProjectGraph(rootDir); + const project = graph.getRoot(); + projectGraphDone(); + + let virBasePath = "/resources/"; + if (!project._isSourceNamespaced) { + // Ensure the virtual filesystem includes the project namespace to allow relative imports + // of framework resources from the project + virBasePath += project.getNamespace() + "/"; + } + const fsBasePath = project.getSourcePath(); + let reader = createReader({ + fsBasePath, + virBasePath, + }); + let virBasePathTest: string | undefined; + let fsBasePathTest: string | undefined; + if (project._testPathExists) { + fsBasePathTest = path.join(project.getRootPath(), project._testPath); + virBasePathTest = "/test-resources/"; + if (!project._isSourceNamespaced) { + // Dynamically add namespace if the physical project structure does not include it + // This logic is identical to the specification implementation in ui5-project + virBasePathTest += project.getNamespace() + "/"; } + reader = createReaderCollection({ + readers: [reader, createReader({ + fsBasePath: fsBasePathTest, + virBasePath: virBasePathTest, + })], + }); + } + let resolvedFilePaths; + if (pathsToLint?.length) { + const absoluteFilePaths = resolveFilePaths(rootDir, pathsToLint); + resolvedFilePaths = transformFilePathsToVirtualPaths( + absoluteFilePaths, fsBasePath, virBasePath, fsBasePathTest, virBasePathTest); } -} -async function dirExists(dirPath: string) { - const stats = await fsStat(dirPath); - return stats && stats.isDirectory(); + const res = await lint(reader, { + rootDir, + namespace: project.getNamespace(), + pathsToLint: resolvedFilePaths, + reportCoverage, + includeMessageDetails, + }); + + const relFsBasePath = path.relative(rootDir, fsBasePath); + const relFsBasePathTest = fsBasePathTest ? path.relative(rootDir, fsBasePathTest) : undefined; + res.forEach((result) => { + result.filePath = transformVirtualPathToFilePath(result.filePath, + relFsBasePath, virBasePath, + relFsBasePathTest, virBasePathTest); + }); + return res; } -async function fileExists(dirPath: string) { - const stats = await fsStat(dirPath); - return stats && stats.isFile(); +export async function lintFile({ + rootDir, pathsToLint, namespace, reportCoverage, includeMessageDetails, +}: LinterOptions): Promise { + const reader = createReader({ + fsBasePath: rootDir, + virBasePath: "/", + }); + let resolvedFilePaths; + if (pathsToLint?.length) { + const absoluteFilePaths = resolveFilePaths(rootDir, pathsToLint); + resolvedFilePaths = transformFilePathsToVirtualPaths( + absoluteFilePaths, rootDir, "/", rootDir); + } + + const res = await lint(reader, { + rootDir, + namespace, + pathsToLint: resolvedFilePaths, + reportCoverage, + includeMessageDetails, + }); + + res.forEach((result) => { + result.filePath = transformVirtualPathToFilePath(result.filePath, "", "/"); + }); + return res; } async function getProjectGraph(rootDir: string): Promise { @@ -87,23 +161,103 @@ async function getProjectGraph(rootDir: string): Promise { }); } -export async function lintProject({ - rootDir, filePaths, reportCoverage, messageDetails, -}: LinterOptions): Promise { - const lintEnd = taskStart("Linting Project"); - const projectGraphDone = taskStart("Project Graph creation"); - const graph = await getProjectGraph(rootDir); - const project = graph.getRoot(); - projectGraphDone(); - const tsDetector = new TsProjectDetector(project); - const res = await tsDetector.createReports(filePaths, reportCoverage, messageDetails); - lintEnd(); - return res; +/** + * Resolve provided filePaths to absolute paths and ensure they are located within the project root. + * Returned paths are absolute. +*/ +function resolveFilePaths(rootDir: string, filePaths: string[]): string[] { + /* rootDir is always absolute, e.g. '/home/user/projects/com.ui5.troublesome.app/' + + filePaths can be absolute, or relative to rootDir: + Absolute example: + '/home/user/projects/com.ui5.troublesome.app/webapp/model/formatter.js/webapp/controller/BaseController.js' + '/home/user/projects/com.ui5.troublesome.app/webapp/model/formatter.js/webapp/model/formatter.js' + + Relative example: + 'webapp/controller/BaseController.js' + 'webapp/model/formatter.js' + */ + return filePaths.map((filePath) => { + if (!path.isAbsolute(filePath)) { + // Resolve relative filePaths + filePath = path.join(rootDir, filePath); + } + // Ensure file path is located within project root + if (!filePath.startsWith(rootDir)) { + throw new Error( + `File path ${filePath} is not located within project root ${rootDir}`); + } + return filePath; + }); } -export async function lintFile({ - rootDir, filePaths, reportCoverage, messageDetails, -}: LinterOptions): Promise { - const tsDetector = new TsFileDetector(rootDir); - return await tsDetector.createReports(filePaths, reportCoverage, messageDetails); +function ensurePosix(inputPath: string) { + if (!inputPath.includes("\\")) { + return inputPath; + } + return inputPath.replace(/\\/g, "/"); +} + +/** + * Normalize provided filePaths to virtual paths. + * Returned paths are absolute, POSIX-style paths + */ +function transformFilePathsToVirtualPaths( + filePaths: FilePath[], + srcFsBasePath: string, srcVirBasePath: string, + testFsBasePath?: string, testVirBasePath?: string +): FilePath[] { + return filePaths.map((filePath) => { + if (filePath.startsWith(srcFsBasePath)) { + return posixPath.join(srcVirBasePath, ensurePosix(path.relative(srcFsBasePath, filePath))); + } else if (testFsBasePath && testVirBasePath && filePath.startsWith(testFsBasePath)) { + return posixPath.join(testVirBasePath, ensurePosix(path.relative(testFsBasePath, filePath))); + } else { + throw new Error( + `File path ${filePath} is not located within the detected source or test directories of the project`); + } + }); +} + +/** + * Normalize provided virtual paths to the original file paths + */ +function transformVirtualPathToFilePath( + virtualPath: string, + srcFsBasePath: string, srcVirBasePath: string, + testFsBasePath?: string, testVirBasePath?: string +): FilePath { + if (virtualPath.startsWith(srcVirBasePath)) { + return path.join(srcFsBasePath, posixPath.relative(srcVirBasePath, virtualPath)); + } else if (testFsBasePath && testVirBasePath && virtualPath.startsWith(testVirBasePath)) { + return path.join(testFsBasePath, posixPath.relative(testVirBasePath, virtualPath)); + } else { + throw new Error( + `Resource path ${virtualPath} is not located within the virtual source or test directories of the project`); + } +} + +async function fsStat(fsPath: string) { + try { + return await stat(fsPath); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + // "File or directory does not exist" + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (err.code === "ENOENT") { + return false; + } else { + throw err; + } + } +} + +async function dirExists(dirPath: string) { + const stats = await fsStat(dirPath); + return stats && stats.isDirectory(); +} + +async function fileExists(dirPath: string) { + const stats = await fsStat(dirPath); + return stats && stats.isFile(); } diff --git a/src/linter/json/ManifestLinter.ts b/src/linter/manifestJson/ManifestLinter.ts similarity index 91% rename from src/linter/json/ManifestLinter.ts rename to src/linter/manifestJson/ManifestLinter.ts index a0cab52bb..415923c58 100644 --- a/src/linter/json/ManifestLinter.ts +++ b/src/linter/manifestJson/ManifestLinter.ts @@ -5,11 +5,11 @@ import type { Model as ManifestModel, DataSource as ManifestDataSource, } from "../../manifest.d.ts"; -import type {LintResult} from "../../detectors/AbstractDetector.js"; import ManifestReporter from "./ManifestReporter.js"; -import {LintMessageSeverity} from "../../detectors/AbstractDetector.js"; +import {LintMessageSeverity, ResourcePath} from "../LinterContext.js"; import jsonMap from "json-source-map"; +import LinterContext from "../LinterContext.js"; interface locType { line: number; @@ -50,24 +50,22 @@ export interface jsonSourceMapType { } export default class ManifestLinter { - #reporter: ManifestReporter | null; + #reporter: ManifestReporter | undefined; #content = ""; - #path = ""; + #resourcePath = ""; + #context: LinterContext; - constructor(content: string, path: string) { - this.#reporter = null; + constructor(resourcePath: ResourcePath, content: string, context: LinterContext) { + this.#resourcePath = resourcePath; this.#content = content; - this.#path = path; + this.#context = context; } // eslint-disable-next-line @typescript-eslint/require-await - async getReport(): Promise { + async lint() { const source = this.#parseManifest(this.#content); - this.#reporter = new ManifestReporter(this.#path, source); + this.#reporter = new ManifestReporter(this.#resourcePath, this.#context, source); this.#analyzeManifest(source.data); - - const report = this.#reporter.getReport(); - return report; } #parseManifest(manifest: string): jsonSourceMapType { diff --git a/src/linter/manifestJson/ManifestReporter.ts b/src/linter/manifestJson/ManifestReporter.ts new file mode 100644 index 000000000..59d30ab78 --- /dev/null +++ b/src/linter/manifestJson/ManifestReporter.ts @@ -0,0 +1,77 @@ +import type {jsonSourceMapType, jsonMapPointers} from "./ManifestLinter.js"; +import LinterContext, { + LintMessage, LintMessageSeverity, CoverageInfo, PositionInfo, ResourcePath, +} from "../LinterContext.js"; + +interface ReporterMessage extends LintMessage { + node: string; +} + +interface ReporterCoverageInfo extends CoverageInfo { + node: string; +} + +export default class ManifestReporter { + #resourcePath: ResourcePath; + #pointers: jsonMapPointers; + #context: LinterContext; + + constructor(resourcePath: ResourcePath, context: LinterContext, manifest: jsonSourceMapType) { + this.#resourcePath = resourcePath; + this.#pointers = manifest.pointers; + this.#context = context; + } + + addMessage({node, message, severity, ruleId, fatal = undefined}: ReporterMessage) { + if (fatal && severity !== LintMessageSeverity.Error) { + throw new Error(`Reports flagged as "fatal" must be of severity "Error"`); + } + + const {line, column} = this.#getPosition(node); + + this.#context.addLintingMessage(this.#resourcePath, { + ruleId, + severity, + fatal, + line, + column, + message, + }); + } + + addCoverageInfo({node, message, category}: ReporterCoverageInfo) { + const location = this.#getPositionsForNode(node); + this.#context.addCoverageInfo(this.#resourcePath, { + category, + // One-based to be aligned with most IDEs + line: location.key.line, + column: location.key.column, + endLine: location.valueEnd.line, + endColumn: location.valueEnd.column, + message, + }); + } + + #getPositionsForNode(path: string) { + const location = this.#pointers[path]; + + return location || {key: 1, keyEnd: 1, value: 1, valueEnd: 1}; + } + + #getPosition(path: string): PositionInfo { + let line = 1; + let column = 1; + + const location = this.#pointers[path]; + + if (location) { + line = location.key.line + 1; + column = location.key.column + 1; + } + + return { + line, + column, + }; + } +} diff --git a/src/linter/manifestJson/linter.ts b/src/linter/manifestJson/linter.ts new file mode 100644 index 000000000..fa76c77fa --- /dev/null +++ b/src/linter/manifestJson/linter.ts @@ -0,0 +1,32 @@ +import {Resource} from "@ui5/fs"; +import ManifestLinter from "./ManifestLinter.js"; +import {taskStart} from "../../util/perf.js"; +import {LinterParameters} from "../LinterContext.js"; + +export default async function lintJson({workspace, context}: LinterParameters) { + const lintingDone = taskStart("Linting manifest.json files"); + + let jsonResources: Resource[]; + + const pathsToLint = context.getPathsToLint(); + if (pathsToLint?.length) { + jsonResources = []; + await Promise.all(pathsToLint.map(async (resourcePath) => { + if (!resourcePath.endsWith("manifest.json") && !resourcePath.endsWith("manifest.appdescr_variant")) { + return; + } + const resource = await workspace.byPath(resourcePath); + if (!resource) { + throw new Error(`Resource not found: ${resourcePath}`); + } + jsonResources.push(resource); + })); + } else { + jsonResources = await workspace.byGlob("**/{manifest.json,manifest.appdescr_variant}"); + } + await Promise.all(jsonResources.map(async (resource: Resource) => { + const linter = new ManifestLinter(resource.getPath(), await resource.getString(), context); + await linter.lint(); + })); + lintingDone(); +} diff --git a/src/detectors/typeChecker/FileLinter.ts b/src/linter/ui5Types/SourceFileLinter.ts similarity index 96% rename from src/detectors/typeChecker/FileLinter.ts rename to src/linter/ui5Types/SourceFileLinter.ts index 3be04f347..d68e09b68 100644 --- a/src/detectors/typeChecker/FileLinter.ts +++ b/src/linter/ui5Types/SourceFileLinter.ts @@ -1,43 +1,43 @@ import ts, {Identifier} from "typescript"; -import Reporter from "../Reporter.js"; -import {CoverageCategory, LintMessageSeverity, LintResult} from "../AbstractDetector.js"; +import SourceFileReporter from "./SourceFileReporter.js"; +import LinterContext, {ResourcePath, CoverageCategory, LintMessageSeverity} from "../LinterContext.js"; interface DeprecationInfo { symbol: ts.Symbol; messageDetails?: string; } -export default class FileLinter { - #filePath: string; +export default class SourceFileLinter { + #resourcePath: ResourcePath; #sourceFile: ts.SourceFile; #checker: ts.TypeChecker; - #reporter: Reporter; + #reporter: SourceFileReporter; #boundVisitNode: (node: ts.Node) => void; #reportCoverage: boolean; #messageDetails: boolean; constructor( - rootDir: string, filePath: string, sourceFile: ts.SourceFile, sourceMap: string | undefined, + context: LinterContext, resourcePath: ResourcePath, sourceFile: ts.SourceFile, sourceMap: string | undefined, checker: ts.TypeChecker, reportCoverage: boolean | undefined = false, messageDetails: boolean | undefined = false ) { - this.#filePath = filePath; + this.#resourcePath = resourcePath; this.#sourceFile = sourceFile; this.#checker = checker; - this.#reporter = new Reporter(rootDir, filePath, sourceFile, sourceMap); + this.#reporter = new SourceFileReporter(context, resourcePath, sourceFile, sourceMap); this.#boundVisitNode = this.visitNode.bind(this); this.#reportCoverage = reportCoverage; this.#messageDetails = messageDetails; } // eslint-disable-next-line @typescript-eslint/require-await - async getReport(): Promise { + async lint() { try { this.visitNode(this.#sourceFile); - return this.#reporter.getReport(); + this.#reporter.deduplicateMessages(); } catch (err) { if (err instanceof Error) { - throw new Error(`Failed to produce report for ${this.#filePath}: ${err.message}`, { + throw new Error(`Failed to produce report for ${this.#resourcePath}: ${err.message}`, { cause: err, }); } diff --git a/src/detectors/Reporter.ts b/src/linter/ui5Types/SourceFileReporter.ts similarity index 74% rename from src/detectors/Reporter.ts rename to src/linter/ui5Types/SourceFileReporter.ts index 0474006b7..f509906cd 100644 --- a/src/detectors/Reporter.ts +++ b/src/linter/ui5Types/SourceFileReporter.ts @@ -1,4 +1,4 @@ -import path from "path"; +import path from "node:path/posix"; import ts from "typescript"; import { TraceMap, @@ -6,34 +6,44 @@ import { LEAST_UPPER_BOUND, GREATEST_LOWER_BOUND, } from "@jridgewell/trace-mapping"; +import {resolveLinks} from "../../formatter/lib/resolveLinks.js"; -import {LintMessageSeverity} from "./AbstractDetector.js"; -import {resolveLinks} from "../formatter/lib/resolveLinks.js"; - -import type {LintResult, LintMessage, CoverageInfo} from "./AbstractDetector.js"; -import type { - BaseReporter, - ReporterMessage, - ReporterCoverageInfo, - PositionInfo, - PositionRange, -} from "./BaseReporter.js"; - -export default class Reporter implements BaseReporter { - #rootDir: string; - #filePath: string; +import LinterContext, { + LintMessage, CoverageInfo, LintMessageSeverity, + PositionInfo, PositionRange, ResourcePath, +} from "../LinterContext.js"; + +interface ReporterMessage extends LintMessage { + node: ts.Node; +} + +interface ReporterCoverageInfo extends CoverageInfo { + node: ts.Node; +} + +export default class SourceFileReporter { + #context: LinterContext; + #resourcePath: ResourcePath; + #originalResourcePath: ResourcePath; #sourceFile: ts.SourceFile | undefined; #traceMap: TraceMap | undefined; #messages: LintMessage[] = []; #coverageInfo: CoverageInfo[] = []; - constructor(rootDir: string, filePath: string, sourceFile?: ts.SourceFile, sourceMap?: string) { - this.#rootDir = rootDir; - this.#filePath = filePath; + constructor( + context: LinterContext, resourcePath: ResourcePath, + sourceFile: ts.SourceFile, sourceMap: string | undefined + ) { + this.#context = context; + this.#resourcePath = resourcePath; this.#sourceFile = sourceFile; if (sourceMap) { this.#traceMap = new TraceMap(sourceMap); } + + this.#originalResourcePath = this.#getOriginalResourcePath() ?? resourcePath; + // Do not use messages from context yet, to allow local de-duplication + this.#coverageInfo = context.getCoverageInfo(this.#originalResourcePath); } addMessage({node, message, messageDetails, severity, ruleId, fatal = undefined}: ReporterMessage) { @@ -43,7 +53,7 @@ export default class Reporter implements BaseReporter { let line = 1, column = 1; if (node) { - const {start} = this.#getPositionsForNode((node as ts.Node)); + const {start} = this.#getPositionsForNode(node); // One-based to be aligned with most IDEs line = start.line + 1; column = start.column + 1; @@ -68,7 +78,7 @@ export default class Reporter implements BaseReporter { } addCoverageInfo({node, message, messageDetails, category}: ReporterCoverageInfo) { - const {start} = this.#getPositionsForNode((node as ts.Node)); + const {start} = this.#getPositionsForNode(node); const coverageInfo: CoverageInfo = { category, // One-based to be aligned with most IDEs @@ -95,7 +105,7 @@ export default class Reporter implements BaseReporter { #getPosition(pos: number): PositionInfo { if (!this.#sourceFile) { - throw new Error(`No source file available for file ${this.#filePath}`); + throw new Error(`No source file available for file ${this.#resourcePath}`); } // Typescript positions are all zero-based const {line, character: column} = this.#sourceFile.getLineAndCharacterOfPosition(pos); @@ -134,22 +144,20 @@ export default class Reporter implements BaseReporter { }; } - #getFileName(): string { - let formattedFilePath: string = this.#filePath; + #getOriginalResourcePath(): ResourcePath | undefined { if (this.#traceMap?.sources?.length && this.#traceMap.sources[0] && this.#traceMap.sources[0] !== "UNKNOWN") { - formattedFilePath = path.join(path.dirname(this.#filePath), this.#traceMap.sources[0]); + return path.join(path.dirname(this.#resourcePath), this.#traceMap.sources[0]); } - // re-format from absolute to relative path: - return path.relative(this.#rootDir, formattedFilePath); } - getReport(): LintResult { - let errorCount = 0; - let warningCount = 0; - let fatalErrorCount = 0; + deduplicateMessages() { const lineColumnMessagesMap = new Map(); const messages: LintMessage[] = []; + if (this.#messages.length === 0) { + return; + } + for (const message of this.#messages) { // Group messages by line/column so that we can deduplicate them if (!message.line || !message.column) { @@ -179,25 +187,10 @@ export default class Reporter implements BaseReporter { // Skip global messages if there are other messages for the same line/column return; } - if (message.severity === LintMessageSeverity.Error) { - errorCount++; - if (message.fatal) { - fatalErrorCount++; - } - } else { - warningCount++; - } messages.push(message); }); } - return { - filePath: this.#getFileName(), - messages, - coverageInfo: this.#coverageInfo, - errorCount, - warningCount, - fatalErrorCount, - }; + this.#context.getLintingMessages(this.#originalResourcePath).push(...messages); } } diff --git a/src/linter/ui5Types/TypeLinter.ts b/src/linter/ui5Types/TypeLinter.ts new file mode 100644 index 000000000..dde3d155f --- /dev/null +++ b/src/linter/ui5Types/TypeLinter.ts @@ -0,0 +1,118 @@ +import ts from "typescript"; +import {FileContents, createVirtualCompilerHost} from "./host.js"; +import SourceFileLinter from "./SourceFileLinter.js"; +import {taskStart} from "../../util/perf.js"; +import {getLogger} from "@ui5/logger"; +import LinterContext, {LinterParameters} from "../LinterContext.js"; +// import {Project} from "@ui5/project"; +import {AbstractAdapter} from "@ui5/fs"; + +const log = getLogger("linter:ui5Types:TypeLinter"); + +const DEFAULT_OPTIONS: ts.CompilerOptions = { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ES2022, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + // Skip lib check to speed up linting. Libs should generally be fine, + // we might want to add a unit test doing the check during development + skipLibCheck: true, + // Include standard typescript libraries for ES2022 and DOM support + lib: ["lib.es2022.d.ts", "lib.dom.d.ts"], + // Allow and check JavaScript files since this is everything we'll do here + allowJs: true, + checkJs: true, + strict: true, + noImplicitAny: false, + strictNullChecks: false, + strictPropertyInitialization: false, + rootDir: "/", + // Library modules (e.g. sap/ui/core/library.js) do not have a default export but + // instead have named exports e.g. for enums defined in the module. + // However, in current JavaScript code (UI5 AMD) the whole object is exported, as there are no + // named exports outside of ES Modules / TypeScript. + // This property compensates this gap and tries to all usage of default imports where actually + // no default export is defined. + // NOTE: This setting should not be used when analyzing TypeScript code, as it would allow + // using an default import on library modules, which is not intended. + // A better solution: + // During transpilation, for every library module (where no default export exists), + // an "import * as ABC" instead of a default import is created. + // This logic needs to be in sync with the generator for UI5 TypeScript definitions. + allowSyntheticDefaultImports: true, +}; + +export default class TypeChecker { + #compilerOptions: ts.CompilerOptions; + #context: LinterContext; + #workspace: AbstractAdapter; + + constructor({workspace, context}: LinterParameters) { + this.#context = context; + this.#workspace = workspace; + this.#compilerOptions = {...DEFAULT_OPTIONS}; + + const namespace = context.getNamespace(); + if (namespace) { + // Map namespace used in imports (without /resources) to /resources paths + this.#compilerOptions.paths = { + [`${namespace}/*`]: [`/resources/${namespace}/*`], + }; + } + } + + async lint() { + const files: FileContents = new Map(); + const sourceMaps = new Map(); // Maps a source path to source map content + let lazyFileLoading = true; + + const resources = await this.#workspace.byGlob("/**/{*.js,*.js.map,*.ts}"); + let pathsToLint = this.#context.getPathsToLint(); + if (!pathsToLint?.length) { + lazyFileLoading = false; + pathsToLint = resources.map((resource) => resource.getPath()); + } + for (const resource of resources) { + const resourcePath = resource.getPath(); + if (resourcePath.endsWith(".js.map")) { + sourceMaps.set( + // Remove ".map" from path to have it reflect the associated source path + resource.getPath().slice(0, -4), + await resource.getString() + ); + } else { + if (lazyFileLoading && resource.getSourceMetadata().adapter === "FileSystem" && + !resource.getSourceMetadata().contentModified) { + files.set(resourcePath, () => ts.sys.readFile(resource.getSourceMetadata().fsPath) ?? ""); + } else { + files.set(resourcePath, await resource.getString()); + } + } + } + + const host = await createVirtualCompilerHost(this.#compilerOptions, files, sourceMaps); + const program = ts.createProgram(pathsToLint, this.#compilerOptions, host); + const checker = program.getTypeChecker(); + + const reportCoverage = this.#context.getReportCoverage(); + const messageDetails = this.#context.getIncludeMessageDetails(); + const typeCheckDone = taskStart("Linting all transpiled resources"); + for (const sourceFile of program.getSourceFiles()) { + if (!sourceFile.isDeclarationFile && pathsToLint.includes(sourceFile.fileName)) { + const sourceMap = sourceMaps.get(sourceFile.fileName); + if (!sourceMap) { + log.warn(`Failed to get source map for ${sourceFile.fileName}`); + } + const linterDone = taskStart("Type-check resource", sourceFile.fileName, true); + const linter = new SourceFileLinter( + this.#context, + sourceFile.fileName, sourceFile, + sourceMap, + checker, reportCoverage, messageDetails + ); + await linter.lint(); + linterDone(); + } + } + typeCheckDone(); + } +} diff --git a/src/detectors/transpilers/amd/moduleDeclarationToDefinition.ts b/src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts similarity index 99% rename from src/detectors/transpilers/amd/moduleDeclarationToDefinition.ts rename to src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts index ac995a552..ec0a5537d 100644 --- a/src/detectors/transpilers/amd/moduleDeclarationToDefinition.ts +++ b/src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts @@ -6,7 +6,7 @@ import {UnsupportedModuleError, toPosStr} from "./util.js"; import pruneNode from "./pruneNode.js"; const {SyntaxKind} = ts; -const log = getLogger("transpilers:amd:moduleDeclarationToDefinition"); +const log = getLogger("linter:ui5Types:amdTranspiler:moduleDeclarationToDefinition"); export interface ModuleDefinition { name?: string; diff --git a/src/detectors/transpilers/amd/parseModuleDeclaration.ts b/src/linter/ui5Types/amdTranspiler/parseModuleDeclaration.ts similarity index 99% rename from src/detectors/transpilers/amd/parseModuleDeclaration.ts rename to src/linter/ui5Types/amdTranspiler/parseModuleDeclaration.ts index 756077e88..8c2ad35de 100644 --- a/src/detectors/transpilers/amd/parseModuleDeclaration.ts +++ b/src/linter/ui5Types/amdTranspiler/parseModuleDeclaration.ts @@ -2,7 +2,7 @@ import ts from "typescript"; import {getLogger} from "@ui5/logger"; import {UnsupportedModuleError} from "./util.js"; -const log = getLogger("transpilers:amd:parseModuleDeclaration"); +const log = getLogger("linter:ui5Types:amdTranspiler:parseModuleDeclaration"); const {SyntaxKind} = ts; diff --git a/src/detectors/transpilers/amd/parseRequire.ts b/src/linter/ui5Types/amdTranspiler/parseRequire.ts similarity index 98% rename from src/detectors/transpilers/amd/parseRequire.ts rename to src/linter/ui5Types/amdTranspiler/parseRequire.ts index 2eb5707ee..2057bbf64 100644 --- a/src/detectors/transpilers/amd/parseRequire.ts +++ b/src/linter/ui5Types/amdTranspiler/parseRequire.ts @@ -1,7 +1,7 @@ import ts from "typescript"; import {getLogger} from "@ui5/logger"; import {UnsupportedModuleError, toPosStr} from "./util.js"; -const log = getLogger("amd:parseRequire"); +const log = getLogger("linter:ui5Types:amdTranspiler:parseRequire"); const {SyntaxKind} = ts; diff --git a/src/detectors/transpilers/amd/pruneNode.ts b/src/linter/ui5Types/amdTranspiler/pruneNode.ts similarity index 99% rename from src/detectors/transpilers/amd/pruneNode.ts rename to src/linter/ui5Types/amdTranspiler/pruneNode.ts index efef3196d..6022af338 100644 --- a/src/detectors/transpilers/amd/pruneNode.ts +++ b/src/linter/ui5Types/amdTranspiler/pruneNode.ts @@ -2,7 +2,7 @@ import ts from "typescript"; import {getLogger} from "@ui5/logger"; import {toPosStr, UnsupportedModuleError} from "./util.js"; const {SyntaxKind} = ts; -const log = getLogger("transpilers:amd:pruneNode"); +const log = getLogger("linter:ui5Types:amdTranspiler:pruneNode"); export class UnsafeNodeRemoval extends Error { constructor(message: string) { diff --git a/src/detectors/transpilers/amd/replaceNodeInParent.ts b/src/linter/ui5Types/amdTranspiler/replaceNodeInParent.ts similarity index 99% rename from src/detectors/transpilers/amd/replaceNodeInParent.ts rename to src/linter/ui5Types/amdTranspiler/replaceNodeInParent.ts index f17a11697..b1ffced7a 100644 --- a/src/detectors/transpilers/amd/replaceNodeInParent.ts +++ b/src/linter/ui5Types/amdTranspiler/replaceNodeInParent.ts @@ -2,7 +2,7 @@ import ts from "typescript"; import {getLogger} from "@ui5/logger"; const {SyntaxKind} = ts; import {toPosStr, UnsupportedModuleError} from "./util.js"; -const log = getLogger("transpilers:amd:replaceNodeInParent"); +const log = getLogger("linter:ui5Types:amdTranspiler:replaceNodeInParent"); export interface NodeReplacement { original: ts.Node; diff --git a/src/detectors/transpilers/amd/requireExpressionToTransformation.ts b/src/linter/ui5Types/amdTranspiler/requireExpressionToTransformation.ts similarity index 98% rename from src/detectors/transpilers/amd/requireExpressionToTransformation.ts rename to src/linter/ui5Types/amdTranspiler/requireExpressionToTransformation.ts index 68b4868ec..38b92a2a7 100644 --- a/src/detectors/transpilers/amd/requireExpressionToTransformation.ts +++ b/src/linter/ui5Types/amdTranspiler/requireExpressionToTransformation.ts @@ -2,7 +2,7 @@ import ts from "typescript"; import {getLogger} from "@ui5/logger"; import {UnsupportedModuleError, toPosStr} from "./util.js"; import {ProbingRequireExpression, RequireExpression} from "./parseRequire.js"; -const log = getLogger("transpilers:amd:transformRequireCall"); +const log = getLogger("linter:ui5Types:amdTranspiler:transformRequireCall"); export interface RequireTransformationAsync { imports: ts.ImportDeclaration[]; diff --git a/src/detectors/transpilers/amd/rewriteExtendCall.ts b/src/linter/ui5Types/amdTranspiler/rewriteExtendCall.ts similarity index 100% rename from src/detectors/transpilers/amd/rewriteExtendCall.ts rename to src/linter/ui5Types/amdTranspiler/rewriteExtendCall.ts diff --git a/src/linter/ui5Types/amdTranspiler/transpiler.ts b/src/linter/ui5Types/amdTranspiler/transpiler.ts new file mode 100644 index 000000000..eb804ae1a --- /dev/null +++ b/src/linter/ui5Types/amdTranspiler/transpiler.ts @@ -0,0 +1,137 @@ +import ts from "typescript"; +import {getLogger} from "@ui5/logger"; +import {taskStart} from "../../../util/perf.js"; +import {TranspileResult} from "../../LinterContext.js"; +import {createTransformer} from "./tsTransformer.js"; +import {UnsupportedModuleError} from "./util.js"; + +const log = getLogger("linter:ui5Types:amdTranspiler:transpiler"); + +type FilePath = string; +type FileContent = string; +type SourceFiles = Map; +type WrittenFiles = Map; + +function createCompilerHost(sourceFiles: SourceFiles, writtenFiles: WrittenFiles): ts.CompilerHost { + return { + getSourceFile: (fileName) => sourceFiles.get(fileName), + writeFile: (name, text) => { + writtenFiles.set(name, text); + }, + getDefaultLibFileName: () => "lib.d.ts", + useCaseSensitiveFileNames: () => false, + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => "", + getNewLine: () => "\n", + fileExists: (fileName): boolean => sourceFiles.has(fileName), + readFile: () => "", + directoryExists: () => true, + getDirectories: () => [], + }; +} + +const compilerOptions = { + moduleResolution: ts.ModuleResolutionKind.NodeNext, + checkJs: true, + allowJs: true, + skipLibCheck: true, + + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ES2022, + isolatedModules: true, + sourceMap: true, + suppressOutputPathCheck: true, + noLib: true, + noResolve: true, + allowNonTsExtensions: true, +}; + +function createProgram(inputFileNames: string[], host: ts.CompilerHost): ts.Program { + return ts.createProgram(inputFileNames, compilerOptions, host); +} + +export function transpileFile(fileName: string, content: string, strict?: boolean): TranspileResult { + // This is heavily inspired by the TypesScript "transpileModule" API + + const taskDone = taskStart("Transpiling AMD to ESM", fileName, true); + const sourceFile = ts.createSourceFile( + fileName, + content, + { + languageVersion: ts.ScriptTarget.ES2022, + jsDocParsingMode: ts.JSDocParsingMode.ParseNone, + } + // /*setParentNodes*/ false, + // ts.ScriptKind.JS + ); + + const sourceFiles: SourceFiles = new Map(); + sourceFiles.set(fileName, sourceFile); + const writtenResources: WrittenFiles = new Map(); + const compilerHost = createCompilerHost(sourceFiles, writtenResources); + const program = createProgram([fileName], compilerHost); + + const transformers: ts.CustomTransformers = { + before: [createTransformer(program)], + }; + + try { + // ts.setEmitFlags(sourceFile, ts.EmitFlags.NoTrailingSourceMap); + // TODO: Investigate whether we can retrieve a source file that can be fed directly into the typeChecker + program.emit( + /* targetSourceFile */ undefined, /* writeFile */ undefined, + /* cancellationToken */ undefined, /* emitOnlyDtsFiles */ undefined, + transformers); + + /* tsc currently does not provide an API to emit TypeScript *with* a source map + (see https://github.com/microsoft/TypeScript/issues/51329) + + The below can be used to emit TypeScript without a source map: + + const result = ts.transform( + sourceFile, + [createTransformer(resourcePath, program)], compilerOptions); + + const printer = ts.createPrinter(); + const printed = printer.printNode(ts.EmitHint.SourceFile, result.transformed[0], sourceFile); + const printed = printer.printFile(result.transformed[0]); + outputText = printed; + */ + } catch (err) { + if (strict) { + throw err; + } + if (err instanceof UnsupportedModuleError) { + log.verbose(`Failed to transform module: ${err.message}`); + if (err.stack && log.isLevelEnabled("verbose")) { + log.verbose(`Stack trace:`); + log.verbose(err.stack); + } + return {source: content, map: ""}; + } else if (err instanceof Error && err.message.startsWith("Debug Failure")) { + // We probably failed to create a valid AST + log.verbose(`AST transformation failed for module: ${err.message}`); + if (err.stack && log.isLevelEnabled("verbose")) { + log.verbose(`Stack trace:`); + log.verbose(err.stack); + } + return {source: content, map: ""}; + } + throw err; + } + + const source = writtenResources.get(fileName); + if (!source) { + throw new Error(`Transpiling yielded no result for ${fileName}`); + } + const map = writtenResources.get(`${fileName}.map`); + if (!map) { + throw new Error(`Transpiling yielded no source map for ${fileName}`); + } + + // Convert sourceMappingURL ending with ".js" to ".ts" + // map = map + // .replace(`//# sourceMappingURL=${fileName}.map`, `//# sourceMappingURL=${fileName}.map`); + taskDone(); + return {source, map}; +} diff --git a/src/detectors/transpilers/amd/TsTransformer.ts b/src/linter/ui5Types/amdTranspiler/tsTransformer.ts similarity index 94% rename from src/detectors/transpilers/amd/TsTransformer.ts rename to src/linter/ui5Types/amdTranspiler/tsTransformer.ts index 6fc9191d1..a86a38e09 100644 --- a/src/detectors/transpilers/amd/TsTransformer.ts +++ b/src/linter/ui5Types/amdTranspiler/tsTransformer.ts @@ -8,7 +8,7 @@ import pruneNode, {UnsafeNodeRemoval} from "./pruneNode.js"; import replaceNodeInParent, {NodeReplacement} from "./replaceNodeInParent.js"; import {UnsupportedModuleError} from "./util.js"; -const log = getLogger("transpilers:amd:TsTransformer"); +const log = getLogger("linter:ui5Types:amdTranspiler:TsTransformer"); // Augment typescript's Node interface to add a property for marking nodes for removal declare module "typescript" { @@ -27,17 +27,19 @@ declare module "typescript" { * error is thrown. In that case, the rest of the module is still processed. However it's possible that the result * will be equal to the input. */ -export function createTransformer(resourcePath: string, program: ts.Program): ts.TransformerFactory { +export function createTransformer(program: ts.Program): ts.TransformerFactory { return function transformer(context: ts.TransformationContext) { return (sourceFile: ts.SourceFile): ts.SourceFile => { - return transform(resourcePath, program, sourceFile, context); + return transform(program, sourceFile, context); }; }; } function transform( - resourcePath: string, program: ts.Program, sourceFile: ts.SourceFile, context: ts.TransformationContext + program: ts.Program, sourceFile: ts.SourceFile, context: ts.TransformationContext ): ts.SourceFile { + const resourcePath = sourceFile.fileName; + log.verbose(`Transforming ${resourcePath}`); const checker = program.getTypeChecker(); const {factory: nodeFactory} = context; const moduleDefinitions: ModuleDefinition[] = []; diff --git a/src/detectors/transpilers/amd/util.ts b/src/linter/ui5Types/amdTranspiler/util.ts similarity index 100% rename from src/detectors/transpilers/amd/util.ts rename to src/linter/ui5Types/amdTranspiler/util.ts diff --git a/src/detectors/typeChecker/host.ts b/src/linter/ui5Types/host.ts similarity index 91% rename from src/detectors/typeChecker/host.ts rename to src/linter/ui5Types/host.ts index ae234e337..e21df1747 100644 --- a/src/detectors/typeChecker/host.ts +++ b/src/linter/ui5Types/host.ts @@ -3,6 +3,8 @@ import path from "node:path"; import posixPath from "node:path/posix"; import fs from "node:fs/promises"; import {createRequire} from "node:module"; +import {transpileFile} from "./amdTranspiler/transpiler.js"; +import {ResourcePath} from "../LinterContext.js"; const require = createRequire(import.meta.url); interface PackageJson { @@ -57,8 +59,11 @@ function addSapui5TypesMappingToCompilerOptions(sapui5TypesFiles: string[], opti }); } +export type FileContents = Map string)>; + export async function createVirtualCompilerHost( - options: ts.CompilerOptions, files: Map + options: ts.CompilerOptions, + files: FileContents, sourceMaps: FileContents ): Promise { const typePathMappings = new Map(); addPathMappingForPackage("typescript", typePathMappings); @@ -108,7 +113,18 @@ export async function createVirtualCompilerHost( // NOTE: This function should be kept in sync with "fileExists" if (files.has(fileName)) { - return files.get(fileName); + let fileContent = files.get(fileName); + if (typeof fileContent === "function") { + fileContent = fileContent(); + } + if (fileContent && fileName.endsWith(".js") && !sourceMaps.get(fileName)) { + // No source map indicates no transpilation was done yet + const res = transpileFile(path.basename(fileName), fileContent); + files.set(fileName, res.source); + sourceMaps.set(fileName, res.map); + fileContent = res.source; + } + return fileContent; } if (fileName.startsWith("/types/")) { const fsPath = mapToTypePath(fileName); diff --git a/src/detectors/transpilers/xml/Parser.ts b/src/linter/xmlTemplate/Parser.ts similarity index 97% rename from src/detectors/transpilers/xml/Parser.ts rename to src/linter/xmlTemplate/Parser.ts index 0377959b6..07f2e5d78 100644 --- a/src/detectors/transpilers/xml/Parser.ts +++ b/src/linter/xmlTemplate/Parser.ts @@ -3,12 +3,12 @@ import he from "he"; import ViewGenerator from "./generator/ViewGenerator.js"; import FragmentGenerator from "./generator/FragmentGenerator.js"; import JSTokenizer from "./lib/JSTokenizer.js"; -import {LintMessage, LintMessageSeverity} from "../../AbstractDetector.js"; -import {TranspileResult} from "../AbstractTranspiler.js"; +import LinterContext, {LintMessageSeverity} from "../LinterContext.js"; +import {TranspileResult} from "../LinterContext.js"; import AbstractGenerator from "./generator/AbstractGenerator.js"; import {getLogger} from "@ui5/logger"; import {ApiExtract} from "./transpiler.js"; -const log = getLogger("transpilers:xml:Parser"); +const log = getLogger("linter:xmlTemplate:Parser"); export type Namespace = string; export interface NamespaceDeclaration { @@ -125,14 +125,14 @@ export default class Parser { #resourceName: string; #xmlDocumentKind: DocumentKind; - #messages: LintMessage[] = []; + #context: LinterContext; #namespaceStack: NamespaceStackEntry[] = []; #nodeStack: NodeDeclaration[] = []; #generator: AbstractGenerator; #apiExtract: ApiExtract; - constructor(resourceName: string, apiExtract: ApiExtract) { + constructor(resourceName: string, apiExtract: ApiExtract, context: LinterContext) { const xmlDocumentKind = determineDocumentKind(resourceName); if (xmlDocumentKind === null) { throw new Error(`Unknown document type for resource ${resourceName}`); @@ -144,6 +144,7 @@ export default class Parser { new FragmentGenerator(resourceName); this.#apiExtract = apiExtract; + this.#context = context; } pushTag(tag: SaxTag) { @@ -175,7 +176,6 @@ export default class Parser { return { source, map, - messages: this.#messages, }; } @@ -317,7 +317,7 @@ export default class Parser { throw new Error(`Unknown namespace ${tagNamespace} for tag ${tagName} in resource ${this.#resourceName}`); } else if (namespace === SVG_NAMESPACE) { // Ignore SVG nodes - this.#messages.push({ + this.#context.addLintingMessage(this.#resourceName, { ruleId: "ui5-linter-no-deprecated-api", severity: LintMessageSeverity.Error, line: tag.openStart.line + 1, // Add one to align with IDEs @@ -333,7 +333,7 @@ export default class Parser { }; } else if (namespace === XHTML_NAMESPACE) { // Ignore XHTML nodes for now - this.#messages.push({ + this.#context.addLintingMessage(this.#resourceName, { ruleId: "ui5-linter-no-deprecated-api", severity: LintMessageSeverity.Error, line: tag.openStart.line + 1, // Add one to align with IDEs diff --git a/src/detectors/transpilers/xml/generator/AbstractGenerator.ts b/src/linter/xmlTemplate/generator/AbstractGenerator.ts similarity index 95% rename from src/detectors/transpilers/xml/generator/AbstractGenerator.ts rename to src/linter/xmlTemplate/generator/AbstractGenerator.ts index 3059ea273..fd48025f8 100644 --- a/src/detectors/transpilers/xml/generator/AbstractGenerator.ts +++ b/src/linter/xmlTemplate/generator/AbstractGenerator.ts @@ -2,6 +2,7 @@ import { ControlDeclaration, RequireExpression, Position, } from "../Parser.js"; import Writer from "./Writer.js"; +import path from "node:path/posix"; interface ImportStatement { moduleName: string; @@ -17,8 +18,9 @@ export default abstract class AbstractGenerator { _variableNames = new Set(); _body: Writer; - constructor(resourceName: string) { - this._body = new Writer(resourceName.replace(/.xml$/, ".js"), resourceName); + constructor(filePath: string) { + const fileName = path.basename(filePath, ".xml"); + this._body = new Writer(fileName + ".js", fileName + ".xml"); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/detectors/transpilers/xml/generator/FragmentGenerator.ts b/src/linter/xmlTemplate/generator/FragmentGenerator.ts similarity index 100% rename from src/detectors/transpilers/xml/generator/FragmentGenerator.ts rename to src/linter/xmlTemplate/generator/FragmentGenerator.ts diff --git a/src/detectors/transpilers/xml/generator/ViewGenerator.ts b/src/linter/xmlTemplate/generator/ViewGenerator.ts similarity index 100% rename from src/detectors/transpilers/xml/generator/ViewGenerator.ts rename to src/linter/xmlTemplate/generator/ViewGenerator.ts diff --git a/src/detectors/transpilers/xml/generator/Writer.ts b/src/linter/xmlTemplate/generator/Writer.ts similarity index 100% rename from src/detectors/transpilers/xml/generator/Writer.ts rename to src/linter/xmlTemplate/generator/Writer.ts diff --git a/src/detectors/transpilers/xml/lib/JSTokenizer.d.ts b/src/linter/xmlTemplate/lib/JSTokenizer.d.ts similarity index 100% rename from src/detectors/transpilers/xml/lib/JSTokenizer.d.ts rename to src/linter/xmlTemplate/lib/JSTokenizer.d.ts diff --git a/src/detectors/transpilers/xml/lib/JSTokenizer.js b/src/linter/xmlTemplate/lib/JSTokenizer.js similarity index 100% rename from src/detectors/transpilers/xml/lib/JSTokenizer.js rename to src/linter/xmlTemplate/lib/JSTokenizer.js diff --git a/src/linter/xmlTemplate/linter.ts b/src/linter/xmlTemplate/linter.ts new file mode 100644 index 000000000..5cb11ab6a --- /dev/null +++ b/src/linter/xmlTemplate/linter.ts @@ -0,0 +1,42 @@ +import {Resource} from "@ui5/fs"; +import {createResource} from "@ui5/fs/resourceFactory"; +import transpileXml from "./transpiler.js"; +import {LinterParameters} from "../LinterContext.js"; + +export default async function lintXml({workspace, context}: LinterParameters) { + let xmlResources: Resource[]; + const pathsToLint = context.getPathsToLint(); + if (pathsToLint?.length) { + xmlResources = []; + await Promise.all(pathsToLint.map(async (resourcePath) => { + if (!resourcePath.endsWith(".view.xml") && !resourcePath.endsWith(".fragment.xml")) { + return; + } + const resource = await workspace.byPath(resourcePath); + if (!resource) { + throw new Error(`Resource not found: ${resourcePath}`); + } + xmlResources.push(resource); + })); + } else { + xmlResources = await workspace.byGlob("**/{*.view.xml,*.fragment.xml}"); + } + + await Promise.all(xmlResources.map(async (resource: Resource) => { + const {source, map} = await transpileXml(resource.getPath(), resource.getStream(), context); + const resourcePath = resource.getPath(); + + // Write transpiled resource to workspace + // TODO: suffix name to prevent clashes with existing files? + const jsPath = resourcePath.replace(/\.xml$/, ".js"); + context.addPathToLint(jsPath); + await workspace.write(createResource({ + path: jsPath, + string: source, + })); + await workspace.write(createResource({ + path: jsPath + ".map", + string: map, + })); + })); +} diff --git a/src/detectors/transpilers/xml/transpiler.ts b/src/linter/xmlTemplate/transpiler.ts similarity index 75% rename from src/detectors/transpilers/xml/transpiler.ts rename to src/linter/xmlTemplate/transpiler.ts index c912d2e35..2bc632354 100644 --- a/src/detectors/transpilers/xml/transpiler.ts +++ b/src/linter/xmlTemplate/transpiler.ts @@ -3,13 +3,13 @@ import {ReadStream} from "node:fs"; import fs from "node:fs/promises"; import {finished} from "node:stream/promises"; import {taskStart} from "../../util/perf.js"; -import {TranspileResult} from "../AbstractTranspiler.js"; +import LinterContext, {TranspileResult} from "../LinterContext.js"; import Parser from "./Parser.js"; import {getLogger} from "@ui5/logger"; import {createRequire} from "node:module"; const require = createRequire(import.meta.url); -const log = getLogger("transpilers:xml:transpiler"); +const log = getLogger("linter:xmlTemplate:transpiler"); export interface ApiExtract { framework: { @@ -22,20 +22,22 @@ export interface ApiExtract { let saxWasmBuffer: Buffer; let apiExtract: ApiExtract; -export async function xmlToJs(resourceName: string, contentStream: ReadStream): Promise { +export default async function transpileXml( + resourcePath: string, contentStream: ReadStream, context: LinterContext +): Promise { await init(); try { - const taskEnd = taskStart("Transpile XML", resourceName, true); - const res = await transpileXmlToJs(resourceName, contentStream); + const taskEnd = taskStart("Transpile XML", resourcePath, true); + const res = await transpileXmlToJs(resourcePath, contentStream, context); taskEnd(); if (!res.source) { - log.verbose(`XML transpiler returned no result for ${resourceName}`); + log.verbose(`XML transpiler returned no result for ${resourcePath}`); return res; } return res; } catch (err) { if (err instanceof Error) { - throw new Error(`Failed to transpile resource ${resourceName}: ${err.message}`, { + throw new Error(`Failed to transpile resource ${resourcePath}: ${err.message}`, { cause: err, }); } else { @@ -56,7 +58,7 @@ async function init() { return initializing = Promise.all([ fs.readFile(saxPath), - fs.readFile(new URL("../../../../resources/api-extract.json", import.meta.url), {encoding: "utf-8"}), + fs.readFile(new URL("../../../resources/api-extract.json", import.meta.url), {encoding: "utf-8"}), ]).then((results) => { saxWasmBuffer = results[0]; apiExtract = JSON.parse(results[1]) as ApiExtract; @@ -64,8 +66,10 @@ async function init() { }); } -async function transpileXmlToJs(resourceName: string, contentStream: ReadStream): Promise { - const parser = new Parser(resourceName, apiExtract); +async function transpileXmlToJs( + resourcePath: string, contentStream: ReadStream, context: LinterContext +): Promise { + const parser = new Parser(resourcePath, apiExtract, context); // Initialize parser const options = {highWaterMark: 32 * 1024}; // 32k chunks diff --git a/src/untyped.d.ts b/src/untyped.d.ts index 548a0dad3..0e47ce5ad 100644 --- a/src/untyped.d.ts +++ b/src/untyped.d.ts @@ -7,6 +7,10 @@ declare module "@ui5/project" { getNamespace: () => ProjectNamespace; getReader: (options: import("@ui5/fs").ReaderOptions) => import("@ui5/fs").AbstractReader; getRootPath: () => string; + getSourcePath: () => string; + _testPath: string; // TODO UI5 Tooling: Expose API for optional test path + _testPathExists: string; + _isSourceNamespaced: boolean; } interface ProjectGraph { getRoot: () => Project; @@ -61,6 +65,8 @@ declare module "@ui5/fs" { type ResourcePath = string; interface ResourceSourceMetadata { fsPath: string; + adapter: string; + contentModified: boolean; } interface Resource { getBuffer: () => Promise; @@ -81,6 +87,7 @@ declare module "@ui5/fs" { } export interface AbstractReader { byGlob: (virPattern: string | string[], options?: GlobOptions) => Promise; + byPath: (path: string) => Promise; } export interface AbstractAdapter extends AbstractReader { write: (resource: Resource) => Promise; @@ -88,10 +95,30 @@ declare module "@ui5/fs" { } declare module "@ui5/fs/resourceFactory" { - export function createAdapter(parameters: {fsBasePath: string; virBasePath: string}): - import("@ui5/fs").AbstractAdapter; - export function createResource(parameters: {path: string; string: string}): - import("@ui5/fs").Resource; + export function createAdapter( + parameters: {fsBasePath: string; virBasePath: string} + ): import("@ui5/fs").AbstractAdapter; + export function createResource( + parameters: {path: string; string: string; sourceMetadata?: object} + ): import("@ui5/fs").Resource; + export function createReader( + parameters: { + fsBasePath: string; + virBasePath: string; + project?: import("@ui5/project").Project; + excludes?: string[]; + name?: string; + } + ): import("@ui5/fs").AbstractReader; + export function createWorkspace( + parameters: {reader: import("@ui5/fs").AbstractReader} + ): import("@ui5/fs").AbstractAdapter; + export function createReaderCollection( + parameters: { + readers: import("@ui5/fs").AbstractReader[]; + name?: string; + } + ): import("@ui5/fs").AbstractAdapter; } declare module "@ui5/logger" { diff --git a/src/detectors/util/perf.ts b/src/util/perf.ts similarity index 100% rename from src/detectors/util/perf.ts rename to src/util/perf.ts diff --git a/test/lib/cli/base.integration.ts b/test/lib/cli/base.integration.ts index 0ac1dcb5f..95af56bdf 100644 --- a/test/lib/cli/base.integration.ts +++ b/test/lib/cli/base.integration.ts @@ -4,7 +4,7 @@ import yargs, {Argv} from "yargs"; import path from "node:path"; import cliBase from "../../../src/cli/base.js"; import {fileURLToPath} from "node:url"; -import {LintResult} from "../../../src/detectors/AbstractDetector.js"; +import {LintResult} from "../../../src/linter/LinterContext.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const sampleProjectPath = path.join(__dirname, "..", "..", "fixtures", "linter", "projects", "com.ui5.troublesome.app"); diff --git a/test/lib/cli/base.ts b/test/lib/cli/base.ts index d50110f8a..99e98aa82 100644 --- a/test/lib/cli/base.ts +++ b/test/lib/cli/base.ts @@ -5,7 +5,7 @@ import esmock from "esmock"; import chalk from "chalk"; import yargs, {Argv} from "yargs"; import path from "node:path"; -import type {LintResult} from "../../../src/detectors/AbstractDetector.js"; +import type {LintResult} from "../../../src/linter/LinterContext.js"; import type Base from "../../../src/cli/base.js"; const test = anyTest as TestFn<{ @@ -98,8 +98,8 @@ test.serial("ui5lint (default) ", async (t) => { t.true(lintProject.calledOnce, "Linter is called"); t.is(writeFile.callCount, 0, "Coverage was not called"); t.deepEqual(lintProject.getCall(0).args[0], { - rootDir: path.join(process.cwd()), filePaths: undefined, - messageDetails: false, reportCoverage: false, + rootDir: path.join(process.cwd()), pathsToLint: undefined, + includeMessageDetails: false, reportCoverage: false, }); t.is(t.context.consoleLogStub.callCount, 0, "console.log should not be used"); }); @@ -115,8 +115,8 @@ test.serial("ui5lint --file-paths ", async (t) => { t.true(lintProject.calledOnce, "Linter is called"); t.deepEqual(lintProject.getCall(0).args[0], { - rootDir: path.join(process.cwd()), filePaths, - messageDetails: false, reportCoverage: false, + rootDir: path.join(process.cwd()), pathsToLint: filePaths, + includeMessageDetails: false, reportCoverage: false, }); t.is(t.context.consoleLogStub.callCount, 0, "console.log should not be used"); }); @@ -129,8 +129,8 @@ test.serial("ui5lint --coverage ", async (t) => { t.true(lintProject.calledOnce, "Linter is called"); t.is(writeFile.callCount, 1, "Coverage was called"); t.deepEqual(lintProject.getCall(0).args[0], { - rootDir: path.join(process.cwd()), filePaths: undefined, - messageDetails: false, reportCoverage: true, + rootDir: path.join(process.cwd()), pathsToLint: undefined, + includeMessageDetails: false, reportCoverage: true, }); t.is(t.context.consoleLogStub.callCount, 0, "console.log should not be used"); }); diff --git a/test/lib/detectors/transpilers/amd/snapshots/transpiler.ts.snap b/test/lib/detectors/transpilers/amd/snapshots/transpiler.ts.snap deleted file mode 100644 index 932b959e5..000000000 Binary files a/test/lib/detectors/transpilers/amd/snapshots/transpiler.ts.snap and /dev/null differ diff --git a/test/lib/formatter/json.ts b/test/lib/formatter/json.ts index e7d476e9b..cb144fe5e 100644 --- a/test/lib/formatter/json.ts +++ b/test/lib/formatter/json.ts @@ -1,6 +1,6 @@ import anyTest, {TestFn} from "ava"; import {Json} from "../../../src/formatter/json.js"; -import {LintResult} from "../../../src/detectors/AbstractDetector.js"; +import {LintResult} from "../../../src/linter/LinterContext.js"; const test = anyTest as TestFn<{ lintResults: LintResult[]; diff --git a/test/lib/linter/_linterHelper.ts b/test/lib/linter/_linterHelper.ts index 1054eae81..60d21dce0 100644 --- a/test/lib/linter/_linterHelper.ts +++ b/test/lib/linter/_linterHelper.ts @@ -3,10 +3,9 @@ import sinonGlobal, {SinonStub} from "sinon"; import util from "util"; import {readdirSync} from "node:fs"; import esmock from "esmock"; -import {LintResult} from "../../../src/detectors/AbstractDetector.js"; -import FileLinter from "../../../src/detectors/typeChecker/FileLinter.js"; +import SourceFileLinter from "../../../src/linter/ui5Types/SourceFileLinter.js"; import {SourceFile, TypeChecker} from "typescript"; -import {LinterOptions} from "../../../src/linter/linter.js"; +import LinterContext, {LinterOptions, LintResult} from "../../../src/linter/LinterContext.js"; util.inspect.defaultOptions.depth = 4; // Increase AVA's printing depth since coverageInfo objects are on level 4 @@ -23,28 +22,31 @@ test.before(async (t) => { // Mock getDeprecationText as we do not have control over the deprecated texts and they could // change anytime creating false positive failing tests. That way is ensured consistent and testable behavior. export async function esmockDeprecationText() { - const checkerModule = await esmock("../../../src/detectors/typeChecker/index.js", { - "../../../src/detectors/typeChecker/FileLinter.js": + const typeLinterModule = await esmock("../../../src/linter/ui5Types/TypeLinter.js", { + "../../../src/linter/ui5Types/SourceFileLinter.js": function ( - rootDir: string, filePath: string, sourceFile: SourceFile, + context: LinterContext, filePath: string, sourceFile: SourceFile, sourceMap: string | undefined, checker: TypeChecker, reportCoverage: boolean | undefined = false, messageDetails: boolean | undefined = false ) { // Don't use sinon's stubs as it's hard to clean after them in this case and it leaks memory. - const linter = new FileLinter( - rootDir, filePath, sourceFile, sourceMap, checker, reportCoverage, messageDetails + const linter = new SourceFileLinter( + context, filePath, sourceFile, sourceMap, checker, reportCoverage, messageDetails ); linter.getDeprecationText = () => "Deprecated test message"; return linter; }, }); + const lintWorkspaceModule = await esmock("../../../src/linter/lintWorkspace.js", { + "../../../src/linter/ui5Types/TypeLinter.js": typeLinterModule, + }); const lintModule = await esmock("../../../src/linter/linter.js", { - "../../../src/detectors/typeChecker/index.js": checkerModule, + "../../../src/linter/lintWorkspace.js": lintWorkspaceModule, }); - return {lintModule, checkerModule}; + return {lintModule}; } // Helper function to compare file paths since we don't want to store those in the snapshots @@ -72,7 +74,6 @@ export function createTestsForFixtures(fixturesPath: string) { // Ignore non-JavaScript, non-XML, non-JSON and non-HTML files continue; } - let testName = fileName; let defineTest = test.serial; if (fileName.startsWith("_")) { @@ -92,9 +93,9 @@ export function createTestsForFixtures(fixturesPath: string) { const res = await lintFile({ rootDir: fixturesPath, - filePaths, + pathsToLint: filePaths, reportCoverage: true, - messageDetails: true, + includeMessageDetails: true, }); assertExpectedLintResults(t, res, fixturesPath, filePaths); res.forEach((results) => { diff --git a/test/lib/detectors/transpilers/amd/_helper.ts b/test/lib/linter/js/_helper.ts similarity index 90% rename from test/lib/detectors/transpilers/amd/_helper.ts rename to test/lib/linter/js/_helper.ts index 6bf17dcaa..cb99bf048 100644 --- a/test/lib/detectors/transpilers/amd/_helper.ts +++ b/test/lib/linter/js/_helper.ts @@ -4,7 +4,7 @@ import path from "node:path"; import util from "util"; import {readdirSync} from "node:fs"; import fs from "node:fs/promises"; -import {amdToEsm} from "../../../../../src/detectors/transpilers/amd/transpiler.js"; +import {transpileFile} from "../../../../src/linter/ui5Types/amdTranspiler/transpiler.js"; util.inspect.defaultOptions.depth = 4; // Increase AVA's printing depth since coverageInfo objects are on level 4 @@ -41,7 +41,7 @@ export function createTestsForFixtures(fixturesPath: string) { defineTest(`Transpile ${testName}`, async (t) => { const filePath = path.join(fixturesPath, fileName); const fileContent = await fs.readFile(filePath); - const {source, map} = amdToEsm(testName, fileContent.toString(), true); + const {source, map} = transpileFile(testName, fileContent.toString(), true); t.snapshot(source); t.snapshot(map && JSON.parse(map)); }); diff --git a/test/lib/detectors/transpilers/amd/parseModuleDeclaration.ts b/test/lib/linter/js/parseModuleDeclaration.ts similarity index 98% rename from test/lib/detectors/transpilers/amd/parseModuleDeclaration.ts rename to test/lib/linter/js/parseModuleDeclaration.ts index 530940c07..a53b24908 100644 --- a/test/lib/detectors/transpilers/amd/parseModuleDeclaration.ts +++ b/test/lib/linter/js/parseModuleDeclaration.ts @@ -1,7 +1,7 @@ import test from "ava"; import ts from "typescript"; import {ModuleDeclaration, DefineCallArgument, _matchArgumentsToParameters} from - "../../../../../src/detectors/transpilers/amd/parseModuleDeclaration.js"; + "../../../../src/linter/ui5Types/amdTranspiler/parseModuleDeclaration.js"; const {SyntaxKind} = ts; test("All parameters provided directly", (t) => { diff --git a/test/lib/detectors/transpilers/amd/snapshots/transpiler.ts.md b/test/lib/linter/js/snapshots/transpiler.ts.md similarity index 96% rename from test/lib/detectors/transpilers/amd/snapshots/transpiler.ts.md rename to test/lib/linter/js/snapshots/transpiler.ts.md index 35ed0ea49..8833c8ab3 100644 --- a/test/lib/detectors/transpilers/amd/snapshots/transpiler.ts.md +++ b/test/lib/linter/js/snapshots/transpiler.ts.md @@ -1,4 +1,4 @@ -# Snapshot report for `test/lib/detectors/transpilers/amd/transpiler.ts` +# Snapshot report for `test/lib/linter/js/transpiler.ts` The actual snapshot is saved in `transpiler.ts.snap`. @@ -12,7 +12,7 @@ Generated by [AVA](https://avajs.dev). import Device from "sap/ui/Device";␊ export default class Component extends UIComponent {␊ }␊ - //# sourceMappingURL=Dependencies.ts.map` + //# sourceMappingURL=Dependencies.js.map` > Snapshot 2 @@ -38,7 +38,7 @@ Generated by [AVA](https://avajs.dev). return alphaOrBeta.verb() + 2 + delta();␊ }␊ };␊ - //# sourceMappingURL=Dependencies_Dynamic.ts.map` + //# sourceMappingURL=Dependencies_Dynamic.js.map` > Snapshot 2 @@ -62,7 +62,7 @@ Generated by [AVA](https://avajs.dev). import models from "./model/models";␊ export default class Component extends UIComponent {␊ }␊ - //# sourceMappingURL=Dependencies_LocalImport.ts.map` + //# sourceMappingURL=Dependencies_LocalImport.js.map` > Snapshot 2 @@ -86,7 +86,7 @@ Generated by [AVA](https://avajs.dev). return x + y;␊ }␊ };␊ - //# sourceMappingURL=Dependencies_NotProvided_Export_Object.ts.map` + //# sourceMappingURL=Dependencies_NotProvided_Export_Object.js.map` > Snapshot 2 @@ -110,7 +110,7 @@ Generated by [AVA](https://avajs.dev). import models from "./model/models";␊ export default class Component extends UIComponent {␊ }␊ - //# sourceMappingURL=Dependencies_Var.ts.map` + //# sourceMappingURL=Dependencies_Var.js.map` > Snapshot 2 @@ -132,7 +132,7 @@ Generated by [AVA](https://avajs.dev). `import ControllerExtension from "sap/ui/core/mvc/ControllerExtension";␊ export default class ReuseExtension extends ControllerExtension {␊ }␊ - //# sourceMappingURL=ExportFlag.ts.map` + //# sourceMappingURL=ExportFlag.js.map` > Snapshot 2 @@ -161,7 +161,7 @@ Generated by [AVA](https://avajs.dev). return oModel;␊ }␊ };␊ - //# sourceMappingURL=Export_Object.ts.map` + //# sourceMappingURL=Export_Object.js.map` > Snapshot 2 @@ -185,7 +185,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_ArrowFunction.ts.map` + //# sourceMappingURL=Factory_ArrowFunction.js.map` > Snapshot 2 @@ -207,7 +207,7 @@ Generated by [AVA](https://avajs.dev). `import Controller from "sap/ui/core/mvc/Controller";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_ArrowFunctionExpr.ts.map` + //# sourceMappingURL=Factory_ArrowFunctionExpr.js.map` > Snapshot 2 @@ -241,7 +241,7 @@ Generated by [AVA](https://avajs.dev). return btn;␊ }␊ }␊ - //# sourceMappingURL=Factory_BasicModule.ts.map` + //# sourceMappingURL=Factory_BasicModule.js.map` > Snapshot 2 @@ -262,7 +262,7 @@ Generated by [AVA](https://avajs.dev). `import ControllerExtension from "sap/ui/core/mvc/ControllerExtension";␊ export default ControllerExtension.extend({});␊ - //# sourceMappingURL=Factory_ClassWithoutName.ts.map` + //# sourceMappingURL=Factory_ClassWithoutName.js.map` > Snapshot 2 @@ -281,7 +281,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - '//# sourceMappingURL=Factory_Empty.ts.map' + '//# sourceMappingURL=Factory_Empty.js.map' > Snapshot 2 @@ -300,7 +300,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - '//# sourceMappingURL=Factory_Empty_2.ts.map' + '//# sourceMappingURL=Factory_Empty_2.js.map' > Snapshot 2 @@ -350,7 +350,7 @@ Generated by [AVA](https://avajs.dev). console.log(value);␊ }␊ }␊ - //# sourceMappingURL=Factory_ExtendCall.ts.map` + //# sourceMappingURL=Factory_ExtendCall.js.map` > Snapshot 2 @@ -374,7 +374,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_FunctionVarDeclaration.ts.map` + //# sourceMappingURL=Factory_FunctionVarDeclaration.js.map` > Snapshot 2 @@ -398,7 +398,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_FunctionVarExpression.ts.map` + //# sourceMappingURL=Factory_FunctionVarExpression.js.map` > Snapshot 2 @@ -421,7 +421,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/UIComponent";␊ import "sap/ui/core/routing/History";␊ export default window.MyFactory;␊ - //# sourceMappingURL=Factory_GlobalVar.ts.map` + //# sourceMappingURL=Factory_GlobalVar.js.map` > Snapshot 2 @@ -449,7 +449,7 @@ Generated by [AVA](https://avajs.dev). }␊ return merge;␊ }(merge));␊ - //# sourceMappingURL=Factory_MultipleReturns.ts.map` + //# sourceMappingURL=Factory_MultipleReturns.js.map` > Snapshot 2 @@ -468,7 +468,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - '//# sourceMappingURL=Factory_NotProvided_A.ts.map' + '//# sourceMappingURL=Factory_NotProvided_A.js.map' > Snapshot 2 @@ -488,7 +488,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 `import "sap/ui/core/UIComponent";␊ - //# sourceMappingURL=Factory_NotProvided_B.ts.map` + //# sourceMappingURL=Factory_NotProvided_B.js.map` > Snapshot 2 @@ -509,7 +509,7 @@ Generated by [AVA](https://avajs.dev). `import merge from "sap/base/util/merge";␊ export default merge({}, {});␊ - //# sourceMappingURL=Factory_ReturnCallExp.ts.map` + //# sourceMappingURL=Factory_ReturnCallExp.js.map` > Snapshot 2 @@ -533,7 +533,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_cond_iife.ts.map` + //# sourceMappingURL=Factory_cond_iife.js.map` > Snapshot 2 @@ -557,7 +557,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_cond_iife_2.ts.map` + //# sourceMappingURL=Factory_cond_iife_2.js.map` > Snapshot 2 @@ -581,7 +581,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_hoisted_function.ts.map` + //# sourceMappingURL=Factory_hoisted_function.js.map` > Snapshot 2 @@ -605,7 +605,7 @@ Generated by [AVA](https://avajs.dev). import "sap/ui/core/routing/History";␊ export default class MyController extends Controller {␊ }␊ - //# sourceMappingURL=Factory_iife.ts.map` + //# sourceMappingURL=Factory_iife.js.map` > Snapshot 2 @@ -627,7 +627,7 @@ Generated by [AVA](https://avajs.dev). `import jQuery from "sap/ui/thirdparty/jquery";␊ import base from "./base";␊ export default jQuery.extend(base, { foo: "bar" });␊ - //# sourceMappingURL=Factory_jQueryExtend.ts.map` + //# sourceMappingURL=Factory_jQueryExtend.js.map` > Snapshot 2 @@ -649,7 +649,7 @@ Generated by [AVA](https://avajs.dev). `export default function () {␊ return "MyModuleContent";␊ }␊ - //# sourceMappingURL=ModuleName.ts.map` + //# sourceMappingURL=ModuleName.js.map` > Snapshot 2 @@ -674,7 +674,7 @@ Generated by [AVA](https://avajs.dev). return "MyModuleContent";␊ };␊ });␊ - //# sourceMappingURL=ModuleName_Dynamic.ts.map` + //# sourceMappingURL=ModuleName_Dynamic.js.map` > Snapshot 2 @@ -696,7 +696,7 @@ Generated by [AVA](https://avajs.dev). `export default function () {␊ return "MyModuleContent";␊ }␊ - //# sourceMappingURL=ModuleName_FactoryArrowFunction.ts.map` + //# sourceMappingURL=ModuleName_FactoryArrowFunction.js.map` > Snapshot 2 @@ -720,7 +720,7 @@ Generated by [AVA](https://avajs.dev). .then(() => { console.log('success'); })␊ .catch(() => { console.log('error'); });␊ };␊ - //# sourceMappingURL=Noop_DynamicImport.ts.map` + //# sourceMappingURL=Noop_DynamicImport.js.map` > Snapshot 2 @@ -739,7 +739,7 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - '//# sourceMappingURL=Noop_EmptyDefine.ts.map' + '//# sourceMappingURL=Noop_EmptyDefine.js.map' > Snapshot 2 @@ -760,7 +760,7 @@ Generated by [AVA](https://avajs.dev). `// This file contains "sap.ui.define", but no real sap.ui.define call␊ sap.ui.define;␊ - //# sourceMappingURL=Noop_NoDefine.ts.map` + //# sourceMappingURL=Noop_NoDefine.js.map` > Snapshot 2 @@ -797,7 +797,7 @@ Generated by [AVA](https://avajs.dev). console.log(require("./NotLoaded")); // throws an error␊ }␊ }).placeAt("content");␊ - //# sourceMappingURL=Require_AMD.ts.map` + //# sourceMappingURL=Require_AMD.js.map` > Snapshot 2 @@ -830,7 +830,7 @@ Generated by [AVA](https://avajs.dev). console.log(\`Not implemented\`);␊ }␊ };␊ - //# sourceMappingURL=Require_Callback.ts.map` + //# sourceMappingURL=Require_Callback.js.map` > Snapshot 2 @@ -1028,7 +1028,7 @@ Generated by [AVA](https://avajs.dev). return Module;␊ });␊ };␊ - //# sourceMappingURL=Require_Callback_2.ts.map` + //# sourceMappingURL=Require_Callback_2.js.map` > Snapshot 2 @@ -1054,7 +1054,7 @@ Generated by [AVA](https://avajs.dev). function extracted_require_errback_1(err) {␊ console.log(err);␊ }␊ - //# sourceMappingURL=Require_Errback.ts.map` + //# sourceMappingURL=Require_Errback.js.map` > Snapshot 2 @@ -1075,7 +1075,7 @@ Generated by [AVA](https://avajs.dev). `import "my/module";␊ import "my/other/module";␊ - //# sourceMappingURL=Require_Import.ts.map` + //# sourceMappingURL=Require_Import.js.map` > Snapshot 2 @@ -1174,7 +1174,7 @@ Generated by [AVA](https://avajs.dev). var specialRequire = function () {␊ sap.ui.require(param);␊ };␊ - //# sourceMappingURL=Require_Import_2.ts.map` + //# sourceMappingURL=Require_Import_2.js.map` > Snapshot 2 @@ -1204,7 +1204,7 @@ Generated by [AVA](https://avajs.dev). }␊ }␊ };␊ - //# sourceMappingURL=Require_Probing.ts.map` + //# sourceMappingURL=Require_Probing.js.map` > Snapshot 2 @@ -1330,7 +1330,7 @@ Generated by [AVA](https://avajs.dev). var specialRequire = function (param = my_module_36) {␊ sap.ui.require(param);␊ };␊ - //# sourceMappingURL=Require_Probing_2.ts.map` + //# sourceMappingURL=Require_Probing_2.js.map` > Snapshot 2 @@ -1619,7 +1619,7 @@ Generated by [AVA](https://avajs.dev). }␊ };␊ export default Utils;␊ - //# sourceMappingURL=sapUiTestGenericUtils.ts.map` + //# sourceMappingURL=sapUiTestGenericUtils.js.map` > Snapshot 2 diff --git a/test/lib/linter/js/snapshots/transpiler.ts.snap b/test/lib/linter/js/snapshots/transpiler.ts.snap new file mode 100644 index 000000000..ec070bbd1 Binary files /dev/null and b/test/lib/linter/js/snapshots/transpiler.ts.snap differ diff --git a/test/lib/detectors/transpilers/amd/transpiler.ts b/test/lib/linter/js/transpiler.ts similarity index 87% rename from test/lib/detectors/transpilers/amd/transpiler.ts rename to test/lib/linter/js/transpiler.ts index 4735707a1..ee8ee5a84 100644 --- a/test/lib/detectors/transpilers/amd/transpiler.ts +++ b/test/lib/linter/js/transpiler.ts @@ -7,7 +7,7 @@ import {fileURLToPath} from "node:url"; import {createTestsForFixtures} from "./_helper.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const fixtures = path.join(__dirname, "..", "..", "..", "..", "fixtures", "transpiler", "amd"); +const fixtures = path.join(__dirname, "..", "..", "..", "fixtures", "transpiler", "amd"); const test = anyTest as TestFn<{ sinon: sinonGlobal.SinonSandbox; diff --git a/test/lib/linter/linter.ts b/test/lib/linter/linter.ts index 0137f1929..5d4da9cf3 100644 --- a/test/lib/linter/linter.ts +++ b/test/lib/linter/linter.ts @@ -6,8 +6,7 @@ import { createTestsForFixtures, assertExpectedLintResults, esmockDeprecationText, preprocessLintResultsForSnapshot, } from "./_linterHelper.js"; -import {LintResult} from "../../../src/detectors/AbstractDetector.js"; -import {LinterOptions} from "../../../src/linter/linter.js"; +import {LinterOptions, LintResult} from "../../../src/linter/LinterContext.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesBasePath = path.join(__dirname, "..", "..", "fixtures", "linter"); @@ -39,9 +38,9 @@ test.serial("lint: All files of com.ui5.troublesome.app", async (t) => { const res = await lintProject({ rootDir: projectPath, - filePaths: [], + pathsToLint: [], reportCoverage: true, - messageDetails: true, + includeMessageDetails: true, }); t.snapshot(preprocessLintResultsForSnapshot(res)); @@ -58,7 +57,7 @@ test.serial("lint: Some files of com.ui5.troublesome.app (without details / cove const res = await lintProject({ rootDir: projectPath, - filePaths, + pathsToLint: filePaths, }); assertExpectedLintResults(t, res, projectPath, [ @@ -75,9 +74,9 @@ test.serial("lint: All files of library.with.custom.paths", async (t) => { const res = await lintProject({ rootDir: projectPath, - filePaths: [], + pathsToLint: [], reportCoverage: true, - messageDetails: true, + includeMessageDetails: true, }); t.snapshot(preprocessLintResultsForSnapshot(res)); @@ -89,9 +88,9 @@ test.serial("lint: All files of library with sap.f namespace", async (t) => { let res = await lintProject({ rootDir: projectPath, - filePaths: [], + pathsToLint: [], reportCoverage: true, - messageDetails: true, + includeMessageDetails: true, }); res = res.sort((a: {filePath: string}, b: {filePath: string}) => { diff --git a/test/lib/linter/rules/snapshots/CSPCompliance.ts.md b/test/lib/linter/rules/snapshots/CSPCompliance.ts.md index 00977a276..14c91c621 100644 --- a/test/lib/linter/rules/snapshots/CSPCompliance.ts.md +++ b/test/lib/linter/rules/snapshots/CSPCompliance.ts.md @@ -78,13 +78,4 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - [ - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'NoInlineJS_negative.html', - messages: [], - warningCount: 0, - }, - ] + [] diff --git a/test/lib/linter/rules/snapshots/CSPCompliance.ts.snap b/test/lib/linter/rules/snapshots/CSPCompliance.ts.snap index 08c4b12a1..c5a1b6880 100644 Binary files a/test/lib/linter/rules/snapshots/CSPCompliance.ts.snap and b/test/lib/linter/rules/snapshots/CSPCompliance.ts.snap differ diff --git a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md index 62bfd2e23..db9f30fc8 100644 --- a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md +++ b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md @@ -1140,31 +1140,13 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 - [ - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'manifest_negative.json', - messages: [], - warningCount: 0, - }, - ] + [] ## General: manifest_negative_empty.json > Snapshot 1 - [ - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'manifest_negative_empty.json', - messages: [], - warningCount: 0, - }, - ] + [] ## General: sap.ui.jsview.js diff --git a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap index 11120fef8..bcb2e27af 100644 Binary files a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap and b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap differ diff --git a/test/lib/linter/snapshots/linter.ts.md b/test/lib/linter/snapshots/linter.ts.md index b3a17d44e..ab8fd7ad9 100644 --- a/test/lib/linter/snapshots/linter.ts.md +++ b/test/lib/linter/snapshots/linter.ts.md @@ -363,22 +363,6 @@ Generated by [AVA](https://avajs.dev). messages: [], warningCount: 0, }, - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'webapp/index-cdn.html', - messages: [], - warningCount: 0, - }, - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'webapp/index.html', - messages: [], - warningCount: 0, - }, { coverageInfo: [], errorCount: 5, @@ -577,14 +561,6 @@ Generated by [AVA](https://avajs.dev). messages: [], warningCount: 0, }, - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'webapp/test/integration/opaTests.qunit.html', - messages: [], - warningCount: 0, - }, { coverageInfo: [], errorCount: 2, @@ -671,14 +647,6 @@ Generated by [AVA](https://avajs.dev). messages: [], warningCount: 0, }, - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'webapp/test/testsuite.qunit.html', - messages: [], - warningCount: 0, - }, { coverageInfo: [ { @@ -708,14 +676,6 @@ Generated by [AVA](https://avajs.dev). messages: [], warningCount: 0, }, - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'webapp/test/unit/unitTests.qunit.html', - messages: [], - warningCount: 0, - }, { coverageInfo: [], errorCount: 2, @@ -785,14 +745,6 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 [ - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'webapp/Component.js', - messages: [], - warningCount: 0, - }, { coverageInfo: [], errorCount: 1, @@ -983,14 +935,6 @@ Generated by [AVA](https://avajs.dev). ], warningCount: 0, }, - { - coverageInfo: [], - errorCount: 0, - fatalErrorCount: 0, - filePath: 'src/test/js/Example.html', - messages: [], - warningCount: 0, - }, { coverageInfo: [ { diff --git a/test/lib/linter/snapshots/linter.ts.snap b/test/lib/linter/snapshots/linter.ts.snap index d779a50b4..4dafd76ff 100644 Binary files a/test/lib/linter/snapshots/linter.ts.snap and b/test/lib/linter/snapshots/linter.ts.snap differ diff --git a/test/lib/detectors/transpilers/xml/_helper.ts b/test/lib/linter/xml/_helper.ts similarity index 81% rename from test/lib/detectors/transpilers/xml/_helper.ts rename to test/lib/linter/xml/_helper.ts index 79fb92cc4..8eef11747 100644 --- a/test/lib/detectors/transpilers/xml/_helper.ts +++ b/test/lib/linter/xml/_helper.ts @@ -3,7 +3,8 @@ import sinonGlobal from "sinon"; import path from "node:path"; import util from "util"; import fs from "node:fs"; -import {xmlToJs} from "../../../../../src/detectors/transpilers/xml/transpiler.js"; +import LinterContext from "../../../../src/linter/LinterContext.js"; +import transpileXml from "../../../../src/linter/xmlTemplate/transpiler.js"; util.inspect.defaultOptions.depth = 4; // Increase AVA's printing depth since coverageInfo objects are on level 4 @@ -39,10 +40,13 @@ export function createTestsForFixtures(fixturesPath: string) { defineTest(`Transpile ${testName}`, async (t) => { const filePath = path.join(fixturesPath, fileName); const fileStream = fs.createReadStream(filePath); - const {source, map, messages} = await xmlToJs(testName, fileStream); + const context = new LinterContext({ + rootDir: fixturesPath, + }); + const {source, map} = await transpileXml(testName, fileStream, context); t.snapshot(source); t.snapshot(map && JSON.parse(map)); - t.snapshot(messages); + t.snapshot(context.getLintingMessages(testName)); }); } } catch (err) { diff --git a/test/lib/detectors/transpilers/xml/snapshots/transpiler.ts.md b/test/lib/linter/xml/snapshots/transpiler.ts.md similarity index 99% rename from test/lib/detectors/transpilers/xml/snapshots/transpiler.ts.md rename to test/lib/linter/xml/snapshots/transpiler.ts.md index 4bd552b74..614d94eaa 100644 --- a/test/lib/detectors/transpilers/xml/snapshots/transpiler.ts.md +++ b/test/lib/linter/xml/snapshots/transpiler.ts.md @@ -1,4 +1,4 @@ -# Snapshot report for `test/lib/detectors/transpilers/xml/transpiler.ts` +# Snapshot report for `test/lib/linter/xml/transpiler.ts` The actual snapshot is saved in `transpiler.ts.snap`. diff --git a/test/lib/linter/xml/snapshots/transpiler.ts.snap b/test/lib/linter/xml/snapshots/transpiler.ts.snap new file mode 100644 index 000000000..33cb3f6fc Binary files /dev/null and b/test/lib/linter/xml/snapshots/transpiler.ts.snap differ diff --git a/test/lib/detectors/transpilers/xml/transpiler.ts b/test/lib/linter/xml/transpiler.ts similarity index 69% rename from test/lib/detectors/transpilers/xml/transpiler.ts rename to test/lib/linter/xml/transpiler.ts index dc4e6389e..a42076546 100644 --- a/test/lib/detectors/transpilers/xml/transpiler.ts +++ b/test/lib/linter/xml/transpiler.ts @@ -3,6 +3,6 @@ import {fileURLToPath} from "node:url"; import {createTestsForFixtures} from "./_helper.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const fixtures = path.join(__dirname, "..", "..", "..", "..", "fixtures", "transpiler", "xml"); +const fixtures = path.join(__dirname, "..", "..", "..", "fixtures", "transpiler", "xml"); createTestsForFixtures(fixtures);