UNPKG

@ui5/linter

Version:

A static code analysis tool for UI5

466 lines 22.3 kB
import ts from "typescript"; import { getFactoryPosition, } from "./autofix.js"; import { collectIdentifierDeclarations, collectIdentifiers, matchPropertyAccessExpression, removeConflictingFixes, } from "./utils.js"; import parseModuleDeclaration from "../linter/ui5Types/amdTranspiler/parseModuleDeclaration.js"; import parseRequire from "../linter/ui5Types/amdTranspiler/parseRequire.js"; import { getLogger } from "@ui5/logger"; import { NO_PARAM_FOR_DEPENDENCY, addDependencies, getDependencies, removeDependencies, hasBody, getFactoryBody, } from "./amdImports.js"; import { resolveUniqueName } from "../linter/ui5Types/utils/utils.js"; const log = getLogger("linter:autofix:generateChangesJs"); export default function generateChanges(resourcePath, checker, sourceFile, content, messages, changeSets) { const nodeSearchInfo = new Set(); // Collect all fixes from the messages for (const { fix } of messages) { if (!fix) { continue; } // Map "position for search" (calculated from the transpiled AST using the source map) to the absolute // position in the source file const { position: fixStart, nodeTypes } = fix.getNodeSearchParameters(); // TypeScript lines and columns are 0-based const line = fixStart.line - 1; const column = fixStart.column - 1; const pos = sourceFile.getPositionOfLineAndCharacter(line, column); nodeSearchInfo.add({ fix, nodeTypes, position: { line, column, pos, }, }); } const moduleDeclarations = new Map(); const matchedFixes = new Set(); function visitNode(node) { for (const nodeInfo of nodeSearchInfo) { // For each fix, search for a match for requested position and node type until it acquires a context // (meaning that it found its node in the source file) if (node.getStart() === nodeInfo.position.pos && nodeInfo.nodeTypes.includes(node.kind) && nodeInfo.fix.visitAutofixNode(node, nodeInfo.position.pos, sourceFile)) { // A fix has found its node in the autofix AST // Add it to the set of matched fixes, remove it from the search info // and abort searching for further fixes for this node matchedFixes.add(nodeInfo.fix); nodeSearchInfo.delete(nodeInfo); } } // Also collect all module declarations (define and require calls) if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { if (matchPropertyAccessExpression(node.expression, "sap.ui.define")) { try { moduleDeclarations.set(node, { moduleDeclaration: parseModuleDeclaration(node.arguments, checker), importRequests: new Map(), obsoleteModules: new Set(), }); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); log.verbose(`Failed to parse sap.ui.define ` + `call in ${sourceFile.fileName}: ${errorMessage}`); if (err instanceof Error) { log.verbose(`Call stack: ${err.stack}`); } } } else if (matchPropertyAccessExpression(node.expression, "sap.ui.require")) { try { const requireExpression = parseRequire(node.arguments, checker); // Only handle async require calls, not sap.ui.require probing if (requireExpression.async) { moduleDeclarations.set(node, { moduleDeclaration: requireExpression, importRequests: new Map(), obsoleteModules: new Set(), }); } } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); log.verbose(`Failed to parse sap.ui.require ` + `call in ${sourceFile.fileName}: ${errorMessage}`); if (err instanceof Error) { log.verbose(`Call stack: ${err.stack}`); } } } } ts.forEachChild(node, visitNode); } ts.forEachChild(sourceFile, visitNode); removeConflictingFixes(matchedFixes); const dependencyRequests = new Set(); const blockedModuleImports = new Set(); const globalAccessRequests = new Set(); const obsoleteModuleImports = new Set(); for (const fix of matchedFixes) { // Collect new dependencies const newDependencies = fix.getNewModuleDependencies?.(); if (Array.isArray(newDependencies)) { for (const depRequest of newDependencies) { dependencyRequests.add({ ...depRequest, fix, }); if (depRequest.blockNewImport) { // If the request blocks new imports, add it to the blocked module imports blockedModuleImports.add(depRequest.moduleName); } } } else if (newDependencies) { const depRequest = newDependencies; dependencyRequests.add({ ...depRequest, fix, }); if (depRequest.blockNewImport) { // If the request blocks new imports, add it to the blocked module imports blockedModuleImports.add(depRequest.moduleName); } } // Collect new global access const newGlobalAccess = fix.getNewGlobalAccess?.(); if (Array.isArray(newGlobalAccess)) { for (const globalAccess of newGlobalAccess) { globalAccessRequests.add({ ...globalAccess, fix, }); } } else if (newGlobalAccess) { const globalAccess = newGlobalAccess; globalAccessRequests.add({ ...globalAccess, fix, }); } // Collect obsolete modules const obsoleteModules = fix.getObsoleteModuleDependencies?.(); if (Array.isArray(obsoleteModules)) { for (const obsoleteModule of obsoleteModules) { // Add the module to the blocked module imports, so it won't be added again obsoleteModuleImports.add(obsoleteModule); } } else if (obsoleteModules) { obsoleteModuleImports.add(obsoleteModules); } } const dependencyDeclarations = []; for (const [_, moduleDeclarationInfo] of moduleDeclarations) { if (!hasBody(moduleDeclarationInfo.moduleDeclaration)) { // Ignore module declaration without factory or callback functions // This is to safeguard the following methods, which all rely on a factory body continue; } const deps = getDependencies(moduleDeclarationInfo.moduleDeclaration, resourcePath); const { start, end } = getFactoryPosition(moduleDeclarationInfo); dependencyDeclarations.push({ moduleDeclarationInfo, start, end, dependencies: deps, }); const potentiallyObsoleteDeps = new Map(); for (const obsoleteDep of obsoleteModuleImports) { if (start <= obsoleteDep.usagePosition && end >= obsoleteDep.usagePosition && deps.has(obsoleteDep.moduleName)) { const identifierName = deps.get(obsoleteDep.moduleName); if (typeof identifierName !== "string") { // If the dependency is declared, but the parameter is missing, it is likely a side-effect import // that should be preserved // If the parameter is "unsupported" we also need to preserve the dependency continue; } if (!potentiallyObsoleteDeps.has(obsoleteDep.moduleName)) { potentiallyObsoleteDeps.set(obsoleteDep.moduleName, { identifierName, usagePositions: [], }); } potentiallyObsoleteDeps.get(obsoleteDep.moduleName).usagePositions.push(obsoleteDep.usagePosition); } } if (!potentiallyObsoleteDeps.size) { continue; } const body = getFactoryBody(moduleDeclarationInfo); if (!body) { continue; } const identifiersInScope = collectIdentifiers(body); // Check for unused module dependencies for (const [moduleName, { identifierName, usagePositions }] of potentiallyObsoleteDeps) { // Check whether there is any identifier matching the module import one that is not // at the same position as one of the fixes that marked it as obsolete (ignore those) let isStillUsed = false; for (const identifier of identifiersInScope) { if (identifier.text === identifierName && !usagePositions.includes(identifier.getStart())) { // Found another identifier, indicating further usage. Do not mark the module as obsolete isStillUsed = true; break; } } if (!isStillUsed) { // Module can be removed moduleDeclarationInfo.obsoleteModules.add(moduleName); } } } // Sort declarations by start position of the factory/callback dependencyDeclarations.sort((a, b) => { if (a.start !== b.start) { return a.start - b.start; } // If the start positions are the same, sort by end position return a.end - b.end; }); // Handle blocked module imports if (dependencyDeclarations.length) { for (const topLevelDep of dependencyDeclarations[0].dependencies.keys()) { if (blockedModuleImports.has(topLevelDep)) { // If the blocked module is already imported in the top-level module declaration, // lift the block blockedModuleImports.delete(topLevelDep); } } } for (const depRequest of dependencyRequests) { if (blockedModuleImports.has(depRequest.moduleName)) { // If the request is for a module that is blocked from being imported by another fix // (e.g. because a probing/conditional access has been detected), delete the request // this will cause the fix to not be applied dependencyRequests.delete(depRequest); } } // Collect all identifiers in the source file to ensure unique names when adding imports const identifiers = collectIdentifierDeclarations(sourceFile); // Sort dependency requests into declarations mergeDependencyRequests(dependencyRequests, dependencyDeclarations, identifiers); processGlobalRequests(globalAccessRequests, identifiers); // Create changes for new and removed dependencies for (const [defineCall, moduleDeclarationInfo] of moduleDeclarations) { const moduleRemovals = new Set([ "sap/base/strings/NormalizePolyfill", "jquery.sap.unicode", ...moduleDeclarationInfo.obsoleteModules ]); for (const dependencyModuleName of moduleDeclarationInfo.importRequests.keys()) { if (moduleRemovals.has(dependencyModuleName)) { moduleRemovals.delete(dependencyModuleName); } } // Remove dependencies from the existing module declaration removeDependencies(moduleRemovals, moduleDeclarationInfo, changeSets, resourcePath, identifiers); // Resolve dependencies for the module declaration addDependencies(defineCall, moduleDeclarationInfo, changeSets, resourcePath, moduleRemovals); } for (const fix of matchedFixes) { const changes = fix.generateChanges?.(); if (Array.isArray(changes)) { for (const change of changes) { changeSets.push(change); } } else if (changes) { changeSets.push(changes); } } } function mergeDependencyRequests(dependencyRequests, dependencyDeclarations, identifiers) { // Step 1.) Try to fulfill dependency requests using the existing dependency declarations for (const dependencyRequest of dependencyRequests) { const { moduleName, usagePosition: position, fix } = dependencyRequest; if (!fix.setIdentifierForDependency) { throw new Error(`Fix ${fix.constructor.name} requested dependencies but ` + `does not implement setIdentifierForDependency method`); } // Dependency declarations are sorted in order of appearance in the source file // Start the search from the end to visit the most specific ones first for (let i = dependencyDeclarations.length - 1; i >= 0; i--) { const decl = dependencyDeclarations[i]; const depIdentifier = decl.dependencies.get(moduleName); if (depIdentifier && typeof depIdentifier === "string" && decl.start <= position && decl.end >= position) { // Dependency request can be fulfilled directly fix.setIdentifierForDependency(depIdentifier, moduleName); dependencyRequests.delete(dependencyRequest); // Request fulfilled break; } else if (depIdentifier === NO_PARAM_FOR_DEPENDENCY) { // A dependency is declared, but the parameter is missing // First check whether another request for this module name has already been fulfilled if (decl.moduleDeclarationInfo.importRequests.has(moduleName)) { // If so, use the existing identifier const { identifier } = decl.moduleDeclarationInfo.importRequests.get(moduleName); fix.setIdentifierForDependency(identifier, moduleName); dependencyRequests.delete(dependencyRequest); // Request fulfilled break; } // Create a unique name if the preferred identifier is already in use // Use the first preferred identifier const identifier = resolveUniqueName(moduleName, identifiers); identifiers.add(identifier); decl.moduleDeclarationInfo.importRequests.set(moduleName, { identifier, }); fix.setIdentifierForDependency(identifier, moduleName); dependencyRequests.delete(dependencyRequest); // Request fulfilled break; } } } // Step 2.) Build a list of potential additions to existing dependency declarations // that would fulfill all remaining requests const newDependencyDeclarations = new Map(); for (const dependencyRequest of dependencyRequests) { // Dependency declarations are sorted in order of appearance in the source file // Start the search from the end to visit the most specific ones first for (let i = dependencyDeclarations.length - 1; i >= 0; i--) { const decl = dependencyDeclarations[i]; if (decl.start <= dependencyRequest.usagePosition && decl.end >= dependencyRequest.usagePosition) { if (newDependencyDeclarations.has(decl)) { // Add request to existing declaration newDependencyDeclarations.get(decl).push(dependencyRequest); break; } else { newDependencyDeclarations.set(decl, [dependencyRequest]); } break; } } } const mappedDeclarations = new Set(); function createDependencyDeclarationsNode(decl) { if (mappedDeclarations.has(decl)) { // A node for this declaration has already been created and added to the corresponding parent // This check prevents a declaration from being added to multiple parents if the positions overlap // The correct parent is always the first one that has visited this node. return; } mappedDeclarations.add(decl); const children = []; for (const otherDecl of dependencyDeclarations) { if (decl.start <= otherDecl.start && decl.end >= otherDecl.end) { // decl contains otherDecl (might be direct or indirect child) const child = createDependencyDeclarationsNode(otherDecl); if (!child) { // Indirect child, ignore continue; } children.push(child); } } return { declaration: decl, children, }; } const moduleNameToDeclToRequests = new Map(); // Function to traverse and count moduleNames function assignDependencyRequests(node) { const dependencyModuleNames = []; // Traverse depth-first to collect all dependency requests const depsPerChild = node.children.map((child) => { return assignDependencyRequests(child); }); // Check whether there is a request for this dependency declaration const dependencyRequests = newDependencyDeclarations.get(node.declaration); if (dependencyRequests) { for (const dependencyRequest of dependencyRequests) { const moduleName = dependencyRequest.moduleName; if (moduleNameToDeclToRequests.has(moduleName)) { const existingRequests = moduleNameToDeclToRequests.get(moduleName); existingRequests[1].push(dependencyRequest); } else { moduleNameToDeclToRequests.set(moduleName, [node.declaration, [dependencyRequest]]); } if (!dependencyModuleNames.includes(moduleName)) { dependencyModuleNames.push(moduleName); } } } if (depsPerChild.length > 0) { // Check if multiple children have requests for the same module const moduleNameOccurences = new Map(); for (const moduleName of dependencyModuleNames) { moduleNameOccurences.set(moduleName, 1); } for (const childDeps of depsPerChild) { for (const moduleName of childDeps) { const count = moduleNameOccurences.get(moduleName) ?? 0; moduleNameOccurences.set(moduleName, count + 1); } } for (const [moduleName, count] of moduleNameOccurences) { if (count > 1) { // Multiple children have requests for the same module // Assign all requests to this dependency declarations moduleNameToDeclToRequests.get(moduleName)[0] = node.declaration; } } } return dependencyModuleNames; } // Step 4.) Merge new dependencies into existing dependency declarations // First find all the root dependency declarations and create trees under them const rootDependencyDeclarations = new Set(); for (const decl of dependencyDeclarations) { const node = createDependencyDeclarationsNode(decl); if (node) { rootDependencyDeclarations.add(node); } } // Then assign the dependency requests accordingly for (const node of rootDependencyDeclarations) { assignDependencyRequests(node); } for (const [moduleName, [decl, requests]] of moduleNameToDeclToRequests) { // Get preferred identifier unless it's already in use let identifier; for (const request of requests) { if (request.preferredIdentifier && !identifiers.has(request.preferredIdentifier)) { identifier = request.preferredIdentifier; break; } } // Create a unique name if the preferred identifier is already in use // Use the first preferred identifier identifier ??= resolveUniqueName(moduleName, identifiers); identifiers.add(identifier); decl.moduleDeclarationInfo.importRequests.set(moduleName, { identifier }); for (const request of requests) { if (!request.fix.setIdentifierForDependency) { throw new Error(`Fix ${request.fix.constructor.name} requested dependencies but ` + `does not implement setIdentifierForDependency method`); } // Set the identifier for the fix request.fix.setIdentifierForDependency(identifier, moduleName); } } } function processGlobalRequests(globalAccessRequests, identifiers) { for (const globalAccessRequest of globalAccessRequests) { const { globalName, fix } = globalAccessRequest; if (!fix.setIdentifierForGlobal) { throw new Error(`Fix ${fix.constructor.name} requested global access but ` + `does not implement setIdentifierForGlobal method`); } if (!identifiers.has(globalName)) { // If the global name is not already in use, we can use it directly fix.setIdentifierForGlobal(globalName, globalName); continue; } // If the global name is already in use, prefix it with globalThis const identifier = `globalThis.${globalName}`; identifiers.add(identifier); fix.setIdentifierForGlobal(identifier, globalName); } } //# sourceMappingURL=generateChangesJs.js.map