Skip to content

Commit

Permalink
Add @babel/eslint-plugin-development-internal (#11376)
Browse files Browse the repository at this point in the history
* Add @babel/eslint-plugin-internal

* Add dry-error-messages rule

* Address feedback

* Enable new rule

* fix author field

* Fix errors

* Add readme

* Add example configuration

* Handle directories

* run make bootstrap

* More updates!

* Fix errors

* Update tests

* Fix CI race condition
  • Loading branch information
kaicataldo committed Jun 22, 2020
1 parent beca7e2 commit 75c2300
Show file tree
Hide file tree
Showing 49 changed files with 1,088 additions and 74 deletions.
31 changes: 26 additions & 5 deletions .eslintrc.js
@@ -1,10 +1,17 @@
"use strict";

const path = require("path");

module.exports = {
root: true,
plugins: ["prettier", "@babel/development", "import", "jest"],
// replace it by `@babel/internal` when `@babel/eslint-config-internal` is published
extends: path.resolve(__dirname, "eslint/babel-eslint-config-internal"),
plugins: [
"import",
"jest",
"prettier",
"@babel/development",
"@babel/development-internal",
],
extends: "@babel/internal",
rules: {
"prettier/prettier": "error",
// TODO: remove after babel-eslint-config-internal is fully integrated into this repository.
Expand Down Expand Up @@ -44,8 +51,8 @@ module.exports = {
"jest/no-identical-title": "off",
"jest/no-standalone-expect": "off",
"jest/no-test-callback": "off",
"jest/valid-describe": "off"
}
"jest/valid-describe": "off",
},
},
{
files: ["packages/babel-plugin-*/src/index.js"],
Expand All @@ -55,5 +62,19 @@ module.exports = {
eqeqeq: ["error", "always", { null: "ignore" }],
},
},
{
files: ["packages/babel-parser/src/**/*.js"],
rules: {
"@babel/development-internal/dry-error-messages": [
"error",
{
errorModule: path.resolve(
__dirname,
"packages/babel-parser/src/parser/error.js"
),
},
],
},
},
],
};
14 changes: 7 additions & 7 deletions Makefile
Expand Up @@ -84,27 +84,27 @@ build-no-bundle-ci: bootstrap-only
watch: build-no-bundle
BABEL_ENV=development $(YARN) gulp watch

code-quality-ci: flowcheck-ci lint-ci
code-quality-ci: build-no-bundle-ci
$(MAKE) flowcheck-ci & $(MAKE) lint-ci

flowcheck-ci: bootstrap-flowcheck

flowcheck-ci:
$(MAKE) flow

code-quality: flow lint

flow:
$(YARN) flow check --strip-root

bootstrap-flowcheck: build-no-bundle-ci

lint-ci: lint-js-ci lint-ts-ci check-compat-data-ci

lint-js-ci: bootstrap-only
lint-js-ci:
$(MAKE) lint-js

lint-ts-ci: bootstrap-flowcheck
lint-ts-ci:
$(MAKE) lint-ts

check-compat-data-ci: build-no-bundle-ci
check-compat-data-ci:
$(MAKE) check-compat-data

lint: lint-js lint-ts
Expand Down
2 changes: 1 addition & 1 deletion eslint/babel-eslint-config-internal/index.js
@@ -1,7 +1,7 @@
"use strict";

module.exports = {
parser: "babel-eslint",
parser: "@babel/eslint-parser",
extends: "eslint:recommended",
plugins: ["flowtype"],
parserOptions: {
Expand Down
2 changes: 1 addition & 1 deletion eslint/babel-eslint-config-internal/package.json
Expand Up @@ -13,7 +13,7 @@
},
"main": "index.js",
"peerDependencies": {
"babel-eslint": "^10.0.0 || ^11.0.0-0",
"@babel/eslint-parser": "*",
"eslint-plugin-flowtype": "^3.0.0"
}
}
4 changes: 4 additions & 0 deletions eslint/babel-eslint-plugin-development-internal/.npmignore
@@ -0,0 +1,4 @@
src
test
.*
*.log
70 changes: 70 additions & 0 deletions eslint/babel-eslint-plugin-development-internal/README.md
@@ -0,0 +1,70 @@
# @babel/eslint-plugin-development-internal

The Babel team's custom ESLint rules for the babel/babel monorepo.

## Installation

```sh
$ npm install --save-dev @babel/eslint-plugin-development-internal
```
or
```sh
$ yarn add --save-dev @babel/eslint-plugin-development-internal
```

## Usage

The plugin can be loaded in your `.eslintrc.*` configuration file as follows: (note that you can omit the `eslint-plugin-` prefix):

```json
{
"plugins": ["@babel/development-internal"]
}
```

## Rules

### `@babel/development-internal/dry-error-messages`

Intended for use in `packages/babel-parser/src/**/*`. When enabled, this rule warns when `this.raise()` invocations raise errors that are not imported from a designated error module.

Accepts an object configuration option:

```ts
{
errorModule: string
}
```

`errorModule` (required): The rule expects either an absolute path or a module name (for a module in `node_modules`). Please note that the rule will not check anything if` errorModule` is not given.

Example configuration:

```js
{
rules: {
"@babel/development-internal/dry-error-messages": [
"error",
{
errorModule: "@babel/shared-error-messages"
}
]
}
}
```
and
```js
{
rules: {
"@babel/development-internal/dry-error-messages": [
"error",
{
errorModule: path.resolve(
__dirname,
"packages/shared-error-messages/lib/index.js"
)
}
]
}
}
```
36 changes: 36 additions & 0 deletions eslint/babel-eslint-plugin-development-internal/package.json
@@ -0,0 +1,36 @@
{
"name": "@babel/eslint-plugin-development-internal",
"version": "0.0.0",
"description": "The Babel Team's ESLint custom rules plugin. Since it's internal, it might not respect semver.",
"main": "lib/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/babel/babel.git",
"directory": "eslint/babel-eslint-plugin-development-internal"
},
"keywords": [
"babel",
"eslint",
"eslintplugin",
"eslint-plugin",
"babel-eslint"
],
"author": "Kai Cataldo <kai@kaicataldo.com>",
"license": "MIT",
"private": true,
"engines": {
"node": ">=10.9"
},
"bugs": {
"url": "https://github.com/babel/babel/issues"
},
"homepage": "https://github.com/babel/babel/tree/master/eslint/babel-eslint-plugin-development-internal",
"peerDependencies": {
"@babel/eslint-parser": "0.0.0",
"eslint": ">=6.0.0"
},
"devDependencies": {
"@babel/eslint-shared-fixtures": "*",
"eslint": "^6.0.0"
}
}
7 changes: 7 additions & 0 deletions eslint/babel-eslint-plugin-development-internal/src/index.js
@@ -0,0 +1,7 @@
import dryErrorMessages from "./rules/dry-error-messages";

module.exports = {
rules: {
"dry-error-messages": dryErrorMessages,
},
};
@@ -0,0 +1,148 @@
import path from "path";

const REL_PATH_REGEX = /^\.{1,2}/;

function isRelativePath(filePath) {
return REL_PATH_REGEX.test(filePath);
}

function resolveAbsolutePath(currentFilePath, moduleToResolve) {
return isRelativePath(moduleToResolve)
? path.resolve(path.dirname(currentFilePath), moduleToResolve)
: moduleToResolve;
}

function isSourceErrorModule(currentFilePath, targetModulePath, src) {
for (const srcPath of [src, `${src}.js`, `${src}/index`, `${src}/index.js`]) {
if (
path.normalize(resolveAbsolutePath(currentFilePath, targetModulePath)) ===
path.normalize(resolveAbsolutePath(currentFilePath, srcPath))
) {
return true;
}
}

return false;
}

function isCurrentFileErrorModule(currentFilePath, errorModule) {
return currentFilePath === errorModule;
}

function findIdNode(node) {
if (node.type === "Identifier") {
return node;
}

if (node.type === "MemberExpression" && node.object.type === "Identifier") {
return node.object;
}

return null;
}

function findReference(node, scope) {
let currentScope = scope;

while (currentScope) {
const ref = currentScope.set.get(node.name);

if (ref) {
return ref;
}

currentScope = currentScope.upper;
}

return null;
}

function referencesImportedBinding(node, scope, bindings) {
const ref = findReference(node, scope);

if (ref) {
const topLevelDef = ref.defs[0];

if (topLevelDef.type === "ImportBinding") {
const defNode = topLevelDef.node;

for (const spec of bindings) {
if (
spec.loc.start === defNode.loc.start &&
spec.loc.end === defNode.loc.end
) {
return true;
}
}
}
}

return false;
}

export default {
meta: {
type: "suggestion",
docs: {
description:
"enforce @babel/parser's error messages to be consolidated in one module",
},
schema: [
{
type: "object",
properties: {
errorModule: { type: "string" },
},
additionalProperties: false,
required: ["errorModule"],
},
],
messages: {
mustBeImported: 'Error messages must be imported from "{{errorModule}}".',
},
},
create({ options, report, getFilename, getScope }) {
const [{ errorModule = "" } = {}] = options;
const filename = getFilename();
const importedBindings = new Set();

if (
// Do not run check if errorModule config option is not given.
!errorModule.length ||
// Do not check the target error module file.
isCurrentFileErrorModule(filename, errorModule)
) {
return {};
}

return {
// Check imports up front so that we don't have to check them for every ThrowStatement.
ImportDeclaration(node) {
if (isSourceErrorModule(filename, errorModule, node.source.value)) {
for (const spec of node.specifiers) {
importedBindings.add(spec);
}
}
},
"CallExpression[callee.type='MemberExpression'][callee.object.type='ThisExpression'][callee.property.name='raise'][arguments.length>=2]"(
node,
) {
const [, errorMsgNode] = node.arguments;
const nodeToCheck = findIdNode(errorMsgNode);

if (
nodeToCheck &&
referencesImportedBinding(nodeToCheck, getScope(), importedBindings)
) {
return;
}

report({
node: errorMsgNode,
messageId: "mustBeImported",
data: { errorModule },
});
},
};
},
};

0 comments on commit 75c2300

Please sign in to comment.