forked from mirrors/gecko-dev
# ignore-this-changeset Differential Revision: https://phabricator.services.mozilla.com/D35940 --HG-- extra : source : d214f0c82813e5a8d3987debc490a2c11f1308ff
261 lines
8.6 KiB
JavaScript
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;
|
|
},
|
|
};
|