Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @babel/eslint-plugin-development-internal #11376

Merged
merged 14 commits into from Jun 22, 2020
Merged
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 },
});
},
};
},
};