UNPKG

@ui5/linter

Version:

A static code analysis tool for UI5

298 lines 12.9 kB
import ts from "typescript"; import MagicString from "magic-string"; import { getLogger } from "@ui5/logger"; import { MESSAGE } from "../linter/messages.js"; import generateChangesJs from "./generateChangesJs.js"; import generateChangesXml from "./generateChangesXml.js"; import { getFactoryBody } from "./amdImports.js"; const log = getLogger("linter:autofix"); export var ChangeAction; (function (ChangeAction) { ChangeAction["INSERT"] = "insert"; ChangeAction["REPLACE"] = "replace"; ChangeAction["DELETE"] = "delete"; })(ChangeAction || (ChangeAction = {})); function createCompilerHost(sourceFiles) { return { getSourceFile: (fileName) => sourceFiles.get(fileName), writeFile: () => undefined, getDefaultLibFileName: () => "lib.d.ts", useCaseSensitiveFileNames: () => false, getCanonicalFileName: (fileName) => fileName, getCurrentDirectory: () => "", getNewLine: () => "\n", fileExists: (fileName) => sourceFiles.has(fileName), readFile: () => "", directoryExists: () => true, getDirectories: () => [], }; } const compilerOptions = { checkJs: false, allowJs: true, skipLibCheck: true, noCheck: true, target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022, isolatedModules: true, sourceMap: true, suppressOutputPathCheck: true, noLib: true, noResolve: true, allowNonTsExtensions: true, }; function createProgram(inputFileNames, host) { return ts.createProgram(inputFileNames, compilerOptions, host); } function createSourceFile(resourcePath, code) { return ts.createSourceFile(resourcePath, code, { languageVersion: ts.ScriptTarget.ES2022, jsDocParsingMode: ts.JSDocParsingMode.ParseNone, }); } function getJsErrorsForSourceFile(sourceFile, program, host) { const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile); return diagnostics.filter(function (d) { return d.file === sourceFile && d.category === ts.DiagnosticCategory.Error; }).map((d) => { let codeSnippet; let pos; if (d.start !== undefined && d.length !== undefined) { codeSnippet = sourceFile.text.substring(d.start, d.start + d.length); pos = ts.getLineAndCharacterOfPosition(sourceFile, d.start); } return { messageText: ts.flattenDiagnosticMessageText(d.messageText, host.getNewLine()), start: d.start, length: d.length, line: pos?.line, character: pos?.character, file: d.file, codeSnippet, }; }); } function getJsErrors(code, resourcePath) { const sourceFile = createSourceFile(resourcePath, code); const host = createCompilerHost(new Map([[resourcePath, sourceFile]])); const program = createProgram([resourcePath], host); return getJsErrorsForSourceFile(sourceFile, program, host); } async function getXmlErrorForFile(content) { const { XMLValidator } = await import("fast-xml-parser"); const validOrError = XMLValidator.validate(content); if (validOrError === true) { return; // No errors } return validOrError.err; } export function getFactoryPosition(moduleDeclaration) { const factory = getFactoryBody(moduleDeclaration); if (!factory) { throw new Error("Module declaration does not have a factory or callback defined"); } return { start: factory.getStart(), end: factory.getEnd(), }; } export default async function ({ rootDir: _unused1, namespace: _unused2, resources: autofixResources, context, }) { const messages = new Map(); const xmlResources = []; const jsResources = []; for (const [_, autofixResource] of autofixResources) { const fixMessages = autofixResource.messages.filter((msg) => msg.fix); if (!fixMessages.length) { // No fixes available for this resource continue; } messages.set(autofixResource.resource.getPath(), fixMessages); if (autofixResource.resource.getPath().endsWith(".xml")) { xmlResources.push(autofixResource.resource); } else { jsResources.push(autofixResource.resource); } } const res = new Map(); if (jsResources.length) { log.verbose(`Applying autofixes for ${jsResources.length} JS resources`); await autofixJs(jsResources, messages, context, res); } if (xmlResources.length) { log.verbose(`Applying autofixes for ${xmlResources.length} XML resources`); await autofixXml(xmlResources, messages, context, res); } return res; } async function autofixJs(jsResources, messages, context, res) { const sourceFiles = new Map(); const jsResourcePaths = []; for (const resource of jsResources) { const resourcePath = resource.getPath(); const sourceFile = ts.createSourceFile(resource.getPath(), await resource.getString(), { languageVersion: ts.ScriptTarget.ES2022, jsDocParsingMode: ts.JSDocParsingMode.ParseNone, }); sourceFiles.set(resourcePath, sourceFile); jsResourcePaths.push(resourcePath); } const compilerHost = createCompilerHost(sourceFiles); const program = createProgram(jsResourcePaths, compilerHost); const checker = program.getTypeChecker(); for (const [resourcePath, sourceFile] of sourceFiles) { // Checking for syntax errors in the original source file. // We should not apply autofixes to files with syntax errors const existingJsErrors = getJsErrorsForSourceFile(sourceFile, program, compilerHost); if (existingJsErrors.length) { log.verbose(`Skipping autofix for '${resourcePath}'. Syntax error(s) in original source file:\n - ` + `${existingJsErrors.map((d) => { let res = d.messageText; if (d.codeSnippet) { res += ` (\`${d.codeSnippet}\`)`; } return res; }).join("\n - ")}`); continue; } log.verbose(`Applying autofix for ${resourcePath}`); let newContent; try { newContent = applyFixesJs(checker, sourceFile, resourcePath, messages.get(resourcePath)); } catch (err) { if (err instanceof Error) { log.verbose(`Error while applying autofix to ${resourcePath}: ${err}`); if (err instanceof Error) { log.verbose(`Call stack: ${err.stack}`); } context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, { message: err.message }); continue; } throw err; } if (newContent) { const jsErrors = getJsErrors(newContent, resourcePath); if (jsErrors.length) { const contentWithMarkers = newContent.split("\n"); let lineOffset = 0; const message = `Syntax error after applying autofix for '${resourcePath}'. ` + `This is likely a UI5 linter internal issue. Please check the verbose log. ` + `Please report this using the bug report template: ` + `https://github.com/UI5/linter/issues/new?template=bug-report.md`; const errors = jsErrors .sort((a, b) => { if (a.start === undefined || b.start === undefined) { return 0; } return a.start - b.start; }).map((d) => { let res = d.messageText; if (d.line !== undefined && d.character !== undefined) { // Prepend line and character res = `${d.line + 1}:${d.character + 1} ${res}`; // Fill contentWithMarkers array for debug logging // Insert line below the finding and mark the character const lineContent = contentWithMarkers[d.line + lineOffset]; // Count number of tabs until the character const tabCount = lineContent.slice(0, d.character).split("\t").length - 1; const leadingTabs = "\t".repeat(tabCount); const markerLine = d.line + lineOffset + 1; contentWithMarkers.splice(markerLine, 0, leadingTabs + " ".repeat(d.character - tabCount) + "^"); lineOffset++; } if (d.codeSnippet) { res += ` (\`${d.codeSnippet}\`)`; } return res; }).join("\n - "); log.verbose(message); log.verbose(errors); log.verbose(resourcePath + ":\n" + contentWithMarkers.join("\n")); context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, { message }); } else { log.verbose(`Autofix applied to ${resourcePath}`); res.set(resourcePath, newContent); } } } } async function autofixXml(xmlResources, messages, context, res) { for (const resource of xmlResources) { const resourcePath = resource.getPath(); const existingXmlError = await getXmlErrorForFile(await resource.getString()); if (existingXmlError) { log.verbose(`Skipping autofix for '${resourcePath}'. Syntax error reported in original source file:\n` + `[${existingXmlError.line}:${existingXmlError.col}] ${existingXmlError.msg}`); continue; } const newContent = await applyFixesXml(resource, messages.get(resourcePath)); if (!newContent) { continue; } const newXmlError = await getXmlErrorForFile(newContent); if (newXmlError) { const message = `Syntax error after applying autofix for '${resourcePath}'. ` + `This is likely a UI5 linter internal issue. Please check the verbose log. ` + `Please report this using the bug report template: ` + `https://github.com/UI5/linter/issues/new?template=bug-report.md`; const error = `Reported error (${newXmlError.line}:${newXmlError.col}): ${newXmlError.msg}`; log.verbose(message); log.verbose(error); const contentWithMarkers = newContent.split("\n"); if (newXmlError.line !== undefined && newXmlError.col !== undefined) { const line = newXmlError.line - 1; // Insert line below the finding and mark the character const lineContent = contentWithMarkers[line]; // Count number of tabs until the character const tabCount = lineContent.slice(0, newXmlError.col).split("\t").length - 1; const leadingTabs = "\t".repeat(tabCount); const markerLine = line + 1; contentWithMarkers.splice(markerLine, 0, leadingTabs + " ".repeat(newXmlError.col - tabCount) + "^"); } log.verbose(resourcePath + ":\n" + contentWithMarkers.join("\n")); context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, { message }); continue; } res.set(resourcePath, newContent); } } function applyFixesJs(checker, sourceFile, resourcePath, messages) { const content = sourceFile.getFullText(); const changeSet = []; generateChangesJs(resourcePath, checker, sourceFile, content, messages, changeSet); if (changeSet.length === 0) { // No modifications needed return; } return applyChanges(content, changeSet); } async function applyFixesXml(resource, messages) { const content = await resource.getString(); const changeSet = []; await generateChangesXml(messages, changeSet, content, resource); if (changeSet.length === 0) { return undefined; } return applyChanges(content, changeSet); } function applyChanges(content, changeSet) { changeSet.sort((a, b) => b.start - a.start); const s = new MagicString(content); for (const change of changeSet) { switch (change.action) { case ChangeAction.INSERT: s.appendRight(change.start, change.value); break; case ChangeAction.REPLACE: s.update(change.start, change.end, change.value); break; case ChangeAction.DELETE: s.remove(change.start, change.end); break; } } return s.toString(); } //# sourceMappingURL=autofix.js.map