зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1812977 - Move ESLint functions for globals handling from helpers.js to globals.js. r=Gijs
This is a better location for these functions and helps avoid circular dependencies in the next patches. Differential Revision: https://phabricator.services.mozilla.com/D168067
This commit is contained in:
Родитель
385fa57278
Коммит
cc23ce6654
|
@ -13,6 +13,40 @@ const fs = require("fs");
|
||||||
const helpers = require("./helpers");
|
const helpers = require("./helpers");
|
||||||
const htmlparser = require("htmlparser2");
|
const htmlparser = require("htmlparser2");
|
||||||
|
|
||||||
|
const callExpressionDefinitions = [
|
||||||
|
/^loader\.lazyGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^loader\.lazyServiceGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^loader\.lazyRequireGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineLazyModuleGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^ChromeUtils\.defineModuleGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineLazyProxy\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^XPCOMUtils\.defineConstant\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^DevToolsUtils\.defineLazyModuleGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^DevToolsUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^Object\.defineProperty\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^Reflect\.defineProperty\((?:globalThis|this), "(\w+)"/,
|
||||||
|
/^this\.__defineGetter__\("(\w+)"/,
|
||||||
|
];
|
||||||
|
|
||||||
|
const callExpressionMultiDefinitions = [
|
||||||
|
"XPCOMUtils.defineLazyGlobalGetters(this,",
|
||||||
|
"XPCOMUtils.defineLazyGlobalGetters(globalThis,",
|
||||||
|
"XPCOMUtils.defineLazyModuleGetters(this,",
|
||||||
|
"XPCOMUtils.defineLazyModuleGetters(globalThis,",
|
||||||
|
"XPCOMUtils.defineLazyServiceGetters(this,",
|
||||||
|
"XPCOMUtils.defineLazyServiceGetters(globalThis,",
|
||||||
|
"ChromeUtils.defineESModuleGetters(this,",
|
||||||
|
"ChromeUtils.defineESModuleGetters(globalThis,",
|
||||||
|
"loader.lazyRequireGetter(this,",
|
||||||
|
"loader.lazyRequireGetter(globalThis,",
|
||||||
|
];
|
||||||
|
|
||||||
|
const workerImportFilenameMatch = /(.*\/)*((.*?)\.jsm?)/;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a list of "name:boolean_value" or/and "name" options divided by comma
|
* Parses a list of "name:boolean_value" or/and "name" options divided by comma
|
||||||
* or whitespace.
|
* or whitespace.
|
||||||
|
@ -72,6 +106,180 @@ var globalDiscoveryInProgressForFiles = new Set();
|
||||||
*/
|
*/
|
||||||
var lastHTMLGlobals = {};
|
var lastHTMLGlobals = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to convert an CallExpressions that look like module imports
|
||||||
|
* into global variable definitions.
|
||||||
|
*
|
||||||
|
* @param {Object} node
|
||||||
|
* The AST node to convert.
|
||||||
|
* @param {boolean} isGlobal
|
||||||
|
* True if the current node is in the global scope.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* An array of objects that contain details about the globals:
|
||||||
|
* - {String} name
|
||||||
|
* The name of the global.
|
||||||
|
* - {Boolean} writable
|
||||||
|
* If the global is writeable or not.
|
||||||
|
*/
|
||||||
|
function convertCallExpressionToGlobals(node, isGlobal) {
|
||||||
|
let express = node.expression;
|
||||||
|
if (
|
||||||
|
express.type === "CallExpression" &&
|
||||||
|
express.callee.type === "MemberExpression" &&
|
||||||
|
express.callee.object &&
|
||||||
|
express.callee.object.type === "Identifier" &&
|
||||||
|
express.arguments.length === 1 &&
|
||||||
|
express.arguments[0].type === "ArrayExpression" &&
|
||||||
|
express.callee.property.type === "Identifier" &&
|
||||||
|
express.callee.property.name === "importGlobalProperties"
|
||||||
|
) {
|
||||||
|
return express.arguments[0].elements.map(literal => {
|
||||||
|
return {
|
||||||
|
explicit: true,
|
||||||
|
name: literal.value,
|
||||||
|
writable: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let source;
|
||||||
|
try {
|
||||||
|
source = helpers.getASTSource(node);
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The definition matches below must be in the global scope for us to define
|
||||||
|
// a global, so bail out early if we're not a global.
|
||||||
|
if (!isGlobal) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let reg of callExpressionDefinitions) {
|
||||||
|
let match = source.match(reg);
|
||||||
|
if (match) {
|
||||||
|
return [{ name: match[1], writable: true, explicit: true }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) &&
|
||||||
|
node.expression.arguments[1]
|
||||||
|
) {
|
||||||
|
let arg = node.expression.arguments[1];
|
||||||
|
if (arg.type === "ObjectExpression") {
|
||||||
|
return arg.properties
|
||||||
|
.map(p => ({
|
||||||
|
name: p.type === "Property" && p.key.name,
|
||||||
|
writable: true,
|
||||||
|
explicit: true,
|
||||||
|
}))
|
||||||
|
.filter(g => g.name);
|
||||||
|
}
|
||||||
|
if (arg.type === "ArrayExpression") {
|
||||||
|
return arg.elements
|
||||||
|
.map(p => ({
|
||||||
|
name: p.type === "Literal" && p.value,
|
||||||
|
writable: true,
|
||||||
|
explicit: true,
|
||||||
|
}))
|
||||||
|
.filter(g => typeof g.name == "string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.expression.callee.type == "MemberExpression" &&
|
||||||
|
node.expression.callee.property.type == "Identifier" &&
|
||||||
|
node.expression.callee.property.name == "defineLazyScriptGetter"
|
||||||
|
) {
|
||||||
|
// The case where we have a single symbol as a string has already been
|
||||||
|
// handled by the regexp, so we have an array of symbols here.
|
||||||
|
return node.expression.arguments[1].elements.map(n => ({
|
||||||
|
name: n.value,
|
||||||
|
writable: true,
|
||||||
|
explicit: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to convert an AssignmentExpression into a global variable
|
||||||
|
* definition if it applies to `this` in the global scope.
|
||||||
|
*
|
||||||
|
* @param {Object} node
|
||||||
|
* The AST node to convert.
|
||||||
|
* @param {boolean} isGlobal
|
||||||
|
* True if the current node is in the global scope.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* An array of objects that contain details about the globals:
|
||||||
|
* - {String} name
|
||||||
|
* The name of the global.
|
||||||
|
* - {Boolean} writable
|
||||||
|
* If the global is writeable or not.
|
||||||
|
*/
|
||||||
|
function convertThisAssignmentExpressionToGlobals(node, isGlobal) {
|
||||||
|
if (
|
||||||
|
isGlobal &&
|
||||||
|
node.expression.left &&
|
||||||
|
node.expression.left.object &&
|
||||||
|
node.expression.left.object.type === "ThisExpression" &&
|
||||||
|
node.expression.left.property &&
|
||||||
|
node.expression.left.property.type === "Identifier"
|
||||||
|
) {
|
||||||
|
return [{ name: node.expression.left.property.name, writable: true }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to convert an ExpressionStatement to likely global variable
|
||||||
|
* definitions.
|
||||||
|
*
|
||||||
|
* @param {Object} node
|
||||||
|
* The AST node to convert.
|
||||||
|
* @param {boolean} isGlobal
|
||||||
|
* True if the current node is in the global scope.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
* An array of objects that contain details about the globals:
|
||||||
|
* - {String} name
|
||||||
|
* The name of the global.
|
||||||
|
* - {Boolean} writable
|
||||||
|
* If the global is writeable or not.
|
||||||
|
*/
|
||||||
|
function convertWorkerExpressionToGlobals(node, isGlobal, dirname) {
|
||||||
|
let results = [];
|
||||||
|
let expr = node.expression;
|
||||||
|
|
||||||
|
if (
|
||||||
|
node.expression.type === "CallExpression" &&
|
||||||
|
expr.callee &&
|
||||||
|
expr.callee.type === "Identifier" &&
|
||||||
|
expr.callee.name === "importScripts"
|
||||||
|
) {
|
||||||
|
for (var arg of expr.arguments) {
|
||||||
|
var match = arg.value && arg.value.match(workerImportFilenameMatch);
|
||||||
|
if (match) {
|
||||||
|
if (!match[1]) {
|
||||||
|
let filePath = path.resolve(dirname, match[2]);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
let additionalGlobals = module.exports.getGlobalsForFile(filePath);
|
||||||
|
results = results.concat(additionalGlobals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Import with relative/absolute path should explicitly use
|
||||||
|
// `import-globals-from` comment.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that returns found globals for given AST node types. Each prototype
|
* An object that returns found globals for given AST node types. Each prototype
|
||||||
* property should be named for a node type and accepts a node parameter and a
|
* property should be named for a node type and accepts a node parameter and a
|
||||||
|
@ -163,12 +371,9 @@ GlobalsForNode.prototype = {
|
||||||
// Note: We check the expression types here and only call the necessary
|
// Note: We check the expression types here and only call the necessary
|
||||||
// functions to aid performance.
|
// functions to aid performance.
|
||||||
if (node.expression.type === "AssignmentExpression") {
|
if (node.expression.type === "AssignmentExpression") {
|
||||||
globals = helpers.convertThisAssignmentExpressionToGlobals(
|
globals = convertThisAssignmentExpressionToGlobals(node, isGlobal);
|
||||||
node,
|
|
||||||
isGlobal
|
|
||||||
);
|
|
||||||
} else if (node.expression.type === "CallExpression") {
|
} else if (node.expression.type === "CallExpression") {
|
||||||
globals = helpers.convertCallExpressionToGlobals(node, isGlobal);
|
globals = convertCallExpressionToGlobals(node, isGlobal);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here we assume that if importScripts is set in the global scope, then
|
// Here we assume that if importScripts is set in the global scope, then
|
||||||
|
@ -177,7 +382,7 @@ GlobalsForNode.prototype = {
|
||||||
//
|
//
|
||||||
// If this is testing context without path, ignore import.
|
// If this is testing context without path, ignore import.
|
||||||
if (globalScope && globalScope.set.get("importScripts") && this.dirname) {
|
if (globalScope && globalScope.set.get("importScripts") && this.dirname) {
|
||||||
let workerDetails = helpers.convertWorkerExpressionToGlobals(
|
let workerDetails = convertWorkerExpressionToGlobals(
|
||||||
node,
|
node,
|
||||||
isGlobal,
|
isGlobal,
|
||||||
this.dirname
|
this.dirname
|
||||||
|
|
|
@ -18,40 +18,6 @@ const recommendedConfig = require("./configs/recommended");
|
||||||
var gRootDir = null;
|
var gRootDir = null;
|
||||||
var directoryManifests = new Map();
|
var directoryManifests = new Map();
|
||||||
|
|
||||||
const callExpressionDefinitions = [
|
|
||||||
/^loader\.lazyGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^loader\.lazyServiceGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^loader\.lazyRequireGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineLazyModuleGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^ChromeUtils\.defineModuleGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineLazyPreferenceGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineLazyProxy\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineLazyScriptGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineLazyServiceGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^XPCOMUtils\.defineConstant\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^DevToolsUtils\.defineLazyModuleGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^DevToolsUtils\.defineLazyGetter\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^Object\.defineProperty\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^Reflect\.defineProperty\((?:globalThis|this), "(\w+)"/,
|
|
||||||
/^this\.__defineGetter__\("(\w+)"/,
|
|
||||||
];
|
|
||||||
|
|
||||||
const callExpressionMultiDefinitions = [
|
|
||||||
"XPCOMUtils.defineLazyGlobalGetters(this,",
|
|
||||||
"XPCOMUtils.defineLazyGlobalGetters(globalThis,",
|
|
||||||
"XPCOMUtils.defineLazyModuleGetters(this,",
|
|
||||||
"XPCOMUtils.defineLazyModuleGetters(globalThis,",
|
|
||||||
"XPCOMUtils.defineLazyServiceGetters(this,",
|
|
||||||
"XPCOMUtils.defineLazyServiceGetters(globalThis,",
|
|
||||||
"ChromeUtils.defineESModuleGetters(this,",
|
|
||||||
"ChromeUtils.defineESModuleGetters(globalThis,",
|
|
||||||
"loader.lazyRequireGetter(this,",
|
|
||||||
"loader.lazyRequireGetter(globalThis,",
|
|
||||||
];
|
|
||||||
|
|
||||||
const workerImportFilenameMatch = /(.*\/)*((.*?)\.jsm?)/;
|
|
||||||
|
|
||||||
let xpidlData;
|
let xpidlData;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -251,182 +217,6 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to convert an ExpressionStatement to likely global variable
|
|
||||||
* definitions.
|
|
||||||
*
|
|
||||||
* @param {Object} node
|
|
||||||
* The AST node to convert.
|
|
||||||
* @param {boolean} isGlobal
|
|
||||||
* True if the current node is in the global scope.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
* An array of objects that contain details about the globals:
|
|
||||||
* - {String} name
|
|
||||||
* The name of the global.
|
|
||||||
* - {Boolean} writable
|
|
||||||
* If the global is writeable or not.
|
|
||||||
*/
|
|
||||||
convertWorkerExpressionToGlobals(node, isGlobal, dirname) {
|
|
||||||
var getGlobalsForFile = require("./globals").getGlobalsForFile;
|
|
||||||
|
|
||||||
let results = [];
|
|
||||||
let expr = node.expression;
|
|
||||||
|
|
||||||
if (
|
|
||||||
node.expression.type === "CallExpression" &&
|
|
||||||
expr.callee &&
|
|
||||||
expr.callee.type === "Identifier" &&
|
|
||||||
expr.callee.name === "importScripts"
|
|
||||||
) {
|
|
||||||
for (var arg of expr.arguments) {
|
|
||||||
var match = arg.value && arg.value.match(workerImportFilenameMatch);
|
|
||||||
if (match) {
|
|
||||||
if (!match[1]) {
|
|
||||||
let filePath = path.resolve(dirname, match[2]);
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
let additionalGlobals = getGlobalsForFile(filePath);
|
|
||||||
results = results.concat(additionalGlobals);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Import with relative/absolute path should explicitly use
|
|
||||||
// `import-globals-from` comment.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to convert an AssignmentExpression into a global variable
|
|
||||||
* definition if it applies to `this` in the global scope.
|
|
||||||
*
|
|
||||||
* @param {Object} node
|
|
||||||
* The AST node to convert.
|
|
||||||
* @param {boolean} isGlobal
|
|
||||||
* True if the current node is in the global scope.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
* An array of objects that contain details about the globals:
|
|
||||||
* - {String} name
|
|
||||||
* The name of the global.
|
|
||||||
* - {Boolean} writable
|
|
||||||
* If the global is writeable or not.
|
|
||||||
*/
|
|
||||||
convertThisAssignmentExpressionToGlobals(node, isGlobal) {
|
|
||||||
if (
|
|
||||||
isGlobal &&
|
|
||||||
node.expression.left &&
|
|
||||||
node.expression.left.object &&
|
|
||||||
node.expression.left.object.type === "ThisExpression" &&
|
|
||||||
node.expression.left.property &&
|
|
||||||
node.expression.left.property.type === "Identifier"
|
|
||||||
) {
|
|
||||||
return [{ name: node.expression.left.property.name, writable: true }];
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to convert an CallExpressions that look like module imports
|
|
||||||
* into global variable definitions.
|
|
||||||
*
|
|
||||||
* @param {Object} node
|
|
||||||
* The AST node to convert.
|
|
||||||
* @param {boolean} isGlobal
|
|
||||||
* True if the current node is in the global scope.
|
|
||||||
*
|
|
||||||
* @return {Array}
|
|
||||||
* An array of objects that contain details about the globals:
|
|
||||||
* - {String} name
|
|
||||||
* The name of the global.
|
|
||||||
* - {Boolean} writable
|
|
||||||
* If the global is writeable or not.
|
|
||||||
*/
|
|
||||||
convertCallExpressionToGlobals(node, isGlobal) {
|
|
||||||
let express = node.expression;
|
|
||||||
if (
|
|
||||||
express.type === "CallExpression" &&
|
|
||||||
express.callee.type === "MemberExpression" &&
|
|
||||||
express.callee.object &&
|
|
||||||
express.callee.object.type === "Identifier" &&
|
|
||||||
express.arguments.length === 1 &&
|
|
||||||
express.arguments[0].type === "ArrayExpression" &&
|
|
||||||
express.callee.property.type === "Identifier" &&
|
|
||||||
express.callee.property.name === "importGlobalProperties"
|
|
||||||
) {
|
|
||||||
return express.arguments[0].elements.map(literal => {
|
|
||||||
return {
|
|
||||||
explicit: true,
|
|
||||||
name: literal.value,
|
|
||||||
writable: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let source;
|
|
||||||
try {
|
|
||||||
source = this.getASTSource(node);
|
|
||||||
} catch (e) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// The definition matches below must be in the global scope for us to define
|
|
||||||
// a global, so bail out early if we're not a global.
|
|
||||||
if (!isGlobal) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let reg of callExpressionDefinitions) {
|
|
||||||
let match = source.match(reg);
|
|
||||||
if (match) {
|
|
||||||
return [{ name: match[1], writable: true, explicit: true }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
callExpressionMultiDefinitions.some(expr => source.startsWith(expr)) &&
|
|
||||||
node.expression.arguments[1]
|
|
||||||
) {
|
|
||||||
let arg = node.expression.arguments[1];
|
|
||||||
if (arg.type === "ObjectExpression") {
|
|
||||||
return arg.properties
|
|
||||||
.map(p => ({
|
|
||||||
name: p.type === "Property" && p.key.name,
|
|
||||||
writable: true,
|
|
||||||
explicit: true,
|
|
||||||
}))
|
|
||||||
.filter(g => g.name);
|
|
||||||
}
|
|
||||||
if (arg.type === "ArrayExpression") {
|
|
||||||
return arg.elements
|
|
||||||
.map(p => ({
|
|
||||||
name: p.type === "Literal" && p.value,
|
|
||||||
writable: true,
|
|
||||||
explicit: true,
|
|
||||||
}))
|
|
||||||
.filter(g => typeof g.name == "string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
node.expression.callee.type == "MemberExpression" &&
|
|
||||||
node.expression.callee.property.type == "Identifier" &&
|
|
||||||
node.expression.callee.property.name == "defineLazyScriptGetter"
|
|
||||||
) {
|
|
||||||
// The case where we have a single symbol as a string has already been
|
|
||||||
// handled by the regexp, so we have an array of symbols here.
|
|
||||||
return node.expression.arguments[1].elements.map(n => ({
|
|
||||||
name: n.value,
|
|
||||||
writable: true,
|
|
||||||
explicit: true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a variable to the current scope.
|
* Add a variable to the current scope.
|
||||||
* HACK: This relies on eslint internals so it could break at any time.
|
* HACK: This relies on eslint internals so it could break at any time.
|
||||||
|
|
Загрузка…
Ссылка в новой задаче