fune/tools/lint/eslint/eslint-plugin-mozilla/lib/processors/xul.js
Victor Porof 5c7cdbd4ba Bug 1561435 - Format tools/, a=automatic-formatting
# ignore-this-changeset

Differential Revision: https://phabricator.services.mozilla.com/D35940

--HG--
extra : source : d214f0c82813e5a8d3987debc490a2c11f1308ff
2019-07-05 11:18:19 +02:00

261 lines
8.6 KiB
JavaScript

/**
* @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 XBL
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_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;
},
};