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
JavaScript
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 };