@ui5/linter
Version:
A static code analysis tool for UI5
347 lines • 11.8 kB
JavaScript
import ts from "typescript";
import { getPropertyNameText } from "./utils/utils.js";
export var Ui5TypeInfoKind;
(function (Ui5TypeInfoKind) {
Ui5TypeInfoKind["Module"] = "Module";
Ui5TypeInfoKind["Namespace"] = "Namespace";
Ui5TypeInfoKind["Class"] = "Class";
Ui5TypeInfoKind["Constructor"] = "Constructor";
Ui5TypeInfoKind["ConstructorParameter"] = "ConstructorParameter";
Ui5TypeInfoKind["ManagedObjectSettings"] = "ManagedObjectSettings";
Ui5TypeInfoKind["MetadataProperty"] = "MetadataProperty";
Ui5TypeInfoKind["MetadataEvent"] = "MetadataEvent";
Ui5TypeInfoKind["MetadataAggregation"] = "MetadataAggregation";
Ui5TypeInfoKind["MetadataAssociation"] = "MetadataAssociation";
Ui5TypeInfoKind["Function"] = "Function";
Ui5TypeInfoKind["Method"] = "Method";
Ui5TypeInfoKind["Property"] = "Property";
Ui5TypeInfoKind["StaticMethod"] = "StaticMethod";
Ui5TypeInfoKind["StaticProperty"] = "StaticProperty";
Ui5TypeInfoKind["Enum"] = "Enum";
Ui5TypeInfoKind["EnumMember"] = "EnumMember";
Ui5TypeInfoKind["Export"] = "Export";
})(Ui5TypeInfoKind || (Ui5TypeInfoKind = {}));
function isManagedObjectSettingsInterfaceName(name) {
// This check is based on the naming convention when generating the Interface for ManagedObject settings:
// $<ClassName>Settings
return name.startsWith("$") && name.endsWith("Settings") && name.length > 9;
}
function isInterfaceInheritingFromManagedObjectSettings(node) {
if (!node.heritageClauses) {
return false;
}
if (!isManagedObjectSettingsInterfaceName(node.name.text)) {
return false;
}
return true;
}
function getLibraryNameFromSourceFile(sourceFile) {
const fileName = sourceFile.fileName;
const match = /(?:^|\/)([a-zA-Z0-9.]+)\.d\.ts$/.exec(fileName);
if (match?.[1]) {
return match[1];
}
return fileName.replace(/\.d\.ts$/, "");
}
/**
* Extracts module / global type information from UI5 symbols.
*/
export function getUi5TypeInfoFromSymbol(symbol, apiExtract) {
if (symbol.valueDeclaration) {
return getUi5TypeInfoFromDeclaration(symbol.valueDeclaration, apiExtract);
}
else if (symbol.declarations) {
for (const declaration of symbol.declarations) {
const typeInfo = getUi5TypeInfoFromDeclaration(declaration, apiExtract);
if (typeInfo) {
return typeInfo;
}
}
}
}
function getUi5TypeInfoFromDeclaration(declaration, apiExtract) {
const sourceFile = declaration.getSourceFile();
const node = declaration;
let currentNode = declaration;
let currentNamespaceTypeInfo;
let namespaceTypeInfo;
let moduleName;
while (currentNode) {
if (ts.isModuleDeclaration(currentNode)) {
if (currentNode.flags & ts.NodeFlags.Namespace) {
const newNamespace = {
kind: Ui5TypeInfoKind.Namespace,
name: currentNode.name.text,
};
if (currentNamespaceTypeInfo) {
currentNamespaceTypeInfo.parent = newNamespace;
}
else {
namespaceTypeInfo = newNamespace;
}
currentNamespaceTypeInfo = newNamespace;
}
else if (currentNode.parent && ts.isSourceFile(currentNode.parent)) {
// Only consider top-level module declarations
moduleName = currentNode.name.text;
break;
}
}
currentNode = currentNode.parent;
}
const libraryName = getLibraryNameFromSourceFile(sourceFile);
let leafNode;
if (namespaceTypeInfo) {
leafNode = namespaceTypeInfo;
}
if (moduleName) {
const moduleNode = {
kind: Ui5TypeInfoKind.Module,
name: moduleName,
library: libraryName,
};
if (!namespaceTypeInfo) {
leafNode = moduleNode;
}
else {
namespaceTypeInfo.parent = moduleNode;
}
}
if (!leafNode) {
// No module or namespace found, ignore
return;
}
let currentTypeInfo;
if (moduleName && apiExtract) {
currentTypeInfo ??= createMangedObjectSettingsTypeInfo(node, moduleName, leafNode, apiExtract);
}
currentTypeInfo ??= createClassTypeInfo(node, leafNode);
currentTypeInfo ??= createEnumMemberTypeInfo(node, leafNode);
currentTypeInfo ??= createFunctionTypeInfo(node, leafNode);
currentTypeInfo ??= createExportTypeInfo(node, leafNode);
return currentTypeInfo;
}
export function getModuleTypeInfo(node) {
if (node.kind === Ui5TypeInfoKind.Module) {
return node;
}
while (node.parent) {
if (node.parent.kind === Ui5TypeInfoKind.Module) {
return node.parent;
}
node = node.parent;
}
return undefined;
}
export function getNamespace(node) {
let namespace;
while (node) {
if (node.kind === Ui5TypeInfoKind.Namespace) {
namespace = node.name + (namespace ? "." + namespace : "");
}
if ("parent" in node && node.parent) {
node = node.parent;
}
else {
break;
}
}
return namespace;
}
function createMangedObjectSettingsTypeInfo(node, moduleName, parent, apiExtract) {
if (ts.isPropertySignature(node)) {
// Potentially a UI5 metadata property in a constructor call
const parentNode = node.parent;
if (ts.isInterfaceDeclaration(parentNode) && isInterfaceInheritingFromManagedObjectSettings(parentNode)) {
// This is a UI5 metadata property
const className = moduleName.replace(/\//g, ".");
const name = getPropertyNameText(node.name);
if (className && name) {
let kind = undefined;
if (apiExtract.isProperty(className, name)) {
kind = Ui5TypeInfoKind.MetadataProperty;
}
else if (apiExtract.isAggregation(className, name)) {
kind = Ui5TypeInfoKind.MetadataAggregation;
}
else if (apiExtract.isAssociation(className, name)) {
kind = Ui5TypeInfoKind.MetadataAssociation;
}
else if (apiExtract.isEvent(className, name)) {
kind = Ui5TypeInfoKind.MetadataEvent;
} // else: Special settings, etc.
if (kind) {
return {
kind,
name,
parent: {
kind: Ui5TypeInfoKind.ManagedObjectSettings,
name: parentNode.name.text,
parent,
},
};
}
}
}
}
}
function createClassTypeInfo(node, parent) {
if (ts.isClassDeclaration(node) && node.name) {
return {
kind: Ui5TypeInfoKind.Class,
name: node.name.text,
parent,
};
}
let classNode = node.parent;
const skippedNodes = [];
while (!ts.isInterfaceDeclaration(classNode) && !ts.isClassDeclaration(classNode)) {
if (!classNode.parent) {
return;
}
skippedNodes.unshift(classNode);
classNode = classNode.parent;
}
if (!classNode.name) {
// Class name can be undefined in `export default class { ... }`. But we generally don't expect
// to encounter this case in the UI5 types so we can safely ignore it and keeps our types greedy.
return;
}
const classTypeInfo = {
kind: Ui5TypeInfoKind.Class,
name: classNode.name.text,
parent,
};
if (ts.isMethodSignature(node) || ts.isMethodDeclaration(node)) {
if (skippedNodes.length) {
throw new Error(`Unexpected nested method declaration: ${skippedNodes.map((n) => n.getText()).join(" -> ")}`);
}
const name = getPropertyNameText(node.name);
if (!name) {
// We need a name for methods and properties
return undefined;
}
return {
kind: hasStaticModifier(node) ? Ui5TypeInfoKind.StaticMethod : Ui5TypeInfoKind.Method,
name,
parent: classTypeInfo,
};
}
if (ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isParameter(node)) {
let parent = classTypeInfo;
for (const skippedNode of skippedNodes) {
if (ts.isConstructorDeclaration(skippedNode)) {
parent = {
kind: Ui5TypeInfoKind.Constructor,
name: "constructor",
parent,
};
}
else if (ts.isParameter(skippedNode)) {
parent = {
kind: Ui5TypeInfoKind.ConstructorParameter,
name: skippedNode.name.getText(),
parent,
};
}
}
if (!ts.isPropertyName(node.name)) {
return;
}
const name = getPropertyNameText(node.name);
if (!name) {
// We need a name for methods and properties
return undefined;
}
if (ts.isParameter(node)) {
return {
kind: Ui5TypeInfoKind.ConstructorParameter,
name,
parent: parent,
};
}
// PropertySignature or PropertyDeclaration
return {
kind: hasStaticModifier(node) ? Ui5TypeInfoKind.StaticProperty : Ui5TypeInfoKind.Property,
name,
parent,
};
}
}
function createEnumTypeInfo(node, parent) {
if (!ts.isEnumDeclaration(node)) {
return;
}
return {
kind: Ui5TypeInfoKind.Enum,
name: node.name.text,
parent,
};
}
function createEnumMemberTypeInfo(node, parent) {
if (ts.isEnumDeclaration(node)) {
return createEnumTypeInfo(node, parent);
}
if (!ts.isEnumMember(node)) {
return;
}
const enumTypeInfo = createEnumTypeInfo(node.parent, parent);
if (!enumTypeInfo) {
return;
}
const name = getPropertyNameText(node.name);
if (!name) {
// We need a name for enum members
return;
}
return {
kind: Ui5TypeInfoKind.EnumMember,
name,
parent: enumTypeInfo,
};
}
function createFunctionTypeInfo(node, parent) {
if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node) && !ts.isArrowFunction(node)) {
return;
}
if (!node.name) {
// Ignore anonymous functions
return;
}
return {
kind: Ui5TypeInfoKind.Function,
name: node.name.text,
parent,
};
}
function createExportTypeInfo(node, parent) {
if (ts.isExportAssignment(node)) {
// Exported module
return {
kind: Ui5TypeInfoKind.Export,
name: node.isExportEquals ? "default" : "export", // export default foo vs. export = foo
parent,
};
}
else if (ts.isExportSpecifier(node)) {
// Exported symbol
const name = getPropertyNameText(node.name);
if (!name) {
// We need a name for exports
return;
}
return {
kind: Ui5TypeInfoKind.Export,
name,
parent,
};
}
return;
}
function hasStaticModifier(node) {
if (!ts.canHaveModifiers(node)) {
return false;
}
return ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword);
}
//# sourceMappingURL=Ui5TypeInfo.js.map