UNPKG

@ui5/linter

Version:

A static code analysis tool for UI5

411 lines 15.8 kB
import ts from "typescript"; import { getUniqueName } from "./UniqueNameCreator.js"; import { getUi5TypeInfoFromSymbol } from "../Ui5TypeInfo.js"; import Ui5TypeInfoMatcher from "../Ui5TypeInfoMatcher.js"; export function isSourceFileOfTypeScriptLib(sourceFile) { return sourceFile.fileName.startsWith("/types/typescript/lib/"); } export function isSourceFileOfPseudoModuleType(sourceFile) { return sourceFile.fileName.startsWith("/types/@ui5/linter/types/pseudo-modules/"); } /** * Returns the text of a PropertyName node. * This function will not return the source code text of the node, but the text that can be * used to check against when looking for a specific property. * * If the text can't be determined, undefined is returned. * This may happen in the following cases: * - A ComputedPropertyName with a non-string literal-like expression * - A BigIntLiteral, which technically is a PropertyName, but is not valid in this context * * @param node * @returns text of the given node or undefined */ export function getPropertyNameText(node) { if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node) || ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) { return node.text; } else if (ts.isComputedPropertyName(node)) { if (ts.isStringLiteralLike(node.expression) || ts.isNumericLiteral(node.expression)) { return node.expression.text; } } return undefined; } /** * Searches for the symbol of an argument within the given construct signatures. * The first match is returned. * * Returns undefined if the argument is not found in any of the construct signatures. * * @param constructSignatures construct signatures to search in * @param argumentPosition position of the argument in the signature * @returns symbol of the found property or undefined */ export function getSymbolForArgumentInConstructSignatures(constructSignatures, argumentPosition) { for (const constructSignature of constructSignatures) { const propertySymbol = constructSignature.getParameters()[argumentPosition]; if (propertySymbol) { return propertySymbol; } } return undefined; } /** * Searches for the symbol of a property within the given construct signatures. * The first match is returned. * * Returns undefined if the property is not found in any of the construct signatures. * * @param constructSignatures construct signatures to search in * @param argumentPosition position of the signature parameter to search in * @param propertyName property name to search for * @returns symbol of the found property or undefined */ export function getSymbolForPropertyInConstructSignatures(constructSignatures, argumentPosition, propertyName) { for (const constructSignature of constructSignatures) { const propertySymbol = constructSignature .getTypeParameterAtPosition(argumentPosition) .getProperty(propertyName); if (propertySymbol) { return propertySymbol; } } return undefined; } /** * Searches for a class member with the given name. * If no modifiers are passed, any member with the given name is returned. * If modifiers are passed, only members with at least the given modifiers are returned. * * Returns undefined if no matching member is found. * * @param node * @param memberName * @param modifiers * @returns */ export function findClassMember(node, memberName, modifiers = []) { return node.members.find((member) => { if (!member.name) { // Constructor does not have a name return false; } const name = getPropertyNameText(member.name); if (name !== memberName) { return false; } if (modifiers.length === 0) { // No modifiers passed, so we match any member return true; } if (!ts.canHaveModifiers(member)) { // Only match if no modifiers are expected. // This is only the case for constructor declarations, but they are already filtered out above. // Still, this check is here for type checking and in case new node types are added. return modifiers.length === 0; } return modifiers.every(({ modifier, not }) => { const result = member.modifiers?.some(({ kind }) => kind === modifier); return not ? !result : result; }); }); } /** * Checks whether the given class member is a method or function. * This includes the type lookup of a referenced function. * * @param node * @param methodName * @param checker * @returns */ export function isClassMethod(node, checker) { if (ts.isMethodDeclaration(node)) { return true; } if (!ts.isPropertyDeclaration(node) || !node.initializer) { return false; } if (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer)) { return true; } ; if (ts.isIdentifier(node.initializer)) { const symbol = checker.getSymbolAtLocation(node.initializer); if (symbol?.valueDeclaration && ts.isFunctionDeclaration(symbol.valueDeclaration)) { return true; } } return false; } /** * Returns the PropertyAssignment of the given property name in the given ObjectLiteralExpression. * If the property is not found, undefined is returned. * * @param propertyName * @param node * @returns */ export function getPropertyAssignmentInObjectLiteralExpression(propertyName, node) { return getPropertyAssignmentsInObjectLiteralExpression([propertyName], node)[0]; } /** * Returns the PropertyAssignments of the given property names in the given ObjectLiteralExpression. * The order of the returned array matches the order of the propertyNames array and may contain undefined * values for properties that are not found. * * @param propertyNames * @param node * @returns */ export function getPropertyAssignmentsInObjectLiteralExpression(propertyNames, node) { const properties = []; let propertiesFound = 0; for (const property of node.properties) { if (!ts.isPropertyAssignment(property)) { continue; } const name = getPropertyNameText(property.name); for (let i = 0; i < propertyNames.length; i++) { if (name === propertyNames[i] && !properties[i]) { properties[i] = property; propertiesFound++; if (propertiesFound === propertyNames.length) { return properties; } } } } // Fill array with undefined for missing properties for (let i = 0; i < propertyNames.length; i++) { properties[i] ??= undefined; } return properties; } export function resolveUniqueName(inputName, existingIdentifiers) { const parts = inputName.split("/"); const identifier = parts[parts.length - 1]; let name = null; if (identifier === "jquery") { name = "jQuery"; } else if (identifier === "library") { const potentialLibraryName = parts[parts.length - 2]; // Relative imports contain a dot and should not be mistaken for a library name if (!potentialLibraryName.includes(".")) { name = potentialLibraryName + "Library"; } } name = name ?? camelize(identifier); if (existingIdentifiers?.has(name)) { name = getUniqueName(Array.from(existingIdentifiers ?? []), inputName, "/"); } return name; } // Camelize a string by replacing invalid identifier characters function camelize(str) { return str.replace(/[^\p{ID_Start}\p{ID_Continue}]+([\p{ID_Start}\p{ID_Continue}])/gu, (_match, nextChar) => { return typeof nextChar === "string" ? nextChar.toUpperCase() : ""; }); } export function isAssignment(node) { let currentNode = node; while (ts.isPropertyAccessExpression(currentNode) || ts.isElementAccessExpression(currentNode)) { if (!currentNode.parent) { return false; } if (ts.isBinaryExpression(currentNode.parent)) { return currentNode.parent.left === currentNode && currentNode.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; } currentNode = currentNode.parent; } return false; } export function isReturnValueUsed(node) { let isReturnValueUsed = false; while (node && !isReturnValueUsed) { if (!ts.isReturnStatement(node) && !ts.isThrowStatement(node) && node.parent && ts.isBlock(node.parent)) { // If the node is not a return statement, but is part of a block, we can assume that it is not used break; } if (node.parent && ts.isConditionalExpression(node.parent) && node === node.parent.condition) { // If the node is part of a conditional expression, and is the condition, the return value is used // Other positions in the conditional expression depend on how the whole expression is used // Example: `method() ? 1 : 2;` isReturnValueUsed = true; break; } if (node.parent && (ts.isIfStatement(node.parent) || ts.isWhileStatement(node.parent) || ts.isDoStatement(node.parent) || ts.isSwitchStatement(node.parent)) && node !== node.parent.expression) { // If the node is part of an if or while statement, but not the condition we can assume // that the return value is not used // Example: `while(1 === 1) method();` does not use the return value of `method()` break; } if (node.parent && ts.isForStatement(node.parent) && node === node.parent.statement) { // If the node is part of a for statement but part of the statement, we can assume // that the return value is not used // Example: `for (let i = 0; i < 10; i++) method();` does not use the return value of `method()` break; } if (ts.isVariableDeclaration(node) || ts.isBinaryExpression(node) || ts.isIfStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node) || ts.isForStatement(node) || ts.isSwitchStatement(node) || ts.isVariableStatement(node) || ts.isParenthesizedExpression(node) || ts.isReturnStatement(node) || ts.isThrowStatement(node) || // () => jQuery.sap.log.error("FOO"); Explicit return in an arrow function (ts.isArrowFunction(node) && !ts.isBlock(node.body)) || // Argument of a function call (ts.isCallExpression(node) && ts.isCallExpression(node.parent) && node.parent.arguments.some((arg) => arg === node)) || // Chaining (ts.isPropertyAccessExpression(node) && ts.isCallExpression(node.expression) && ts.isPropertyAccessExpression(node.expression.parent) && ts.isCallExpression(node.expression.parent.expression))) { isReturnValueUsed = true; } node = node.parent; } return isReturnValueUsed; } export function isConditionalAccess(node) { const parent = node.parent; if (!parent) { return false; } switch (parent.kind) { case ts.SyntaxKind.IfStatement: return parent.expression === node; case ts.SyntaxKind.ConditionalExpression: return parent.condition === node; case ts.SyntaxKind.BinaryExpression: if (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) { return true; } } let currentNode = node; while (currentNode && (ts.isPropertyAccessExpression(currentNode) || ts.isElementAccessExpression(currentNode))) { if (currentNode.questionDotToken) { return true; } currentNode = currentNode.expression; } return false; } export function extractNamespace(node) { const propAccessChain = []; propAccessChain.push(node.expression.getText()); let scanNode = node; while (ts.isPropertyAccessExpression(scanNode)) { if (!ts.isIdentifier(scanNode.name)) { throw new Error(`Unexpected PropertyAccessExpression node: Expected name to be identifier but got ` + ts.SyntaxKind[scanNode.name.kind]); } propAccessChain.push(scanNode.name.text); scanNode = scanNode.parent; } return propAccessChain.join("."); } export function isGlobalThis(nodeType) { return [ "Window & typeof globalThis", "typeof globalThis", // "Window", // top and parent will resolve to this string, however they are still treated as type 'any' ].includes(nodeType); } export function getSymbolModuleDeclaration(symbol) { let parent = symbol.valueDeclaration?.parent; while (parent && !ts.isModuleDeclaration(parent)) { parent = parent.parent; } return parent; } /** * Extracts the sap.ui API namespace from a symbol name and a module declaration * (from @sapui5/types sap.ui.core.d.ts), e.g. sap.ui.view. */ export function extractSapUiNamespace(symbolName, moduleDeclaration) { const namespace = []; let currentModuleDeclaration = moduleDeclaration; while (currentModuleDeclaration && ts.isModuleDeclaration(currentModuleDeclaration) && currentModuleDeclaration.flags & ts.NodeFlags.Namespace) { namespace.unshift(currentModuleDeclaration.name.text); currentModuleDeclaration = currentModuleDeclaration.parent?.parent; } if (!namespace.length) { return undefined; } else { namespace.push(symbolName); return namespace.join("."); } } /** * Recursively count all child nodes matching the filter */ export function countChildNodesRecursive(node, filterKinds = []) { let count = 0; function countNodes(currentNode) { if (filterKinds.length === 0 || filterKinds.includes(currentNode.kind)) { count++; } currentNode.forEachChild(countNodes); } countNodes(node); // Include the root node in the count return count; } export function findNodeRecursive(node, filterKinds = []) { let foundNode; function findNode(currentNode) { if (filterKinds.length === 0 || filterKinds.includes(currentNode.kind)) { foundNode = currentNode; return; } currentNode.forEachChild(findNode); } findNode(node); // Include the root node in the search return foundNode; } let CachedSideEffectFreeApiMatcher; function getSideEffectFreeApiMatcher() { if (!CachedSideEffectFreeApiMatcher) { const m = new Ui5TypeInfoMatcher("sap.ui.core"); m.declareNamespace("sap", [ m.namespace("ui", [ m.function("getCore", true), ]), ]); m.declareModule("sap/ui/Core", [ m.class("Core", [ m.method("getConfiguration", true), ]), ]); CachedSideEffectFreeApiMatcher = m; } return CachedSideEffectFreeApiMatcher; } export function isSideEffectFree(node, checker) { // Get UI5 Type info for the given node const exprType = checker.getTypeAtLocation(node.expression); if (!exprType.symbol) { return false; } const ui5TypeInfo = getUi5TypeInfoFromSymbol(exprType.symbol); if (!ui5TypeInfo) { return false; } // Check whether the call expression is a side effect free function return !!getSideEffectFreeApiMatcher().match(ui5TypeInfo); } //# sourceMappingURL=utils.js.map