@ui5/linter
Version:
A static code analysis tool for UI5
243 lines • 10.7 kB
JavaScript
import { MESSAGE } from "../messages.js";
import BindingParser from "./lib/BindingParser.js";
const ODATA_EXPRESSION_ADDONS_MODULE = "sap/ui/model/odata/ODataExpressionAddons";
const ODATA_FUNCTION_MODULE_MAP = {
compare: "sap/ui/model/odata/v4/ODataUtils",
fillUriTemplate: "sap/ui/thirdparty/URITemplate",
uriEncode: "sap/ui/model/odata/ODataUtils",
};
export default class BindingLinter {
#resourcePath;
#context;
#isJsBindingString;
constructor(resourcePath, context, isJsBindingString = false) {
this.#resourcePath = resourcePath;
this.#context = context;
this.#isJsBindingString = isJsBindingString;
}
#parseBinding(binding) {
// Do not unescape (3rd argument), as we're only interested in the binding
const bindingInfo = BindingParser.complexParser(binding, null, false, true, true, true);
return bindingInfo;
}
#analyzeCommonBindingParts(bindingInfo, requireDeclarations, position) {
let path, events;
if (typeof bindingInfo === "string") {
path = bindingInfo;
}
else {
path = bindingInfo.path;
events = bindingInfo.events;
}
if (events && typeof events === "object") {
for (const eventHandler of Object.values(events)) {
this.checkForGlobalReference(eventHandler, requireDeclarations, position);
}
}
if (path) {
// Check for computed annotations (@@ syntax)
const atAtIndex = path.indexOf("@@");
if (atAtIndex >= 0) {
const openingBracketIndex = path.indexOf("(", atAtIndex); // opening bracket is optional
const computationFunction = path.slice(atAtIndex + 2, openingBracketIndex !== -1 ? openingBracketIndex : undefined);
// Exclude built-in functions for currency and units of measure from global check.
// See: https://github.com/SAP/openui5/blob/e1de19f9a09a0e7a112d63a8f8d4b04afa96620c/src/sap.ui.core/src/sap/ui/model/odata/v4/ODataMetaModel.js#L1268-L1274
if (computationFunction !== "requestCurrencyCodes" && computationFunction !== "requestUnitsOfMeasure") {
this.checkForGlobalReference(computationFunction, requireDeclarations, position);
}
}
}
if (typeof bindingInfo === "object" && "tokens" in bindingInfo) {
this.#lintExpressionBindingTokens(bindingInfo, requireDeclarations, position);
}
}
#analyzePropertyBinding(bindingInfo, requireDeclarations, position) {
if (typeof bindingInfo === "string") {
return;
}
const { formatter, type } = bindingInfo;
if (formatter) {
// If the binding is defined as a string and inside a JS/TS sourcefile,
// the imports cannot be used for resolving formatter references:
if (this.#isJsBindingString) {
requireDeclarations = [];
}
if (Array.isArray(formatter)) {
formatter.forEach((formatterItem) => {
this.checkForGlobalReference(formatterItem, requireDeclarations, position);
});
}
else {
this.checkForGlobalReference(formatter, requireDeclarations, position);
}
}
if (type) {
this.checkForGlobalReference(type, requireDeclarations, position);
}
}
#analyzeAggregationBinding(bindingInfo, requireDeclarations, position) {
if (typeof bindingInfo === "string") {
return;
}
const { factory, groupHeaderFactory, filters, sorter } = bindingInfo;
if (factory) {
this.checkForGlobalReference(factory, requireDeclarations, position);
}
if (groupHeaderFactory) {
this.checkForGlobalReference(groupHeaderFactory, requireDeclarations, position);
}
if (filters) {
this.#analyzeFilters(filters, requireDeclarations, position);
}
if (sorter) {
this.#analyzeSorter(sorter, requireDeclarations, position);
}
}
#analyzeFilters(filters, requireDeclarations, position) {
if (Array.isArray(filters)) {
for (const filter of filters) {
this.#analyzeFilters(filter, requireDeclarations, position);
}
return;
}
const { test, filters: nestedFilters, condition } = filters;
if (test) {
this.checkForGlobalReference(test, requireDeclarations, position);
}
if (nestedFilters) {
this.#analyzeFilters(nestedFilters, requireDeclarations, position);
}
if (condition) {
this.#analyzeFilters(condition, requireDeclarations, position);
}
}
#analyzeSorter(sorter, requireDeclarations, position) {
if (Array.isArray(sorter)) {
for (const sorterItem of sorter) {
this.#analyzeSorter(sorterItem, requireDeclarations, position);
}
return;
}
const { group, comparator } = sorter;
if (group && typeof group !== "boolean") {
this.checkForGlobalReference(group, requireDeclarations, position);
}
if (comparator) {
this.checkForGlobalReference(comparator, requireDeclarations, position);
}
}
checkForGlobalReference(ref, requireDeclarations, position) {
// Global reference detected
const variableName = this.getGlobalReference(ref, requireDeclarations);
if (!variableName) {
return;
}
this.#context.addLintingMessage(this.#resourcePath, MESSAGE.NO_GLOBALS, {
variableName,
namespace: ref,
}, position);
}
getGlobalReference(ref, requireDeclarations) {
if (ref.startsWith(".")) {
// Ignore empty reference or reference to the controller (as indicated by the leading dot)
return null;
}
const parts = ref.split(".");
let variableName;
if (parts.length) {
variableName = parts[0];
}
else {
variableName = ref;
}
const requireDeclaration = requireDeclarations.find((decl) => decl.variableName === variableName ||
decl.moduleName === parts.join("/"));
if (requireDeclaration) {
// Local reference detected
return null;
}
// Global reference detected
return variableName;
}
lintPropertyBinding(bindingDefinition, requireDeclarations, position, reportParsingError = true) {
let bindingInfo, errorMessage;
try {
bindingInfo = this.#parseBinding(bindingDefinition);
if (bindingInfo) {
this.#lintPropertyBindingInfo(bindingInfo, requireDeclarations, position);
}
}
catch (err) {
errorMessage = err instanceof Error ? err.message : String(err);
if (reportParsingError) {
this.reportParsingError(errorMessage, position);
}
}
return { bindingInfo, errorMessage };
}
#lintPropertyBindingInfo(bindingInfo, requireDeclarations, position) {
this.#analyzeCommonBindingParts(bindingInfo, requireDeclarations, position);
this.#analyzePropertyBinding(bindingInfo, requireDeclarations, position);
// Recursively check all parts of the property binding
if (typeof bindingInfo === "object" && bindingInfo.parts) {
for (const part of bindingInfo.parts) {
this.#lintPropertyBindingInfo(part, requireDeclarations, position);
}
}
}
lintAggregationBinding(bindingDefinition, requireDeclarations, position) {
try {
const bindingInfo = this.#parseBinding(bindingDefinition);
if (typeof bindingInfo === "object") {
this.#lintAggregationBindingInfo(bindingInfo, requireDeclarations, position);
}
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.reportParsingError(message, position);
}
}
#lintAggregationBindingInfo(bindingInfo, requireDeclarations, position) {
this.#analyzeCommonBindingParts(bindingInfo, requireDeclarations, position);
this.#analyzeAggregationBinding(bindingInfo, requireDeclarations, position);
// As XML templating interprets all bindings as property bindings we also have to run the
// property binding analysis here.
// Note: This also covers the analysis of commons binding parts such as events and path
this.#analyzePropertyBinding(bindingInfo, requireDeclarations, position);
// Recursively check all parts of the aggregation binding
if (typeof bindingInfo === "object" && bindingInfo.parts) {
for (const part of bindingInfo.parts) {
this.#lintAggregationBindingInfo(part, requireDeclarations, position);
}
}
}
#lintExpressionBindingTokens(expressionBinding, requireDeclarations, position) {
const { tokens } = expressionBinding;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.id !== "IDENTIFIER" || token.value !== "odata" || tokens[i + 1]?.id !== ".") {
continue;
}
const functionToken = tokens[i + 2];
if (functionToken?.id !== "IDENTIFIER") {
// Can't happen. A "." must be followed by an IDENTIFIER
continue;
}
const functionName = functionToken.value;
if (functionName in ODATA_FUNCTION_MODULE_MAP) {
const expectedModuleName = ODATA_FUNCTION_MODULE_MAP[functionName];
if (
// There must be either an import for the corresponding module
// or for the ODataExpressionAddons module
!requireDeclarations.some((decl) => decl.moduleName === expectedModuleName || decl.moduleName === ODATA_EXPRESSION_ADDONS_MODULE)) {
this.#context.addLintingMessage(this.#resourcePath, MESSAGE.NO_ODATA_GLOBALS, {}, position);
}
}
i += 2; // Skip the next two tokens as we already checked them
}
}
reportParsingError(message, position) {
this.#context.addLintingMessage(this.#resourcePath, MESSAGE.PARSING_ERROR, { message }, position);
}
}
//# sourceMappingURL=BindingLinter.js.map