@ui5/linter
Version:
A static code analysis tool for UI5
313 lines • 14.9 kB
JavaScript
import ts from "typescript";
import path from "node:path/posix";
import jsonMap from "json-source-map";
import { MESSAGE } from "../messages.js";
import { getPropertyNameText } from "./utils/utils.js";
var AsyncPropertyStatus;
(function (AsyncPropertyStatus) {
AsyncPropertyStatus[AsyncPropertyStatus["parentPropNotSet"] = 0] = "parentPropNotSet";
AsyncPropertyStatus[AsyncPropertyStatus["propNotSet"] = 1] = "propNotSet";
AsyncPropertyStatus[AsyncPropertyStatus["false"] = 2] = "false";
AsyncPropertyStatus[AsyncPropertyStatus["true"] = 3] = "true";
})(AsyncPropertyStatus || (AsyncPropertyStatus = {}));
;
export default function analyzeComponentJson({ classDeclaration, manifestContent, resourcePath, reporter, context, checker, isUiComponent, }) {
// FIXME: This does a lot more than needed when we only check a Component (not a UIComponent)
// but it requires some refactoring to only perform the "hasManifestDefinition" check in that case
const analysisResult = findAsyncInterface({
classDeclaration, manifestContent, checker, isUiComponent,
});
if (analysisResult) {
reportComponentResults({ analysisResult, reporter, classDeclaration, manifestContent });
if (isUiComponent) {
reportUiComponentResults({ analysisResult, context, reporter, resourcePath, classDeclaration, manifestContent });
}
}
}
function getHighestPropertyStatus(aProp, bProp) {
return aProp > bProp ? aProp : bProp;
}
;
function mergeAsyncFlags(a, b) {
return {
hasManifestDefinition: a.hasManifestDefinition || b.hasManifestDefinition,
routingAsyncFlag: getHighestPropertyStatus(a.routingAsyncFlag, b.routingAsyncFlag),
rootViewAsyncFlag: getHighestPropertyStatus(a.rootViewAsyncFlag, b.rootViewAsyncFlag),
hasAsyncInterface: a.hasAsyncInterface || b.hasAsyncInterface,
};
}
/**
* Search for the async interface in the class hierarchy
*/
function findAsyncInterface({ classDeclaration, manifestContent, checker, isUiComponent }) {
const returnTypeTemplate = {
hasAsyncInterface: false,
routingAsyncFlag: AsyncPropertyStatus.parentPropNotSet,
rootViewAsyncFlag: AsyncPropertyStatus.parentPropNotSet,
hasManifestDefinition: false,
};
// Checks the interfaces and manifest of the class
const curClassAnalysis = classDeclaration.members.reduce((acc, member) => {
const checkResult = doPropsCheck(member, manifestContent);
return mergeAsyncFlags(acc, checkResult);
}, { ...returnTypeTemplate });
const heritageAnalysis = classDeclaration?.heritageClauses?.flatMap((parentClasses) => {
return parentClasses.types.flatMap((parentClass) => {
const parentClassType = checker.getTypeAtLocation(parentClass);
return parentClassType.symbol?.declarations?.flatMap((declaration) => {
let result = { ...returnTypeTemplate };
// Continue down the heritage chain to search for
// the async interface or manifest flags
if (ts.isClassDeclaration(declaration)) {
result = findAsyncInterface({
classDeclaration: declaration,
// We are unable to dynamically search for a parent-component's manifest.json
manifestContent: undefined,
checker,
isUiComponent,
}) ?? result;
}
else if (ts.isInterfaceDeclaration(declaration)) {
result.hasAsyncInterface = doAsyncInterfaceChecks(parentClass) ?? result.hasAsyncInterface;
}
return result;
});
});
}) ?? [];
return [...heritageAnalysis, curClassAnalysis].reduce((acc, curAnalysis) => {
return mergeAsyncFlags(acc ?? { ...returnTypeTemplate }, curAnalysis ?? { ...returnTypeTemplate });
});
}
function isCoreImportDeclaration(statement) {
return ts.isImportDeclaration(statement) &&
ts.isStringLiteral(statement.moduleSpecifier) &&
statement.moduleSpecifier.text === "sap/ui/core/library";
}
function doAsyncInterfaceChecks(importDeclaration) {
const sourceFile = importDeclaration.getSourceFile();
let coreLibImports;
if (sourceFile.isDeclarationFile) {
let moduleDeclaration;
while (!moduleDeclaration && importDeclaration.kind !== ts.SyntaxKind.SourceFile) {
if (ts.isModuleDeclaration(importDeclaration)) {
moduleDeclaration = importDeclaration;
}
else {
importDeclaration = importDeclaration.parent;
}
}
if (moduleDeclaration?.body?.kind === ts.SyntaxKind.ModuleBlock) {
coreLibImports = moduleDeclaration.body.statements.filter(isCoreImportDeclaration);
}
}
else {
coreLibImports = sourceFile.statements.filter(isCoreImportDeclaration);
}
if (!coreLibImports) {
return false;
}
const hasAsyncImport = coreLibImports.some((importDecl) => {
const importClause = importDecl.importClause;
if (!importClause) {
return;
}
if (!importClause.namedBindings) {
// Example: import "sap/ui/core/library"; or import library from "sap/ui/core/library";
}
else if (ts.isNamedImports(importClause.namedBindings)) {
// Example: import { IAsyncContentCreation } from "sap/ui/core/library";
return importClause.namedBindings.elements.some((namedImport) => namedImport.getText() === "IAsyncContentCreation");
}
else {
// Example: import * as library from "sap/ui/core/library";
// TODO: This requires additional handling
}
});
return hasAsyncImport;
}
function doPropsCheck(metadata, manifestContent) {
let classInterfaces;
let componentManifest;
if (metadata && ts.isPropertyDeclaration(metadata) &&
metadata.initializer && ts.isObjectLiteralExpression(metadata.initializer)) {
metadata.initializer.properties.forEach((prop) => {
if (!prop.name) {
return;
}
const propText = getPropertyNameText(prop.name);
if (propText === "interfaces") {
classInterfaces = prop;
}
else if (propText === "manifest") {
componentManifest = prop;
}
});
}
let hasAsyncInterface = false;
if (classInterfaces && ts.isPropertyAssignment(classInterfaces) &&
classInterfaces.initializer && ts.isArrayLiteralExpression(classInterfaces.initializer)) {
hasAsyncInterface = classInterfaces.initializer
.elements.some((implementedInterface) => {
return ts.isStringLiteralLike(implementedInterface) &&
implementedInterface.text === "sap.ui.core.IAsyncContentCreation";
});
}
let rootViewAsyncFlag = AsyncPropertyStatus.parentPropNotSet;
let routingAsyncFlag = AsyncPropertyStatus.parentPropNotSet;
let hasManifestDefinition = false;
if (componentManifest &&
ts.isPropertyAssignment(componentManifest) &&
ts.isObjectLiteralExpression(componentManifest.initializer)) {
/* eslint-disable @typescript-eslint/no-explicit-any */
const instanceOfPropsRecord = (obj) => {
return !!obj && typeof obj === "object";
};
hasManifestDefinition = true;
const manifestJson = extractPropsRecursive(componentManifest.initializer) ?? {};
let manifestSapui5Section;
if (instanceOfPropsRecord(manifestJson["sap.ui5"])) {
manifestSapui5Section = manifestJson["sap.ui5"].value;
}
if (instanceOfPropsRecord(manifestSapui5Section) &&
instanceOfPropsRecord(manifestSapui5Section?.rootView?.value)) {
rootViewAsyncFlag = AsyncPropertyStatus.propNotSet;
if (typeof manifestSapui5Section?.rootView?.value.async?.value === "boolean") {
const isRootViewAsync = manifestSapui5Section?.rootView?.value.async?.value;
rootViewAsyncFlag = isRootViewAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false;
}
}
if (instanceOfPropsRecord(manifestSapui5Section) &&
instanceOfPropsRecord(manifestSapui5Section?.routing?.value)) {
routingAsyncFlag = AsyncPropertyStatus.propNotSet;
if (instanceOfPropsRecord(manifestSapui5Section?.routing?.value.config?.value) &&
typeof manifestSapui5Section?.routing?.value.config?.value.async?.value === "boolean") {
const isRoutingAsync = manifestSapui5Section?.routing?.value.config?.value.async?.value;
routingAsyncFlag = isRoutingAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false;
}
}
}
else if (manifestContent) {
const parsedManifestContent = JSON.parse(manifestContent);
const { rootView, routing } = parsedManifestContent["sap.ui5"] ?? {};
if (rootView) {
rootViewAsyncFlag = AsyncPropertyStatus.propNotSet;
// @ts-expect-error async is part of RootViewDefFlexEnabled and RootViewDef
const isRootViewAsync = rootView.async;
if (typeof isRootViewAsync === "boolean") {
rootViewAsyncFlag = isRootViewAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false;
}
}
if (routing) {
routingAsyncFlag = AsyncPropertyStatus.propNotSet;
const isRoutingAsync = routing?.config?.async;
if (typeof isRoutingAsync === "boolean") {
routingAsyncFlag = isRoutingAsync ? AsyncPropertyStatus.true : AsyncPropertyStatus.false;
}
}
hasManifestDefinition = !!(componentManifest &&
ts.isPropertyAssignment(componentManifest) &&
ts.isPropertyName(componentManifest.initializer) &&
getPropertyNameText(componentManifest.initializer) === "json");
}
return {
routingAsyncFlag,
rootViewAsyncFlag,
hasAsyncInterface,
hasManifestDefinition,
};
}
function extractPropsRecursive(node) {
const properties = Object.create(null);
node.properties?.forEach((prop) => {
if (!ts.isPropertyAssignment(prop)) {
return;
}
const key = getPropertyNameText(prop.name);
if (!key) {
return;
}
if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) {
properties[key] = { value: true, node: prop.initializer };
}
else if (prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
properties[key] = { value: false, node: prop.initializer };
}
else if (prop.initializer.kind === ts.SyntaxKind.NullKeyword) {
properties[key] = { value: null, node: prop.initializer };
}
else if (ts.isObjectLiteralExpression(prop.initializer) && prop.initializer.properties) {
properties[key] = { value: extractPropsRecursive(prop.initializer), node: prop.initializer };
}
else if (ts.isArrayLiteralExpression(prop.initializer)) {
const resolvedValue = prop.initializer.elements.map((elem) => {
if (!ts.isObjectLiteralExpression(elem)) {
return;
}
return extractPropsRecursive(elem);
}).filter(($) => $);
properties[key] = { value: resolvedValue, node: prop.initializer };
}
else if (ts.isStringLiteralLike(prop.initializer) || ts.isNumericLiteral(prop.initializer)) {
properties[key] = { value: prop.initializer.text, node: prop.initializer };
}
else if (ts.isIdentifier(prop.initializer) || ts.isPrivateIdentifier(prop.initializer)) {
properties[key] = { value: prop.initializer.getText(), node: prop.initializer };
}
else {
// throw new Error("Unhandled property assignment");
}
});
return properties;
}
function reportComponentResults({ analysisResult, reporter, classDeclaration, manifestContent, }) {
if (!analysisResult.hasManifestDefinition && !!manifestContent) {
reporter.addMessage(MESSAGE.COMPONENT_MISSING_MANIFEST_DECLARATION, null, { node: classDeclaration });
}
}
function reportUiComponentResults({ analysisResult, reporter, classDeclaration, manifestContent, resourcePath, context, }) {
const { hasAsyncInterface, routingAsyncFlag, rootViewAsyncFlag } = analysisResult;
const componentFileName = path.basename(resourcePath);
if (hasAsyncInterface !== true) {
if ([AsyncPropertyStatus.propNotSet, AsyncPropertyStatus.false].includes(rootViewAsyncFlag) ||
[AsyncPropertyStatus.propNotSet, AsyncPropertyStatus.false].includes(routingAsyncFlag)) {
let asyncFlagMissingIn;
if (AsyncPropertyStatus.parentPropNotSet === rootViewAsyncFlag) {
// sap.ui5/rootView is not set at all, so skip it in the message
asyncFlagMissingIn = `"sap.ui5/routing/config"`;
}
else if (AsyncPropertyStatus.parentPropNotSet === routingAsyncFlag) {
// sap.ui5/routing/config is not set at all, so skip it in the message
asyncFlagMissingIn = `"sap.ui5/rootView"`;
}
else {
asyncFlagMissingIn = `"sap.ui5/routing/config" and "sap.ui5/rootView"`;
}
reporter.addMessage(MESSAGE.COMPONENT_MISSING_ASYNC_INTERFACE, {
componentFileName,
asyncFlagMissingIn,
}, { node: classDeclaration });
}
}
else {
const { pointers } = jsonMap.parse(manifestContent ?? "{}");
const report = (pointerKey) => {
if (manifestContent) {
// If the manifest.json is present, then we need to redirect the message pointers to it
const { key: posInfo } = pointers[pointerKey];
context.addLintingMessage(resourcePath.replace(componentFileName, "manifest.json"), MESSAGE.COMPONENT_REDUNDANT_ASYNC_FLAG, { asyncFlagLocation: pointerKey }, posInfo);
}
else {
reporter.addMessage(MESSAGE.COMPONENT_REDUNDANT_ASYNC_FLAG, {
asyncFlagLocation: pointerKey,
}, { node: classDeclaration });
}
};
if (rootViewAsyncFlag === AsyncPropertyStatus.true) {
report("/sap.ui5/rootView/async");
}
if (routingAsyncFlag === AsyncPropertyStatus.true) {
report("/sap.ui5/routing/config/async");
}
}
}
//# sourceMappingURL=asyncComponentFlags.js.map