@ui5/linter
Version:
A static code analysis tool for UI5
191 lines • 9.26 kB
JavaScript
import ManifestReporter from "./ManifestReporter.js";
import { deprecatedLibraries, deprecatedComponents } from "../../utils/deprecations.js";
import { MESSAGE } from "../messages.js";
import semver from "semver";
import { parseManifest } from "./parser.js";
import RemoveJsonPropertyFix from "./fix/RemoveJsonPropertyFix.js";
const deprecatedViewTypes = ["JSON", "HTML", "JS", "Template"];
export default class ManifestLinter {
#reporter;
#content;
#resourcePath;
#context;
constructor(resourcePath, content, context) {
this.#resourcePath = resourcePath;
this.#content = content;
this.#context = context;
}
// eslint-disable-next-line @typescript-eslint/require-await
async lint() {
try {
const source = parseManifest(this.#content);
this.#reporter = new ManifestReporter(this.#resourcePath, this.#context, source);
this.#analyzeManifest(source);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.#context.addLintingMessage(this.#resourcePath, { id: MESSAGE.PARSING_ERROR, args: { message } });
}
}
#analyzeManifest(source) {
const manifest = source.data;
const minUI5Version = manifest?.["sap.ui5"]?.dependencies?.minUI5Version;
if (minUI5Version) {
// Simplify version extraction - handle both string and array cases
const availableVersions = Array.isArray(minUI5Version) ?
minUI5Version :
(typeof minUI5Version === "string" ? [minUI5Version] : []);
// Check if any version is below 1.136
const isBelow136 = availableVersions.some((version) => {
const normalizedVersion = semver.coerce(version);
return normalizedVersion && semver.lt(normalizedVersion, "1.136.0");
});
if (isBelow136) {
this.#reporter?.addMessage(MESSAGE.NO_LEGACY_UI5_VERSION_IN_MANIFEST, "/sap.ui5/dependencies/minUI5Version");
}
}
if (manifest?._version?.startsWith("2.")) {
this.#validatePropertiesForManifestVersion(source, true);
}
else {
this.#validatePropertiesForManifestVersion(source);
this.#reporter?.addMessage(MESSAGE.NO_OUTDATED_MANIFEST_VERSION, "/_version");
}
}
#validatePropertiesForManifestVersion(source, isManifest2 = false) {
const manifest = source.data;
const { resources, models, dependencies, rootView, routing } = (manifest["sap.ui5"] ?? {});
const { dataSources } = (manifest["sap.app"] ?? {});
// Validate async flags for manifest version 2
if (isManifest2) {
this.#validateAsyncFlagsForManifestV2(rootView, routing);
}
// Detect deprecated libraries:
const libKeys = (dependencies?.libs && Object.keys(dependencies.libs)) ?? [];
libKeys.forEach((libKey) => {
if (deprecatedLibraries.includes(libKey)) {
this.#reporter?.addMessage(MESSAGE.DEPRECATED_LIBRARY, {
libraryName: libKey,
}, `/sap.ui5/dependencies/libs/${libKey}`);
}
});
// Detect deprecated components:
const componentKeys = (dependencies?.components && Object.keys(dependencies.components)) ?? [];
componentKeys.forEach((componentKey) => {
if (deprecatedComponents.includes(componentKey)) {
this.#reporter?.addMessage(MESSAGE.DEPRECATED_COMPONENT, {
componentName: componentKey,
}, `/sap.ui5/dependencies/components/${componentKey}`);
}
});
// Detect deprecated type of rootView:
if (typeof rootView === "object" && rootView.type && deprecatedViewTypes.includes(rootView.type)) {
this.#reporter?.addMessage(MESSAGE.DEPRECATED_VIEW_TYPE, {
viewType: rootView.type,
}, "/sap.ui5/rootView/type");
}
// Detect deprecated view type in routing.config:
if (routing?.config?.viewType && deprecatedViewTypes.includes(routing.config.viewType)) {
this.#reporter?.addMessage(MESSAGE.DEPRECATED_VIEW_TYPE, {
viewType: routing.config.viewType,
}, "/sap.ui5/routing/config/viewType");
}
// Detect deprecations in routing.targets:
const targets = routing?.targets;
if (targets) {
for (const [key, target] of Object.entries(targets)) {
// Check if name starts with module and viewType is defined:
const name = target.name ?? target.viewName;
if (name && name.startsWith("module:")) {
if (target.viewType) {
this.#reporter?.addMessage(MESSAGE.REDUNDANT_VIEW_CONFIG_PROPERTY, {
propertyName: "viewType",
}, `/sap.ui5/routing/targets/${key}/viewType`);
}
}
const pathToViewObject = `/sap.ui5/routing/targets/${key}`;
// Detect deprecated view type:
if (target.viewType && deprecatedViewTypes.includes(target.viewType)) {
this.#reporter?.addMessage(MESSAGE.DEPRECATED_VIEW_TYPE, {
viewType: target.viewType,
}, `${pathToViewObject}/viewType`);
}
}
}
if (resources?.js) {
const key = "/sap.ui5/resources/js";
let fix;
const messageId = isManifest2 ?
MESSAGE.NO_REMOVED_MANIFEST_PROPERTY :
MESSAGE.DEPRECATED_MANIFEST_JS_RESOURCES;
if (!resources.js.length) {
// If there are no js resources, we can remove the whole array and
// if the array is the only property of "resources", we can also remove
// the "resources" object
fix = new RemoveJsonPropertyFix({
key,
pointers: source.pointers,
removeEmptyDirectParent: true,
});
}
this.#reporter?.addMessage(messageId, { propName: key }, key, fix);
}
const modelKeys = (models && Object.keys(models)) ?? [];
modelKeys.forEach((modelKey) => {
const curModel = (models?.[modelKey]) ?? {};
if (!curModel.type) {
const curDataSource = dataSources && curModel.dataSource &&
dataSources[curModel.dataSource];
if (curDataSource &&
/* if not provided dataSource.type="OData" */
(curDataSource.type === "OData" || !curDataSource.type)) {
curModel.type = curDataSource.settings?.odataVersion === "4.0" ?
"sap.ui.model.odata.v4.ODataModel" :
"sap.ui.model.odata.v2.ODataModel";
}
// There are other types that can be found in sap/ui/core/Component, but the one
// we actually care here is just the "sap.ui.model.odata.v4.ODataModel"
}
if (curModel.type && [
"sap.ui.model.odata.ODataModel",
"sap.zen.dsh.widgets.SDKModel",
].includes(curModel.type)) {
this.#reporter?.addMessage(MESSAGE.DEPRECATED_CLASS, {
className: curModel.type,
details: `{@link ${curModel.type}}`,
}, `/sap.ui5/models/${modelKey}/type`);
}
if (curModel.type === "sap.ui.model.odata.v4.ODataModel" &&
curModel.settings && "synchronizationMode" in curModel.settings) {
const key = `/sap.ui5/models/${modelKey}/settings/synchronizationMode`;
const fix = new RemoveJsonPropertyFix({
key,
pointers: source.pointers,
removeEmptyDirectParent: true,
});
this.#reporter?.addMessage(MESSAGE.DEPRECATED_ODATA_MODEL_V4_SYNCHRONIZATION_MODE, {
modelName: modelKey,
}, key, fix);
}
});
}
#validateAsyncFlagsForManifestV2(rootView, routing) {
// Check rootView async flag
if (typeof rootView === "object" && rootView && "async" in rootView &&
typeof rootView.async === "boolean") {
// Error: async is redundant in manifest v2
this.#reporter?.addMessage(MESSAGE.NO_REMOVED_MANIFEST_PROPERTY, {
propName: "/sap.ui5/rootView/async",
}, "/sap.ui5/rootView/async");
}
// Check routing config async flag
if (routing?.config && "async" in routing.config &&
typeof routing.config.async === "boolean") {
// Error: async is redundant in manifest v2
this.#reporter?.addMessage(MESSAGE.NO_REMOVED_MANIFEST_PROPERTY, {
propName: "/sap.ui5/routing/config/async",
}, "/sap.ui5/routing/config/async");
}
}
}
//# sourceMappingURL=ManifestLinter.js.map