I was following Create Plugins - ESLint - Pluggable JavaScript Linter guide and Custom Rule Tutorial - ESLint - Pluggable JavaScript Linter to learn how to build a plugin. In my use case, which turned out to be a foolish endeavour but a great learning experience, I created a plugin that checks against window.__coverage__ assignment in code.

It’s not a useful plugin because that only happens in built code and checking against it doesn’t work like this but here are my notes of how to build one using this use case.

Create a repository

Create a new npm/yarn project:

mkdir eslint-plugin-coverage
cd eslint-plugin-coverage
npm init -y

In package.json, define a name (ESLint plugin naming conventions recommend using eslint-plugin-[your plugin name] convention) and a version.

Make both ESM and CommonJS work

In package.json, define both "main": "eslint-plugin-coverage.cjs" and "module": "eslint-plugin-coverage.mjs" so they can be imported into either one.

Note

Jussi Kinnula shared How to Create a Hybrid NPM Module for ESM and CommonJS. | SenseDeep as a guide to creating hybrid modules but I haven’t read it yet.

Once I’ve read it, I should update this note (👨🏻‍🎓)

Plugin wrapper

To create this entry point for a plugin, we import it and export a default object with at least rules key that defines the rules.

In eslint-plugin-coverage.cjs:

const noInstrumentedCode = require("./no-coverage.cjs");
 
const plugin = {
  meta: { name: "eslint-plugin-coverage" },
  rules: { "no-instrumented-code": noInstrumentedCode },
};
 
module.exports = plugin;

For each rule, create files (for both ESM and CommonJS), in this case no-coverage.cjs.

meta object is explained in docs and create(context) is the entry point to the rule itself.

module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "Prevent window.__coverage__ assignment",
    },
  },
  create(context) {
    return {
      MemberExpression(node) {
        if (node.object.name === "m" && node.property.name == "__coverage__") {
          context.report({
            node,
            message: "No __coverage__ allowed in code",
          });
        }
      },
    };
  },
};

Inside the create(context), you can define the rule logic.

This is documented at Custom Rule Tutorial # Step 4

Here I use MemberExpression which targets any kind of foo.bar expression.

For code such as window.__coverage__, the node looks like this:

Node {
  type: 'MemberExpression',
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 19 }
  },
  range: [ 0, 19 ],
  object: Node {
    type: 'Identifier',
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 0, 6 ],
    name: 'window',
    parent: [Circular *1]
  },
  property: Node {
    type: 'Identifier',
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 7, 19 ],
    name: '__coverage__',
    parent: [Circular *1]
  },
  computed: false,
  parent: <ref *2> Node {
    type: 'AssignmentExpression',
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 0, 27 ],
    operator: '=',
    left: [Circular *1],
    right: Node {
      type: 'Literal',
      loc: [SourceLocation],
      range: [Array],
      value: 'foo',
      raw: "'foo'",
      parent: [Circular *2]
    },
    parent: Node {
      type: 'ExpressionStatement',
      loc: [SourceLocation],
      range: [Array],
      expression: [Circular *2],
      parent: [Node]
    }
  }
}

and context.report() is used to report problems that the rule picks up. These then trigger the ESLint itself to report them.

Testing the rule

To test this rule within the package, create no-instrumented-code.test.mjs:

import { RuleTester } from "eslint";
import noInstrumentedCode from "./no-coverage.mjs";
 
const ruleTester = new RuleTester({
  languageOptions: { ecmaVersion: 2015 },
});
 
ruleTester.run("no-instrumented-code", noInstrumentedCode, {
  valid: [
    {
      code: "const foo = 'bar';",
    },
    {
      code: "custom.__coverage__ = 'Hello world'",
    },
  ],
  invalid: [
    {
      code: "window.__coverage__ = 'foo'",
      errors: 1,
    },
  ],
});
 
console.log("All tests passed");

Here we import the rule(s) we want to test and run these with examples of valid code (that should not report a problem) and invalid code (that should report a problem).

Then add

"scripts": {
	"test": "node no-instrumented-code.test.mjs"
}

and run the tests with npm test.

Installing and using

To install the plugin locally in an external project, add

"eslint-plugin-coverage": "/path/to/eslint-plugin-coverage"

and in .eslintrc.js add it to

"plugins": ["eslint-plugin-coverage"]

and choose any rules:

"rules": { "coverage/no-instrumented-test": 'error' }

Note

If you use a n amespaced package (like "@juhis/eslint-plugin-coverage": "/path/to/eslint-plugin-coverage"), the reduced namespace in rules becomes "@juhis/coverage"