summaryrefslogtreecommitdiffstats
path: root/tools/lint/eslint/eslint-plugin-mozilla/lib/processors
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /tools/lint/eslint/eslint-plugin-mozilla/lib/processors
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tools/lint/eslint/eslint-plugin-mozilla/lib/processors')
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js120
-rw-r--r--tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js262
2 files changed, 382 insertions, 0 deletions
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js
new file mode 100644
index 0000000000..0f56debe1b
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/processor-helpers.js
@@ -0,0 +1,120 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+let sax = require("sax");
+
+// Converts sax's error message to something that eslint will understand
+let errorRegex = /(.*)\nLine: (\d+)\nColumn: (\d+)\nChar: (.*)/;
+function parseError(err) {
+ let matches = err.message.match(errorRegex);
+ if (!matches) {
+ return null;
+ }
+
+ return {
+ fatal: true,
+ message: matches[1],
+ line: parseInt(matches[2]) + 1,
+ column: parseInt(matches[3]),
+ };
+}
+
+let entityRegex = /&[\w][\w-\.]*;/g;
+
+// A simple sax listener that generates a tree of element information
+function XMLParser(text) {
+ // Non-strict allows us to ignore many errors from entities and
+ // preprocessing at the expense of failing to report some XML errors.
+ // Unfortunately it also throws away the case of tagnames and attributes
+ let parser = sax.parser(false, {
+ lowercase: true,
+ xmlns: true,
+ });
+
+ parser.onerror = function(err) {
+ this.lastError = parseError(err);
+ };
+
+ this.parser = parser;
+ parser.onopentag = this.onOpenTag.bind(this);
+ parser.onclosetag = this.onCloseTag.bind(this);
+ parser.ontext = this.onText.bind(this);
+ parser.onopencdata = this.onOpenCDATA.bind(this);
+ parser.oncdata = this.onCDATA.bind(this);
+ parser.oncomment = this.onComment.bind(this);
+
+ this.document = {
+ local: "#document",
+ uri: null,
+ children: [],
+ comments: [],
+ };
+ this._currentNode = this.document;
+
+ parser.write(text);
+}
+
+XMLParser.prototype = {
+ parser: null,
+
+ lastError: null,
+
+ onOpenTag(tag) {
+ let node = {
+ parentNode: this._currentNode,
+ local: tag.local,
+ namespace: tag.uri,
+ attributes: {},
+ children: [],
+ comments: [],
+ textContent: "",
+ textLine: this.parser.line,
+ textColumn: this.parser.column,
+ textEndLine: this.parser.line,
+ };
+
+ for (let attr of Object.keys(tag.attributes)) {
+ if (tag.attributes[attr].uri == "") {
+ node.attributes[attr] = tag.attributes[attr].value;
+ }
+ }
+
+ this._currentNode.children.push(node);
+ this._currentNode = node;
+ },
+
+ onCloseTag(tagname) {
+ this._currentNode.textEndLine = this.parser.line;
+ this._currentNode = this._currentNode.parentNode;
+ },
+
+ addText(text) {
+ this._currentNode.textContent += text;
+ },
+
+ onText(text) {
+ // Replace entities with some valid JS token.
+ this.addText(text.replace(entityRegex, "null"));
+ },
+
+ onOpenCDATA() {
+ // Turn the CDATA opening tag into whitespace for indent alignment
+ this.addText(" ".repeat("<![CDATA[".length));
+ },
+
+ onCDATA(text) {
+ this.addText(text);
+ },
+
+ onComment(text) {
+ this._currentNode.comments.push(text);
+ },
+};
+
+module.exports = {
+ XMLParser,
+};
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js
new file mode 100644
index 0000000000..c486b38e06
--- /dev/null
+++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js
@@ -0,0 +1,262 @@
+/**
+ * @fileoverview Converts inline attributes from XUL into JS
+ * functions
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+let path = require("path");
+let fs = require("fs");
+
+let XMLParser = require("./processor-helpers").XMLParser;
+
+// Stores any XML parse error
+let xmlParseError = null;
+
+// Stores the lines of JS code generated from the XUL.
+let scriptLines = [];
+// Stores a map from the synthetic line number to the real line number
+// and column offset.
+let lineMap = [];
+
+let includedRanges = [];
+
+// Deal with ifdefs. This is the state we pretend to have:
+const kIfdefStateForLinting = {
+ MOZ_UPDATER: true,
+ XP_WIN: true,
+ MOZ_BUILD_APP_IS_BROWSER: true,
+ MOZ_SERVICES_SYNC: true,
+ MOZ_DATA_REPORTING: true,
+ MOZ_TELEMETRY_REPORTING: true,
+ MOZ_CRASHREPORTER: true,
+ MOZ_NORMANDY: true,
+ MOZ_MAINTENANCE_SERVICE: true,
+ HAVE_SHELL_SERVICE: true,
+ MENUBAR_CAN_AUTOHIDE: true,
+ MOZILLA_OFFICIAL: true,
+};
+
+// Anything not in the above list is assumed false.
+function dealWithIfdefs(text, filename) {
+ function stripIfdefsFromLines(input, innerFile) {
+ let outputLines = [];
+ let inSkippingIfdef = [false];
+ for (let i = 0; i < input.length; i++) {
+ let line = input[i];
+ let shouldSkip = inSkippingIfdef.some(x => x);
+ if (!line.startsWith("#")) {
+ outputLines.push(shouldSkip ? "" : line);
+ } else {
+ if (
+ line.startsWith("# ") ||
+ line.startsWith("#filter") ||
+ line == "#" ||
+ line.startsWith("#define")
+ ) {
+ outputLines.push("");
+ continue;
+ }
+ // if this isn't just a comment (which we skip), figure out what to do:
+ let term = "";
+ let negate = false;
+ if (line.startsWith("#ifdef")) {
+ term = line.match(/^#ifdef *([A-Z_]+)/);
+ } else if (line.startsWith("#ifndef")) {
+ term = line.match(/^#ifndef *([A-Z_]+)/);
+ negate = true;
+ } else if (line.startsWith("#if ")) {
+ term = line.match(/^defined\(([A-Z_]+)\)/);
+ } else if (line.startsWith("#elifdef")) {
+ // Replace the old one:
+ inSkippingIfdef.pop();
+ term = line.match(/^#elifdef *([A-Z_]+)/);
+ } else if (line.startsWith("#else")) {
+ // Switch the last one around:
+ let old = inSkippingIfdef.pop();
+ inSkippingIfdef.push(!old);
+ outputLines.push("");
+ } else if (line.startsWith("#endif")) {
+ inSkippingIfdef.pop();
+ outputLines.push("");
+ } else if (line.startsWith("#expand")) {
+ // Just strip expansion instructions
+ outputLines.push(line.substring("#expand ".length));
+ } else if (line.startsWith("#include")) {
+ // Uh oh.
+ if (!shouldSkip) {
+ let fileToInclude = line.substr("#include ".length).trim();
+ let subpath = path.join(path.dirname(innerFile), fileToInclude);
+ let contents = fs.readFileSync(subpath, { encoding: "utf-8" });
+ contents = contents.split(/\n/);
+ // Recurse:
+ contents = stripIfdefsFromLines(contents, subpath);
+ if (innerFile == filename) {
+ includedRanges.push({
+ start: i,
+ end: i + contents.length,
+ filename: subpath,
+ });
+ }
+ // And insert the resulting lines:
+ input = input.slice(0, i).concat(contents, input.slice(i + 1));
+ // Re-process this line now that we've spliced things in.
+ i--;
+ } else {
+ outputLines.push("");
+ }
+ } else {
+ throw new Error("Unknown preprocessor directive: " + line);
+ }
+
+ if (term) {
+ // We always want the first capturing subgroup:
+ term = term && term[1];
+ if (!negate) {
+ inSkippingIfdef.push(!kIfdefStateForLinting[term]);
+ } else {
+ inSkippingIfdef.push(kIfdefStateForLinting[term]);
+ }
+ outputLines.push("");
+ // Now just continue; we'll include lines depending on the state of `inSkippingIfdef`.
+ }
+ }
+ }
+ return outputLines;
+ }
+ let lines = text.split(/\n/);
+ return stripIfdefsFromLines(lines, filename).join("\n");
+}
+
+function addSyntheticLine(line, linePos, addDisableLine) {
+ lineMap[scriptLines.length] = { line: linePos };
+ scriptLines.push(line + (addDisableLine ? "" : " // eslint-disable-line"));
+}
+
+function recursiveExpand(node) {
+ for (let [attr, value] of Object.entries(node.attributes)) {
+ if (attr.startsWith("on")) {
+ if (attr == "oncommand" && value == ";") {
+ // Ignore these, see bug 371900 for why people might do this.
+ continue;
+ }
+ // Ignore dashes in the tag name
+ let nodeDesc = node.local.replace(/-/g, "");
+ if (node.attributes.id) {
+ nodeDesc += "_" + node.attributes.id.replace(/[^a-z]/gi, "_");
+ }
+ if (node.attributes.class) {
+ nodeDesc += "_" + node.attributes.class.replace(/[^a-z]/gi, "_");
+ }
+ addSyntheticLine("function " + nodeDesc + "(event) {", node.textLine);
+ let processedLines = value.split(/\r?\n/);
+ let addlLine = 0;
+ for (let line of processedLines) {
+ line = line.replace(/^\s*/, "");
+ lineMap[scriptLines.length] = {
+ // Unfortunately, we only get a line number for the <tag> finishing,
+ // not for individual attributes.
+ line: node.textLine + addlLine,
+ };
+ scriptLines.push(line);
+ addlLine++;
+ }
+ addSyntheticLine("}", node.textLine + processedLines.length - 1);
+ }
+ }
+ for (let kid of node.children) {
+ recursiveExpand(kid);
+ }
+}
+
+module.exports = {
+ preprocess(text, filename) {
+ if (filename.includes(".inc")) {
+ return [];
+ }
+ xmlParseError = null;
+ // The following rules are annoying in XUL.
+ // Indent because in multiline attributes it's impossible to understand for the XML parser.
+ // Semicolons because those shouldn't be required for inline event handlers.
+ // Quotes because we use doublequotes for attributes so using single quotes
+ // for strings inside them makes sense.
+ // No-undef because it's a bunch of work to teach this code how to read
+ // scripts and get globals from them (though ideally we should do that at some point).
+ scriptLines = [
+ "/* eslint-disable indent */",
+ "/* eslint-disable indent-legacy */",
+ "/* eslint-disable semi */",
+ "/* eslint-disable quotes */",
+ "/* eslint-disable no-undef */",
+ ];
+ lineMap = scriptLines.map(() => ({ line: 0 }));
+ includedRanges = [];
+ // Do C-style preprocessing first:
+ text = dealWithIfdefs(text, filename);
+
+ let xp = new XMLParser(text);
+ if (xp.lastError) {
+ xmlParseError = xp.lastError;
+ }
+ let doc = xp.document;
+ if (!doc) {
+ return [];
+ }
+ let node = doc;
+ for (let kid of node.children) {
+ recursiveExpand(kid);
+ }
+
+ let scriptText = scriptLines.join("\n") + "\n";
+ return [scriptText];
+ },
+
+ postprocess(messages, filename) {
+ // If there was an XML parse error then just return that
+ if (xmlParseError) {
+ return [xmlParseError];
+ }
+
+ // For every message from every script block update the line to point to the
+ // correct place.
+ let errors = [];
+ for (let i = 0; i < messages.length; i++) {
+ for (let message of messages[i]) {
+ // ESLint indexes lines starting at 1 but our arrays start at 0
+ let mapped = lineMap[message.line - 1];
+ // Ensure we don't modify this by making a copy. We might need it for another failure.
+ let target = mapped.line;
+ let includedRange = includedRanges.find(
+ r => target >= r.start && target <= r.end
+ );
+ // If this came from an #included file, indicate this in the message
+ if (includedRange) {
+ target = includedRange.start;
+ message.message +=
+ " (from included file " +
+ path.basename(includedRange.filename) +
+ ")";
+ }
+ // Compensate for line numbers shifting as a result of #include:
+ let includeBallooning = includedRanges
+ .filter(r => target >= r.end)
+ .map(r => r.end - r.start)
+ .reduce((acc, next) => acc + next, 0);
+ target -= includeBallooning;
+ // Add back the 1 to go back to 1-indexing.
+ message.line = target + 1;
+
+ // We never have column information, unfortunately.
+ message.column = NaN;
+
+ errors.push(message);
+ }
+ }
+
+ return errors;
+ },
+};