diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /tools/lint/eslint/eslint-plugin-mozilla/lib/processors | |
parent | Initial commit. (diff) | |
download | firefox-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.js | 120 | ||||
-rw-r--r-- | tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js | 262 |
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; + }, +}; |