Skip to content

Commit

Permalink
Add eslint plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Nov 18, 2018
1 parent 42985a7 commit d68c3db
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 162 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.json
Expand Up @@ -26,6 +26,13 @@
"env": {
"jest": true
}
},
{
"files": ["packages/babel-plugin-*/src/index.js"],
"excludedFiles": ["packages/babel-plugin-transform-regenerator/**/*.js"],
"rules": {
"local-rules/plugin-name": "error"
}
}
]
}
2 changes: 2 additions & 0 deletions eslint-local-rules.js
Expand Up @@ -2,8 +2,10 @@

const noDeprecatedClone = require("./scripts/eslint_rules/no-deprecated-clone");
const noUndefinedIdentifier = require("./scripts/eslint_rules/no-undefined-identifier");
const pluginName = require("./scripts/eslint_rules/plugin-name");

module.exports = {
"no-deprecated-clone": noDeprecatedClone,
"no-undefined-identifier": noUndefinedIdentifier,
"plugin-name": pluginName,
};
163 changes: 1 addition & 162 deletions scripts/eslint_rules/no-deprecated-clone.js
Expand Up @@ -2,168 +2,7 @@

"use strict";

function getVariableDefinition(name /*: string */, scope /*: Scope */) {
let currentScope = scope;
do {
const variable = currentScope.set.get(name);
if (variable && variable.defs[0]) {
return { scope: currentScope, definition: variable.defs[0] };
}
} while ((currentScope = currentScope.upper));
}

/*::
type ReferenceOriginImport = { kind: "import", source: string, name: string };
type ReferenceOriginParam = {
kind: "export param",
exportName: string,
index: number,
};
type ReferenceOrigin =
| ReferenceOriginImport
| ReferenceOriginParam
| { kind: "import *", source: string }
| {
kind: "property",
base: ReferenceOriginImport | ReferenceOriginParam,
path: string,
};
*/

// Given a node and a context, returns a description of where its value comes
// from.
// It resolves imports, parameters of exported functions and property accesses.
// See the ReferenceOrigin type for more informations.
function getReferenceOrigin(
node /*: Node */,
scope /*: Scope */
) /*: ?ReferenceOrigin */ {
if (node.type === "Identifier") {
const variable = getVariableDefinition(node.name, scope);
if (!variable) return null;

const definition = variable.definition;
const defNode = definition.node;

if (definition.type === "ImportBinding") {
if (defNode.type === "ImportSpecifier") {
return {
kind: "import",
source: definition.parent.source.value,
name: defNode.imported.name,
};
}
if (defNode.type === "ImportNamespaceSpecifier") {
return {
kind: "import *",
source: definition.parent.source.value,
};
}
}

if (definition.type === "Variable" && defNode.init) {
const origin = getReferenceOrigin(defNode.init, variable.scope);
return origin && patternToProperty(definition.name, origin);
}

if (definition.type === "Parameter") {
const parent = defNode.parent;
let exportName /*: string */;
if (parent.type === "ExportDefaultDeclaration") {
exportName = "default";
} else if (parent.type === "ExportNamedDeclaration") {
exportName = defNode.id.name;
} else if (
parent.type === "AssignmentExpression" &&
parent.left.type === "MemberExpression" &&
parent.left.object.type === "Identifier" &&
parent.left.object.name === "module" &&
parent.left.property.type === "Identifier" &&
parent.left.property.name === "exports"
) {
exportName = "module.exports";
} else {
return null;
}
return patternToProperty(definition.name, {
kind: "export param",
exportName,
index: definition.index,
});
}
}

if (node.type === "MemberExpression" && !node.computed) {
const origin = getReferenceOrigin(node.object, scope);
return origin && addProperty(origin, node.property.name);
}

return null;
}

function patternToProperty(
id /*: Node */,
base /*: ReferenceOrigin */
) /*: ?ReferenceOrigin */ {
const path = getPatternPath(id);
return path && path.reduce(addProperty, base);
}

// Adds a property to a given origin. If it was a namespace import it becomes
// a named import, so that `import * as x from "foo"; x.bar` and
// `import { bar } from "foo"` have the same origin.
function addProperty(
origin /*: ReferenceOrigin */,
name /*: string */
) /* ReferenceOrigin */ {
if (origin.kind === "import *") {
return {
kind: "import",
source: origin.source,
name,
};
}
if (origin.kind === "property") {
return {
kind: "property",
base: origin.base,
path: origin.path + "." + name,
};
}
return {
kind: "property",
base: origin,
path: name,
};
}

// if "node" is c of { a: { b: c } }, the result is ["a","b"]
function getPatternPath(node /*: Node */) /*: ?string[] */ {
let current = node;
const path = [];

// Unshift keys to path while going up
do {
const property = current.parent;
if (
property.type === "ArrayPattern" ||
property.type === "AssignmentPattern" ||
property.computed
) {
// These nodes are not supported.
return null;
}
if (property.type === "Property") {
path.unshift(property.key.name);
} else {
// The destructuring pattern is finished
break;
}
} while ((current = current.parent.parent));

return path;
}
const getReferenceOrigin = require("./utils/get-reference-origin");

function reportError(context /*: Context */, node /*: Node */) {
const isMemberExpression = node.type === "MemberExpression";
Expand Down
62 changes: 62 additions & 0 deletions scripts/eslint_rules/plugin-name.js
@@ -0,0 +1,62 @@
"use strict";

const getReferenceOrigin = require("./utils/get-reference-origin");

function reportNoPlugin(context /*: Context */, node /*: Node */) {
context.report({
node,
message: "This file does not export a Babel plugin",
});
}

function is(type /*: string */) /*: (node: Node) => boolean */ {
return node => node.type === type;
}

module.exports = {
meta: {
schema: [],
},
create(context /*: Context */) {
return {
Program(program /*: Node */) {
if (!program.body.some(is("ExportDefaultDeclaration"))) {
return reportNoPlugin(context, program);
}
},
ExportDefaultDeclaration(exportDefaultDecl) {
let plugin = exportDefaultDecl.declaration;

if (plugin.type === "CallExpression") {
// export default declare(api => { ... });
const origin = getReferenceOrigin(plugin.callee, context.getScope());

if (
origin &&
origin.kind === "import" &&
origin.name === "declare" &&
origin.source === "@babel/helper-plugin-utils"
) {
plugin = plugin.arguments[0];
}
}

if (!plugin.type.includes("Function")) {
return reportNoPlugin(context, exportDefaultDecl.parent);
}

const returnNode = plugin.body.body.find(is("ReturnStatement"));
if (!returnNode || returnNode.argument.type !== "ObjectExpression") {
return reportNoPlugin(context, exportDefaultDecl.parent);
}

if (!returnNode.argument.properties.some(p => p.key.name === "name")) {
context.report(
returnNode,
"This Babel plugin doesn't have a 'name' property."
);
}
},
};
},
};
34 changes: 34 additions & 0 deletions scripts/eslint_rules/utils/eslint-types.js
@@ -0,0 +1,34 @@
/*:: // ESLint types
type Node = { type: string, [string]: any };
type Definition = {
type: "ImportedBinding",
name: Node,
node: Node,
parent: Node,
};
type Variable = {
defs: Definition[],
};
type Scope = {
set: Map<string, Variable>,
upper: ?Scope,
};
type Context = {
report(options: {
node: Node,
message: string,
fix?: (fixer: Fixer) => ?Fixer,
}): void,
getScope(): Scope,
};
type Fixer = {
replaceText(node: Node, replacement: string): Fixer,
};
*/

0 comments on commit d68c3db

Please sign in to comment.