Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@

Enforce consistent and maintainable TODO comments.

![Screenshot](.github/assets/screenshot.png)
![A screenshot of ESLint output in the editor, displaying "TODO comment doesn't reference a ticket number. Ticket pattern: PROJ-\[0-9\]+""](.github/assets/screenshot.png)

## Installation

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

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

Next, install `eslint-plugin-todo-plz`:

```
$ npm install eslint-plugin-todo-plz --save-dev
```sh
npm install --save-dev eslint-plugin-todo-plz
```

## Usage
Expand All @@ -40,8 +40,14 @@ Then configure the rules you want to use under the rules section.

## Supported Rules

- [`ticket-ref`](docs/rules/ticket-ref.md)
<!-- begin auto-generated rules list -->

| Name | Description |
| :------------------------------------- | :--------------------------------------------- |
| [ticket-ref](docs/rules/ticket-ref.md) | Require a ticket reference in the TODO comment |

<!-- end auto-generated rules list -->

## Inspiration

- Shoutout [`expiring-todo-comments`](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/master/docs/rules/expiring-todo-comments.md) for showing me how to build my first ESLint rule.
- Shoutout to [`unicorn/expiring-todo-comments`](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/master/docs/rules/expiring-todo-comments.md) for showing me how to build my first ESLint rule.
45 changes: 35 additions & 10 deletions docs/rules/ticket-ref.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Require a ticket reference in the TODO comment (ticket-ref)
# Require a ticket reference in the TODO comment (`todo-plz/ticket-ref`)

Adding a `TODO` comment that will be addressed in the future should have a corresponding ticket (AKA issue) in the project backlog, so the team doesn't lose track of the pending work.
<!-- end auto-generated rule header -->

Adding a `TODO` comment that will be addressed in the future should have a corresponding ticket (AKA issue) in the project backlog, so the team doesn't lose track of pending work.

## Options

### `pattern`
### pattern

Type: `RegExp | string`

**This option is required**, and controls what the ticket pattern is to match against. Expects a regex string.

Expand Down Expand Up @@ -32,7 +36,9 @@ Examples of **correct** code for this rule when using the above options:
// TODO (PROJ-123): Connect to the API
```

### `terms`
### terms

Type: `Array<string>`

_Optional._ Change what terms to require the ticket reference on. Defaults to: `["TODO"]`

Expand Down Expand Up @@ -66,18 +72,35 @@ Examples of **correct** code for this rule when using the above options:

## Advanced options

### `commentPattern`
### commentPattern

_Optional._ Override the overall comment pattern that matches both term and ticket. When used, `term` and `pattern` options are ignored. Expects a regex string.
Type: `RegExp | string`

For example, let's say you expect a different comment pattern such as `TODO: [PROJ-123]`, you would configure this rule like:
_Optional._ Override the overall comment pattern that matches both term and ticket. When used, `term` and `pattern` options are ignored. Expects a regex, or a string that can be passed to `new RegExp`.

```json
For example, let's say you expect a different comment pattern such as `TODO: [PROJ-123]`, you would configure this rule like this:

```js
export default [
{
rules: {
"todo-plz/ticket-ref": [
"error",
{ commentPattern: /TODO:\s\[(PROJ-[0-9]+[,\s]*)+\]/ },
],
},
},
];
```

If you use a legacy JSON config, you would configure it like this:

```js
{
"rules": {
"todo-plz/ticket-ref": [
"error",
{ "commentPattern": "TODO:\\s\\[(PROJ-[0-9]+[,\\s]*)+\\]" }
{ "commentPattern": /TODO:\s\[(PROJ-[0-9]+[,\s]*)+\]/ }
]
}
}
Expand All @@ -96,7 +119,9 @@ Examples of **correct** code for this rule when using the above options:
// TODO: [PROJ-456] Connect to the API
```

### `description`
### description

Type: `string`

_Optional_. Override the error message portion that provides guidance on the expected ticket pattern. Defaults to: `Ticket pattern: <pattern>`

Expand Down
43 changes: 43 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import eslint from "@eslint/js";
import todoPlz from "./lib/index.js";
import eslintPlugin from "eslint-plugin-eslint-plugin";

/**
* @import {Linter} from 'eslint'
*/

/**
* @type {Linter.FlatConfig[]}
*/
export default [
{
ignores: ["tests/integration/**"],
},
{
files: ["lib/**/*.js"],
languageOptions: {
sourceType: "commonjs",
},
rules: eslint.configs.recommended.rules,
},
{
files: ["lib/**/*.js"],
plugins: { "todo-plz": todoPlz },
rules: {
"todo-plz/ticket-ref": ["error", { pattern: /#[0-9]+/ }],
},
},
{
files: ["lib/**/*.js"],
plugins: { "eslint-plugin": eslintPlugin },
rules: {
...eslintPlugin.configs["flat/rules"].rules,
"eslint-plugin/require-meta-docs-url": "error",
},
},
{
files: ["tests/**/*.js"],
plugins: { "eslint-plugin": eslintPlugin },
rules: eslintPlugin.configs["flat/tests"].rules,
},
];
13 changes: 11 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
"use strict";

module.exports.rules = {
"ticket-ref": require("./rules/ticket-ref"),
const packageJson = require("../package.json");

/** @type {import('eslint').ESLint.Plugin} */
module.exports = {
meta: {
name: packageJson.name,
version: packageJson.version,
},
rules: {
"ticket-ref": require("./rules/ticket-ref"),
},
};
73 changes: 52 additions & 21 deletions lib/rules/ticket-ref.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
// eslint-disable-next-line todo-plz/ticket-ref
/**
* @fileoverview Require a ticket reference in the TODO comment
* @fileoverview Require a ticket reference in TODO comments.
* @author Sawyer
*/
"use strict";

// @ts-check

/**
* @import { JSONSchema4 } from "json-schema";
* @import { Rule } from "eslint";
* @import * as ESTree from "estree";
*/

const messages = {
missingTicket:
"{{ term }} comment doesn't reference a ticket number. Ticket pattern: {{ pattern }}",
Expand All @@ -25,48 +34,60 @@ function getMessageId({ commentPattern, description }) {
return "missingTicket";
}

/** @type {JSONSchema4[]} */
const schema = [
{
type: "object",
properties: {
commentPattern: {
type: "string",
},
commentPattern: {},
description: {
type: "string",
},
pattern: {
type: "string",
},
pattern: {},
terms: {
type: "array",
items: {
type: "string",
uniqueItems: true,
},
},
},
additionalProperties: false,
},
];

/**
*
* @param {Rule.RuleContext} context
* @returns {Rule.RuleListener}
*/
function create(context) {
const { commentPattern, description, pattern, terms } = {
terms: ["TODO"],
...context.options[0],
};
const sourceCode = context.getSourceCode();
/** @type {{ pattern?: string | RegExp, terms?: string[], commentPattern?: string | RegExp }} */
const {
commentPattern,
description,
pattern,
terms = ["TODO"],
} = context.options[0];
const sourceCode = context.sourceCode;
const comments = sourceCode.getAllComments();
/** @type {Record<string, RegExp>} */
const termSearchPatterns = {};

const patternSource = pattern instanceof RegExp ? pattern.source : pattern;

terms.forEach((term) => {
termSearchPatterns[term] = new RegExp(
commentPattern || `${term}\\s?\\((${pattern}[,\\s]*)+\\)`,
"i"
commentPattern ?? `${term}\\s?\\((${patternSource}[,\\s]*)+\\)`,
"ui"
);
});

// eslint-disable-next-line todo-plz/ticket-ref
/**
* Check whether an individual comment includes a valid TODO
* @param {object} comment
* @param {ESTree.Comment} comment
* @returns {void}
*/
function validate(comment) {
const value = comment.value;
Expand All @@ -81,10 +102,20 @@ function create(context) {

if (searchPattern.test(value)) return;

const commentPatternSource =
commentPattern instanceof RegExp
? commentPattern.source
: commentPattern;

context.report({
loc: comment.loc,
messageId: getMessageId({ commentPattern, description }),
data: { commentPattern, description, pattern, term },
data: {
commentPattern: commentPatternSource,
description,
pattern: patternSource,
term,
},
});
});
}
Expand All @@ -94,18 +125,18 @@ function create(context) {
return {};
}

/** @type {Rule.RuleModule} */
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Require a ticket reference in the TODO comment",
category: "Fill me in",
description: "require a ticket reference in the TODO comment",
recommended: false,
url: "https://github.com/sawyerh/eslint-plugin-todo-plz/blob/main/docs/rules/ticket-ref.md",
},
fixable: null, // or "code" or "whitespace"
messages,
schema,
type: "suggestion",
messages,
},
create,
schema,
};
Loading