ui5plugin-linter
Version:
320 lines (319 loc) • 16.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TagAttributeLinter = void 0;
const ui5plugin_parser_1 = require("ui5plugin-parser");
const __1 = require("../../..");
const Linter_1 = require("../../Linter");
const XMLLinter_1 = require("./abstraction/XMLLinter");
const EventPatternValidator_1 = require("./pattern/EventPatternValidator");
const IdPatternValidator_1 = require("./pattern/IdPatternValidator");
function isNumeric(value) {
return /^-{0,1}\d+$/.test(value);
}
class TagAttributeLinter extends XMLLinter_1.XMLLinter {
constructor() {
super(...arguments);
this.className = Linter_1.XMLLinters.TagAttributeLinter;
}
_getErrors(document) {
let errors = [];
const XMLFile = this._parser.textDocumentTransformer.toXMLFile(document);
if (!XMLFile) {
return errors;
}
const tags = this._parser.xmlParser.getAllTags(XMLFile);
errors = tags.reduce((tagErrors, tag, index) => {
const previousTag = tags[index - 1];
const tagAttributes = this._parser.xmlParser.getAttributesOfTheTag(tag);
if (!tagAttributes || (previousTag && this._isTagIgnored(previousTag))) {
return tagErrors;
}
const tagPrefix = this._parser.xmlParser.getTagPrefix(tag.text);
const className = this._parser.xmlParser.getClassNameFromTag(tag.text);
const libraryPath = className && this._parser.xmlParser.getLibraryPathFromTagPrefix(XMLFile, tagPrefix, tag.positionEnd);
if (!className || !libraryPath) {
return tagErrors;
}
const classOfTheTag = [libraryPath, className].join(".");
const attributeErrors = tagAttributes.reduce((errors, tagAttribute) => this._getTagAttributeErrors(errors, tagAttribute, {
XMLFile,
tags,
tagAttributes,
tag,
document,
classOfTheTag,
previousTag
}), []);
tagErrors.push(...attributeErrors);
return tagErrors;
}, []);
return errors;
}
_isTagIgnored(previousTag) {
return /<!-- ? ?-->/.test(previousTag.text);
}
_getTagAttributeErrors(errors, tagAttribute, { classOfTheTag, tagAttributes, document, tags, tag, XMLFile, previousTag }) {
//check tag attributes
const attributeValidation = this._validateTagAttribute(classOfTheTag, tagAttribute, tagAttributes, document, tags, tag, previousTag);
if (attributeValidation.valid) {
return errors;
}
const indexOfTagBegining = tag.text.indexOf(tagAttribute);
const positionBegin = tag.positionBegin + indexOfTagBegining;
const positionEnd = positionBegin + tagAttribute.length;
const range = __1.RangeAdapter.offsetsRange(document.getText(), positionBegin, positionEnd);
if (!range || !this._parser.xmlParser.getIfPositionIsNotInComments(XMLFile, tag.positionBegin)) {
return errors;
}
errors.push({
code: "UI5plugin",
message: attributeValidation.message || "Invalid attribute",
source: this.className,
severity: attributeValidation.severity,
attribute: tagAttribute,
range: range,
className: XMLFile.name || "",
fsPath: XMLFile.fsPath
});
return errors;
}
_validateTagAttribute(className, attribute, attributes, document, tags, tag, previousTag) {
let attributeValidation = {
valid: false,
severity: this._configHandler.getSeverity(this.className)
};
const UIClass = this._parser.classFactory.getUIClass(className);
const { attributeName, attributeValue } = this._parser.xmlParser.getAttributeNameAndValue(attribute);
const isExclusion = attributeName.startsWith("xmlns") || this._isAttributeNameAlwaysValid(className, attributeName);
const isAttributeNameDuplicated = this._getIfAttributeNameIsDuplicated(attribute, attributes);
const attributeNameValid = !isAttributeNameDuplicated &&
(isExclusion || this._validateAttributeName(className, attribute, previousTag));
const attributeValueValidData = this._validateAttributeValue(className, attribute, document, tags, tag, previousTag);
attributeValidation.valid = attributeNameValid && attributeValueValidData.isValueValid;
if (!attributeNameValid && UIClass.parentClassNameDotNotation) {
attributeValidation = this._validateTagAttribute(UIClass.parentClassNameDotNotation, attribute, attributes, document, tags, tag, previousTag);
}
else if (!attributeValidation.valid) {
let message = "";
if (isAttributeNameDuplicated) {
message = `Duplicated attribute ${attributeName}`;
}
else if (!attributeNameValid) {
message = `Invalid attribute name (${attributeName})`;
}
else if (!attributeValueValidData.isValueValid) {
message = attributeValueValidData.message || `Invalid attribute value (${attributeValue})`;
attributeValidation.severity = attributeValueValidData.severity;
}
attributeValidation.message = message;
}
return attributeValidation;
}
_isAttributeIgnored(previousTag, attributeName) {
return !!previousTag?.text
.match(/(?<=<!-- ? ?)(([a-zA-Z]|\d|,| )*?)(?= ?-->)/)?.[0]
.split(",")
.some(part => part.trim() === attributeName);
}
_isAttributePatternIgnored(previousTag, attributeName) {
return (!!previousTag?.text
.match(/(?<=<!-- ?-patterns ?)(([a-zA-Z]|\d|,| )*?)(?= ?-->)/)?.[0]
.split(",")
.some(part => part.trim() === attributeName) ||
!!(previousTag && /<!-- ?-patterns ?-->/.test(previousTag.text)));
}
_getIfAttributeNameIsDuplicated(attribute, attributes) {
const attributeNames = attributes.map(attribute => this._parser.xmlParser.getAttributeNameAndValue(attribute).attributeName);
const nameOfTheCurrentAttribute = this._parser.xmlParser.getAttributeNameAndValue(attribute).attributeName;
const isDuplicated = attributeNames.filter(attributeName => attributeName === nameOfTheCurrentAttribute).length > 1;
return isDuplicated;
}
_validateAttributeValue(className, attribute, document, tags, tag, previousTag) {
let isValueValid = true;
let message;
let severity = this._configHandler.getSeverity(this.className);
const shouldIdStyleBeChecked = !!this._configHandler.getIdNamingPattern();
const { attributeName, attributeValue } = this._parser.xmlParser.getAttributeNameAndValue(attribute);
if (this._isAttributeIgnored(previousTag, attributeName)) {
const isValueValid = true;
const severity = Linter_1.Severity.Information;
const message = "";
return { isValueValid, severity, message };
}
const UIClass = this._parser.classFactory.getUIClass(className);
const property = UIClass.properties.find(property => property.name === attributeName);
const event = UIClass.events.find(event => event.name === attributeName);
let responsibleControlName;
if (event) {
responsibleControlName = this._parser.fileReader.getResponsibleClassForXMLDocument(document);
}
const isAttributeBinded = attributeValue.startsWith("{") && attributeValue.endsWith("}");
if (attributeValue.startsWith("cmd:")) {
isValueValid = this._checkIfCommandIsMentionedInManifest(attributeValue, document);
if (!isValueValid) {
message = `Command "${attributeValue}" is not found in manifest`;
}
}
else if (attributeName === "id" &&
shouldIdStyleBeChecked &&
!this._isAttributePatternIgnored(previousTag, attributeName)) {
const id = attributeValue;
const pattern = this._configHandler.getIdNamingPattern();
const patternValidator = new IdPatternValidator_1.default(pattern, document, this._parser, this._configHandler);
try {
patternValidator.validateValue(id, tag);
isValueValid = true;
}
catch (error) {
isValueValid = false;
if (error instanceof Error) {
message = error.message;
}
}
}
else if (isAttributeBinded || property?.type === "string") {
isValueValid = true;
}
else if (property?.type === "sap.ui.core.URI") {
isValueValid = true;
}
else if (property && property.typeValues.length > 0) {
isValueValid = !!property.typeValues.find(typeValue => typeValue.text === attributeValue);
}
else if (property?.type === "boolean") {
isValueValid = ["true", "false"].indexOf(attributeValue) > -1;
}
else if (property?.type === "int") {
isValueValid = isNumeric(attributeValue);
}
else if (event && responsibleControlName) {
const eventHandlerName = this._parser.xmlParser.getEventHandlerNameFromAttributeValue(attributeValue);
const eventHandlerNoDot = eventHandlerName.startsWith(".") ? eventHandlerName.replace(".", "") : eventHandlerName;
const pattern = this._configHandler.getEventNamingPattern();
const patternValidator = pattern && new EventPatternValidator_1.default(pattern, document, this._parser, this._configHandler);
try {
if (patternValidator && !this._isAttributePatternIgnored(previousTag, attributeName)) {
patternValidator.validateValue(eventHandlerName, [event, tag]);
}
isValueValid = !!this._parser.xmlParser
.getMethodsOfTheControl(responsibleControlName)
.find(method => method.name === eventHandlerNoDot);
if (!isValueValid) {
const manifest = ui5plugin_parser_1.ParserPool.getManifestForClass(eventHandlerName);
if (manifest) {
const parts = eventHandlerName.split(".");
const formattedEventName = parts.pop();
const className = parts.join(".");
isValueValid = !!this._parser.xmlParser
.getMethodsOfTheControl(className)
.find(method => method.name === formattedEventName);
}
else if (eventHandlerNoDot.split(".").length === 2) {
({ isValueValid, message } = this._validateAttributeValueInCaseOfMethodCallFromMember(eventHandlerNoDot, responsibleControlName));
if (!isValueValid) {
({ isValueValid, message } = this._validateAttributeValueInCaseOfInTagRequire(eventHandlerName, tags, isValueValid, message));
}
}
}
message = message || `Event handler "${eventHandlerName}" not found in "${responsibleControlName}".`;
}
catch (error) {
isValueValid = false;
if (error instanceof Error) {
message = error.message;
}
}
}
if (isValueValid &&
property?.defaultValue &&
attributeValue === property.defaultValue &&
this._configHandler.getLinterUsage(Linter_1.XMLLinters.TagAttributeDefaultValueLinter)) {
isValueValid = false;
severity = this._configHandler.getSeverity(Linter_1.XMLLinters.TagAttributeDefaultValueLinter);
message = `Value "${attributeValue}" is unnecessary, it is the sames as default value of "${property.name}" property`;
}
return { isValueValid, message, severity };
}
_validateAttributeValueInCaseOfMethodCallFromMember(eventName, responsibleControlName) {
const eventNameParts = eventName.split(".");
const fieldName = eventNameParts.shift();
const methodName = eventNameParts.shift();
let isValueValid = false;
let message = "";
if (fieldName && methodName) {
const field = this._parser.classFactory
.getClassFields(responsibleControlName)
.find(field => field.name === fieldName);
isValueValid =
!!field?.type &&
!!this._parser.classFactory.getClassMethods(field.type).find(method => method.name === methodName);
if (!isValueValid) {
message = `Method "${methodName}" not found in "${fieldName}"`;
}
}
return { isValueValid, message };
}
_checkIfCommandIsMentionedInManifest(attributeValue, document) {
let isCommandMentionedInManifest = false;
const documentClassName = this._parser.fileReader.getClassNameFromPath(document.fileName);
const manifest = documentClassName && ui5plugin_parser_1.ParserPool.getManifestForClass(documentClassName);
if (manifest) {
const commandName = attributeValue.replace("cmd:", "");
isCommandMentionedInManifest = !!manifest.content["sap.ui5"]?.commands?.[commandName];
}
else {
//Let's skip linting if manifest wasn't found
isCommandMentionedInManifest = true;
}
return isCommandMentionedInManifest;
}
_validateAttributeValueInCaseOfInTagRequire(eventName, tags, isValueValid, message) {
const eventNameParts = eventName.split(".");
const className = eventNameParts.shift();
const methodName = eventNameParts.shift();
if (className && methodName) {
const attributesWithRequire = this._parser.xmlParser.getAllAttributesWithRequire(tags);
const classPath = this._parser.xmlParser.getClassPathFromRequire(attributesWithRequire, className);
if (classPath) {
const className = classPath.replace(/\//g, ".");
isValueValid = !!this._parser.xmlParser
.getMethodsOfTheControl(className)
.find(method => method.name === methodName);
message = `Method "${methodName}" not found in "${className}"`;
}
}
return { isValueValid, message };
}
_validateAttributeName(className, attribute, previousTag) {
const indexOfEqualSign = attribute.indexOf("=");
const attributeName = attribute.substring(0, indexOfEqualSign).trim();
if (this._isAttributeIgnored(previousTag, attributeName)) {
const isNameValid = true;
const severity = Linter_1.Severity.Information;
const message = "";
return { isValueValid: isNameValid, severity, message };
}
const UIClass = this._parser.classFactory.getUIClass(className);
const property = UIClass.properties.find(property => property.name === attributeName);
const event = UIClass.events.find(event => event.name === attributeName);
const aggregation = UIClass.aggregations.find(aggregation => aggregation.name === attributeName);
const association = UIClass.associations.find(association => association.name === attributeName);
const isXHTML = className.startsWith("http://www.w3.org/1999/xhtml.");
const somethingInClassWasFound = !!(property || event || aggregation || association || isXHTML);
return somethingInClassWasFound;
}
_isAttributeNameAlwaysValid(className, attribute) {
const exclusions = {
"*": ["id", "class", "binding"],
"sap.ui.core.mvc.View": ["controllerName"],
"sap.ui.core.mvc.XMLView": ["async"],
"sap.ui.core.Fragment": ["fragmentName"],
"sap.ui.core.ExtensionPoint": ["name"]
};
const isClassExclusion = exclusions[className] && exclusions[className].indexOf(attribute) > -1;
const isAlwaysExclusion = exclusions["*"].indexOf(attribute) > -1;
const perhapsItIsCustomData = attribute.indexOf(":") > -1;
return isClassExclusion || isAlwaysExclusion || perhapsItIsCustomData;
}
}
exports.TagAttributeLinter = TagAttributeLinter;