analyze_ui5_performance
Analyze SAPUI5 XML and JavaScript files to identify performance issues and receive actionable recommendations for optimization.
Instructions
Analyze UI5 XML/JS files with performance-focused rules and actionable recommendations.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| sourceDir | No | ||
| maxFiles | No | ||
| maxFindings | No |
Implementation Reference
- Main tool definition with the async handler that scans UI5 XML/JS files for performance issues. The handler lists tracked files, analyzes each XML and JS file for performance anti-patterns, and returns findings with severity ratings.
export const analyzeUi5PerformanceTool = { name: "analyze_ui5_performance", description: "Analyze UI5 XML/JS files with performance-focused rules and actionable recommendations.", inputSchema, outputSchema, async handler(args, { context }) { const { sourceDir, maxFiles, maxFindings } = inputSchema.parse(args); const root = context.rootDir; const resolvedSourceDir = normalizeRelativePath(sourceDir ?? DEFAULT_SOURCE_DIR); const files = await listTrackedFiles({ root, sourceDir: resolvedSourceDir, maxFiles: maxFiles ?? DEFAULT_MAX_FILES }); const findings = []; let xmlFiles = 0; let jsFiles = 0; for (const relativePath of files) { const extension = path.extname(relativePath).toLowerCase(); const content = await readTextFile(relativePath, root); if (extension === ".xml") { xmlFiles += 1; analyzeXmlFile(relativePath, content, findings); } else if (extension === ".js") { jsFiles += 1; analyzeJavaScriptFile(relativePath, content, findings); } } const effectiveMaxFindings = maxFindings ?? DEFAULT_MAX_FINDINGS; const truncated = findings.length > effectiveMaxFindings; const slicedFindings = findings.slice(0, effectiveMaxFindings); const byRule = {}; const bySeverity = { low: 0, medium: 0, high: 0 }; for (const finding of slicedFindings) { bySeverity[finding.severity] += 1; byRule[finding.rule] = (byRule[finding.rule] ?? 0) + 1; } return outputSchema.parse({ sourceDir: resolvedSourceDir, scanned: { files: files.length, xmlFiles, jsFiles }, summary: { totalFindings: slicedFindings.length, bySeverity, byRule, truncated }, findings: slicedFindings }); } }; - Input schema (sourceDir, maxFiles, maxFindings) and output schema (scanned stats, summary with severity/rule counts, findings array) defined using Zod for validation.
const inputSchema = z.object({ sourceDir: z.string().min(1).optional(), maxFiles: z.number().int().min(10).max(2000).optional(), maxFindings: z.number().int().min(10).max(1000).optional() }).strict(); const findingSchema = z.object({ rule: z.string(), severity: z.enum(["low", "medium", "high"]), file: z.string(), line: z.number().int().positive().nullable(), message: z.string(), suggestion: z.string(), category: z.enum(["xml", "javascript"]) }); const outputSchema = z.object({ sourceDir: z.string(), scanned: z.object({ files: z.number().int().nonnegative(), xmlFiles: z.number().int().nonnegative(), jsFiles: z.number().int().nonnegative() }), summary: z.object({ totalFindings: z.number().int().nonnegative(), bySeverity: z.object({ low: z.number().int().nonnegative(), medium: z.number().int().nonnegative(), high: z.number().int().nonnegative() }), byRule: z.record(z.number().int().nonnegative()), truncated: z.boolean() }), findings: z.array(findingSchema) }); - src/tools/index.js:17-17 (registration)Import of analyzeUi5PerformanceTool from the ui5/analyzePerformance.js module.
import { analyzeUi5PerformanceTool } from "./ui5/analyzePerformance.js"; - src/tools/index.js:63-63 (registration)Registration of analyzeUi5PerformanceTool in the allTools export array, making it available to the MCP server.
analyzeUi5PerformanceTool, - src/utils/xmlParser.js:41-151 (helper)analyzeUi5Xml function that parses UI5 XML content to extract controls, bindings, events, and models - used by the handler for XML performance analysis.
export function analyzeUi5Xml(code) { if (typeof code !== "string" || code.trim().length === 0) { throw new ToolError("XML content must be a non-empty string.", { code: "INVALID_XML_INPUT" }); } const validation = XMLValidator.validate(code); if (validation !== true) { const detail = validation?.err ?? {}; throw new ToolError(`Invalid UI5 XML: ${detail.msg ?? "Unknown parse error."}`, { code: "UI5_XML_PARSE_ERROR", details: { line: detail.line ?? null, col: detail.col ?? null } }); } let parsed; try { parsed = parser.parse(code); } catch (error) { throw new ToolError(`Unable to parse UI5 XML: ${error.message}`, { code: "UI5_XML_PARSE_ERROR" }); } const rootEntry = findRootEntry(parsed); if (!rootEntry) { throw new ToolError("Unable to detect XML root element.", { code: "UI5_XML_ROOT_NOT_FOUND" }); } const [rootTag, rootNode] = rootEntry; const namespaces = {}; const controls = []; const bindings = []; const events = []; const models = new Set(); walkElement(rootTag, rootNode, rootTag); return { documentType: detectDocumentType(rootTag), rootTag, namespaces, controls, bindings, events, models: Array.from(models) }; function walkElement(tagName, node, currentPath) { if (!node || typeof node !== "object") { return; } const attributes = extractAttributes(node); registerNamespaces(attributes, namespaces); const { prefix, localName } = splitTag(tagName); controls.push({ tag: tagName, localName, namespacePrefix: prefix, path: currentPath }); for (const [attrName, rawValue] of Object.entries(attributes)) { if (typeof rawValue !== "string") { continue; } const bindingExpressions = extractBindingExpressions(rawValue); for (const expression of bindingExpressions) { const parsedBinding = parseBindingExpression(expression); bindings.push({ path: currentPath, tag: tagName, attribute: attrName, expression, type: parsedBinding.type, model: parsedBinding.model, bindingPath: parsedBinding.bindingPath }); if (parsedBinding.model) { models.add(parsedBinding.model); } } if (isEventAttribute(attrName, rawValue, bindingExpressions.length > 0)) { events.push({ path: currentPath, tag: tagName, event: attrName, handler: rawValue.trim() }); } } for (const [childTag, childValue] of Object.entries(node)) { if (isAttributeKey(childTag) || childTag === "#text" || childTag === "#cdata-section") { continue; } for (const childNode of normalizeNodeCollection(childValue)) { walkElement(childTag, childNode, `${currentPath}/${childTag}`); } } } }