@ui5/linter
Version:
A static code analysis tool for UI5
298 lines • 12.9 kB
JavaScript
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