UNPKG

angular-null-error-analyzer

Version:

CLI tool to detect potential null/undefined access and Angular template binding issues.

110 lines (97 loc) 3.37 kB
const fs = require("fs"); const parse5 = require("parse5"); /** * Analyze HTML file for unsafe bindings * @param {string} filePath * @param {string[]} safeObjects - list of object names to ignore, e.g., ["loginForm", "fb"] * @param {string[]} ignoreBindings - list of attribute bindings to ignore, e.g., ["disabled"] * @returns {Array} issues */ function analyzeHtmlFile( filePath, safeObjects = [], ignoreBindings = ["disabled"] ) { const src = fs.readFileSync(filePath, "utf8"); const document = parse5.parseFragment(src, { sourceCodeLocationInfo: true }); const issues = []; function walk(node) { if (!node) return; // Check interpolation: {{ expr }} if (node.nodeName === "#text" && node.value) { const matches = node.value.match(/{{([^}]+)}}/g); if (matches) { matches.forEach((m) => { const expr = m.replace(/^{{\s*/, "").replace(/\s*}}$/, ""); const base = expr .replace(/^this\./, "") .split(".")[0] .trim(); if ( /\w+\./.test(expr) && !expr.includes("?.") && !safeObjects.includes(base) ) { issues.push({ file: filePath, kind: "TemplateBinding", message: `Interpolation without guard: {{ ${expr} }}`, suggestion: `Use optional chaining in template: {{ ${expr.replace( /\./g, "?." )} }}`, severity: "warning", line: (node.sourceCodeLocation && node.sourceCodeLocation.startLine) || null, }); } }); } } // Check attribute bindings: [value]="expr", (click)="expr", etc. if (node.attrs && node.attrs.length) { node.attrs.forEach((attr) => { // Normalize attribute name by removing brackets/parentheses const cleanName = attr.name.replace(/[\[\]\(\)]/g, ""); // Skip ignored bindings immediately if (ignoreBindings.includes(cleanName)) return; // Only process bindings if ( /^\[.*\]|\(.*\)|\[\(.*\)\]/.test(attr.name) || attr.name.startsWith("*") ) { const val = attr.value || ""; const base = val .replace(/^this\./, "") .split(".")[0] .trim(); if ( /\w+\./.test(val) && !val.includes("?.") && !safeObjects.includes(base) ) { issues.push({ file: filePath, kind: "TemplateBinding", message: `Binding without guard: ${attr.name}='${val}'`, suggestion: `Use optional chaining: ${val.replace(/\./g, "?.")}`, severity: "warning", line: (node.sourceCodeLocation && node.sourceCodeLocation.startLine) || null, }); } } }); } // Recurse into child nodes if (node.childNodes && node.childNodes.length) node.childNodes.forEach(walk); } if (document.childNodes) document.childNodes.forEach(walk); return issues; } module.exports = { analyzeHtmlFile };