Skip to content

Commit

Permalink
Add eslint-plugin package
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit88 committed Mar 28, 2024
1 parent 7f07f5e commit a8508f7
Show file tree
Hide file tree
Showing 9 changed files with 1,405 additions and 405 deletions.
22 changes: 22 additions & 0 deletions packages/eslint-plugin/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use strict";

module.exports = {
root: true,
parserOptions: {
ecmaVersion: 'latest'
},
extends: [
"eslint:recommended",
"plugin:eslint-plugin/recommended",
"plugin:node/recommended",
],
env: {
node: true,
},
overrides: [
{
files: ["tests/**/*.js"],
env: { mocha: true },
},
]
};
2 changes: 2 additions & 0 deletions packages/eslint-plugin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/node_modules
dist
48 changes: 48 additions & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# eslint-plugin-opticks

Opticks

## Installation

You'll first need to install [ESLint](https://eslint.org/):

```sh
npm i eslint --save-dev
```

Next, install `eslint-plugin-opticks`:

```sh
npm install eslint-plugin-opticks --save-dev
```

## Usage

Add `opticks` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:

```json
{
"plugins": [
"opticks"
]
}
```


Then configure the rules you want to use under the rules section.

```json
{
"rules": {
"opticks/rule-name": 2
}
}
```

## Rules

<!-- begin auto-generated rules list -->
TODO: Run eslint-doc-generator to generate the rules list.
<!-- end auto-generated rules list -->


35 changes: 35 additions & 0 deletions packages/eslint-plugin/docs/rules/opticks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Opticks (`opticks`)

Please describe the origin of the rule here.

## Rule Details

This rule aims to...

Examples of **incorrect** code for this rule:

```js

// fill me in

```

Examples of **correct** code for this rule:

```js

// fill me in

```

### Options

If there are any options, describe them here. Otherwise, delete this section.

## When Not To Use It

Give a short description of when it would be appropriate to turn off this rule.

## Further Reading

If there are other links that describe the issue this rule addresses, please include them here in a bulleted list.
22 changes: 22 additions & 0 deletions packages/eslint-plugin/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* @fileoverview Opticks
* @author Jop
*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const requireIndex = require("requireindex");

//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------


// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + "/rules");



127 changes: 127 additions & 0 deletions packages/eslint-plugin/lib/rules/toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* @fileoverview Opticks
* @author Jop
*/
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Detects stale code from expired Opticks experiments",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: "code",
hasSuggestions: true,
schema: [], // Add a schema if the rule has options
messages: {
ExperimentNotConfigured:
"Looks like this experiment is not configured. Please make sure the experiment is added to the experiments config file.",
ExperimentConcluded:
"Looks like this experiment concluded, and can be cleaned up. The winning variant is {{winningVariant}}.",
AddWinningVariant:
"If the experiment is concluded, add the winning variant.",
VariableAssignment:
"It is okay to assign the result of a toggle to a variable, but you might be better off calling the toggle inline for automatic clean up.",
InvalidNrOfVariants:
"Invalid number of variants. Toggles require either 0, 2, or more variants.",
AddNullBVariant:
"If the b side is not supposed to do anything, add a null value.",
},
},

create(context) {
// QUESTION it seems the test runner reads from `context.settings`
// while .eslintrc.js reads from `settings`
const settings = context.settings || settings;
const { opticks } = settings;
// variables should be defined here

//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------

// any helper functions should go here or else delete this section

//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------

return {
CallExpression: (node) => {
const {
callee: { name },
} = node;
// TODO: look for imported toggles from opticks only
if (name === "toggle") {
if (node.arguments.length === 2) {
return context.report({
messageId: "InvalidNrOfVariants",
node,
suggest: [
{
messageId: "AddNullBVariant",
fix: (fixer) => {
const { range } = node;
// TODO: How to work with multilines
return fixer.insertTextBeforeRange(
[range[1] - 1],
", null"
);
},
},
],
});
}
// Clean up
const winningVariant = opticks.experiments[node.arguments[0].value];

if (typeof winningVariant === 'undefined') {
return context.report({
messageId: "ExperimentNotConfigured",
node
});
}

// TODO: Support unlimited amount of arguments
// TODO: Support arrow function replacement
const winningVariantIndex = winningVariant === "a" ? 1 : 2;
const winningVariantContent = node.arguments[winningVariantIndex].raw;

if (typeof winningVariant === "string") {
return context.report({
messageId: "ExperimentConcluded",
data: { winningVariant },
node,
suggests: [
{
messageId: "AddWinningVariant",
fix: (fixer) => {
// TODO: Add tests
console.log(winningVariantContent);
return fixer.replaceText(node, winningVariantContent);
},
}
],
});
}

// Discourage variable assignment
if (node.parent.type === "VariableDeclarator") {
return context.report({
messageId: "VariableAssignment",
node,
});
}
}
},
// visitor functions for different types of nodes
};
},
};
38 changes: 38 additions & 0 deletions packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "eslint-plugin-opticks",
"version": "0.0.1",
"description": "Opticks",
"keywords": [
"eslint",
"eslintplugin",
"eslint-plugin"
],
"author": "Jop de Klein",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"scripts": {
"lint": "npm-run-all \"lint:*\"",
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
"lint:js": "eslint .",
"test": "mocha tests --recursive",
"update:eslint-docs": "eslint-doc-generator"
},
"dependencies": {
"requireindex": "^1.2.0"
},
"devDependencies": {
"eslint": "^8.19.0",
"eslint-doc-generator": "^1.0.0",
"eslint-plugin-eslint-plugin": "^5.0.0",
"eslint-plugin-node": "^11.1.0",
"mocha": "^10.0.0",
"npm-run-all": "^4.1.5"
},
"engines": {
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
},
"peerDependencies": {
"eslint": ">=7"
},
"license": "ISC"
}
91 changes: 91 additions & 0 deletions packages/eslint-plugin/tests/lib/rules/toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* @fileoverview Opticks
* @author Jop
*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require("../../../lib/rules/toggle"),
RuleTester = require("eslint").RuleTester;

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

RuleTester.setDefaultConfig({
settings: {
opticks: { experiments: { foo: "a", bar: undefined, baz: "b" } },
},
});
const ruleTester = new RuleTester();

ruleTester.run("toggle", rule, {
valid: [
{ code: "toggle('bar', 'a', 'b')" },
{ code: "toggle('nonexistent', 'a', 'b')" },
],
invalid: [
{
code: "toggle('foo', 'a', 'b')",
errors: [
{
message:
"Looks like this experiment concluded, and can be cleaned. The winning variant is a.",
type: "CallExpression",
},
],
output: "'a'",
},
{
code: "toggle('baz', 'a', 'b')",
errors: [
{
messageId: "ExperimentConcluded",
type: "CallExpression",
},
],
output: "'b'",
},
{
// TODO: make tests work with const too
code: "var intermediateVariable = toggle('bar', 'a', 'b')",
errors: [
{
messageId: "VariableAssignment",
type: "CallExpression",
},
],
output: null,
},
{
code: "toggle('bar', 'a')",
errors: [
{
messageId: "InvalidNrOfVariants",
type: "CallExpression",
suggestions: [
{
messageId: "AddNullBVariant",
output: "toggle('bar', 'a', null)",
},
],
},
],
output: null,
},
{
code: `var Foo = styled('div')\`
display: flex;
\${toggle("foo", "a", "b")}
\`
`,
errors: [
"This toggle is not called from a function, this might not be what you want to do because it might execute before Opticks received the user id. Is this intended?",
],
output: null,
},
],
});
Loading

0 comments on commit a8508f7

Please sign in to comment.