Skip to content

refactor(SourceFileLinter): Extract common logic to utils #661

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 13 additions & 66 deletions src/linter/ui5Types/SourceFileLinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
isClassMethod,
isSourceFileOfPseudoModuleType,
isSourceFileOfTypeScriptLib,
getSymbolModuleDeclaration,
isGlobalThis,
extractNamespace,
extractSapUiNamespace,
} from "./utils/utils.js";
import {taskStart} from "../../utils/perf.js";
import {getPositionsForNode} from "../../utils/nodePosition.js";
Expand Down Expand Up @@ -665,7 +669,7 @@ export default class SourceFileLinter {
}
const classType = this.checker.getTypeAtLocation(node.expression);

const moduleDeclaration = this.getSymbolModuleDeclaration(nodeType.symbol);
const moduleDeclaration = getSymbolModuleDeclaration(nodeType.symbol);
if (moduleDeclaration?.name.text === "sap/ui/core/routing/Router") {
this.#analyzeNewCoreRouter(node);
} else if (moduleDeclaration?.name.text === "sap/ui/model/odata/v4/ODataModel") {
Expand Down Expand Up @@ -727,47 +731,6 @@ export default class SourceFileLinter {
});
}

extractNamespace(node: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression): string {
const propAccessChain: string[] = [];
propAccessChain.push(node.expression.getText());

let scanNode: ts.Node = 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(".");
}

/**
* 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.
*/
extractSapUiNamespace(symbolName: string, moduleDeclaration: ts.ModuleDeclaration): string | undefined {
const namespace: string[] = [];
let currentModuleDeclaration: ts.Node | undefined = 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(".");
}
}

getDeprecationText(deprecatedTag: ts.JSDocTagInfo): string {
// (Workaround) There's an issue in some UI5 TS definition versions and where the
// deprecation text gets merged with the description. Splitting on double
Expand Down Expand Up @@ -822,14 +785,14 @@ export default class SourceFileLinter {
throw new Error(`Unhandled CallExpression expression syntax: ${ts.SyntaxKind[exprNode.kind]}`);
}

const moduleDeclaration = this.getSymbolModuleDeclaration(exprType.symbol);
const moduleDeclaration = getSymbolModuleDeclaration(exprType.symbol);
let globalApiName;

if (exprType.symbol && moduleDeclaration) {
const symbolName = exprType.symbol.getName();
const moduleName = moduleDeclaration.name.text;
const nodeType = this.checker.getTypeAtLocation(node);
globalApiName = this.extractSapUiNamespace(symbolName, moduleDeclaration);
globalApiName = extractSapUiNamespace(symbolName, moduleDeclaration);

if (symbolName === "init" && moduleName === "sap/ui/core/Lib") {
// Check for sap/ui/core/Lib.init usages
Expand Down Expand Up @@ -919,7 +882,7 @@ export default class SourceFileLinter {
additionalMessage = `of module '${this.checker.typeToString(lhsExprType)}'`;
} else if (ts.isPropertyAccessExpression(exprNode)) {
// left-hand-side is a module or namespace, e.g. "module.deprecatedMethod()"
additionalMessage = `(${this.extractNamespace(exprNode)})`;
additionalMessage = `(${extractNamespace(exprNode)})`;
}
} else if (globalApiName) {
additionalMessage = `(${globalApiName})`;
Expand Down Expand Up @@ -952,14 +915,6 @@ export default class SourceFileLinter {
}
}

getSymbolModuleDeclaration(symbol: ts.Symbol) {
let parent = symbol.valueDeclaration?.parent;
while (parent && !ts.isModuleDeclaration(parent)) {
parent = parent.parent;
}
return parent;
}

#analyzeLibInitCall(
node: ts.CallExpression,
exprNode: ts.CallExpression | ts.ElementAccessExpression | ts.PropertyAccessExpression | ts.Identifier) {
Expand Down Expand Up @@ -1384,7 +1339,7 @@ export default class SourceFileLinter {
*/
#isPropertyBinding(node: ts.NewExpression | ts.CallExpression, propNames: string[]) {
const controlAmbientModule =
this.getSymbolModuleDeclaration(this.checker.getTypeAtLocation(node).symbol);
getSymbolModuleDeclaration(this.checker.getTypeAtLocation(node).symbol);

let classArg;
if (controlAmbientModule?.body && ts.isModuleBlock(controlAmbientModule.body)) {
Expand Down Expand Up @@ -1446,7 +1401,7 @@ export default class SourceFileLinter {
}
let namespace;
if (ts.isPropertyAccessExpression(node)) {
namespace = this.extractNamespace(node);
namespace = extractNamespace(node);
}
if (this.isSymbolOfJquerySapType(deprecationInfo.symbol)) {
const fixHints = this.getJquerySapFixHints(node);
Expand Down Expand Up @@ -1482,14 +1437,6 @@ export default class SourceFileLinter {
}
}

isGlobalThis(nodeType: string) {
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);
}

analyzeExportedValuesByLib(node: ts.PropertyAccessExpression | ts.ElementAccessExpression) {
if (!ts.isElementAccessExpression(node) &&
node.name?.kind !== ts.SyntaxKind.Identifier) {
Expand Down Expand Up @@ -1592,7 +1539,7 @@ export default class SourceFileLinter {

// Get the NodeType in order to check whether this is indirect global access via Window
const nodeType = this.checker.getTypeAtLocation(exprNode);
if (this.isGlobalThis(this.checker.typeToString(nodeType))) {
if (isGlobalThis(this.checker.typeToString(nodeType))) {
// In case of Indirect global access we need to check for
// a global UI5 variable on the right side of the expression instead of left
if (ts.isPropertyAccessExpression(node)) {
Expand All @@ -1613,7 +1560,7 @@ export default class SourceFileLinter {
if (symbol && this.isSymbolOfUi5OrThirdPartyType(symbol) &&
!((ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) &&
this.isAllowedPropertyAccess(node))) {
const namespace = this.extractNamespace((node as ts.PropertyAccessExpression));
const namespace = extractNamespace((node as ts.PropertyAccessExpression));
this.#reporter.addMessage(MESSAGE.NO_GLOBALS, {
variableName: symbol.getName(),
namespace,
Expand All @@ -1632,7 +1579,7 @@ export default class SourceFileLinter {
return true;
}

const propAccess = this.extractNamespace(node);
const propAccess = extractNamespace(node);
return [
"sap.ui.define",
"sap.ui.require",
Expand Down
59 changes: 59 additions & 0 deletions src/linter/ui5Types/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,62 @@ export function isConditionalAccess(node: ts.Node): boolean {

return false;
}

export function extractNamespace(
node: ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression
): string {
const propAccessChain: string[] = [];
propAccessChain.push(node.expression.getText());

let scanNode: ts.Node = 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: string) {
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: ts.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: string, moduleDeclaration: ts.ModuleDeclaration): string | undefined {
const namespace: string[] = [];
let currentModuleDeclaration: ts.Node | undefined = 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(".");
}
}