Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
levibuzolic committed Jun 5, 2024
1 parent 2b860dc commit 99b19bb
Show file tree
Hide file tree
Showing 8 changed files with 634 additions and 661 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ jobs:

strategy:
matrix:
node-version: [14, 16, 18]
node-version: [16, 18, 20, 22]
eslint-version: [4, 5, 6, 7, 8, 9]

steps:
- uses: actions/checkout@v2
Expand All @@ -18,6 +19,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn install
- name: Biome
run: yarn biome ci .
- name: Tests
run: yarn run test
env:
Expand Down
9 changes: 0 additions & 9 deletions .prettierrc

This file was deleted.

20 changes: 20 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.0/schema.json",
"formatter": {
"enabled": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
}
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
"test": "node tests.js"
},
"devDependencies": {
"eslint": ">=3.0.0"
"@biomejs/biome": "^1.8.0",
"@types/eslint": "^8.56.10",
"@types/node": "^20.14.2",
"eslint": ">=3.0.0",
"typescript": "^5.4.5"
},
"engines": {
"node": ">=5.0.0"
"node": ">=16.0.0"
},
"license": "MIT",
"repository": {
Expand Down
224 changes: 142 additions & 82 deletions rules/no-only-tests.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,157 @@
/**
* @fileoverview Rule to flag use of .only in tests, preventing focused tests being committed accidentally
* @fileoverview Rule to prevent use of focused test blocks, preventing accidental commits that only run a subset of tests
* @author Levi Buzolic
*/

'use strict';

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

const defaultOptions = {
block: ['describe', 'it', 'context', 'test', 'tape', 'fixture', 'serial', 'Feature', 'Scenario', 'Given', 'And', 'When', 'Then'],
focus: ['only'],
fix: false,
matchers: [
"{describe,it,context,test,tape,fixture,serial,Feature,Scenario,Given,And,When,Then}.only",
],
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description: 'disallow .only blocks in tests',
category: 'Possible Errors',
recommended: true,
url: 'https://github.com/levibuzolic/eslint-plugin-no-only-tests',
},
fixable: true,
schema: [
{
type: 'object',
properties: {
block: {
type: 'array',
items: {
type: 'string',
},
uniqueItems: true,
default: defaultOptions.block,
},
focus: {
type: 'array',
items: {
type: 'string',
},
uniqueItems: true,
default: defaultOptions.focus,
},
fix: {
type: 'boolean',
default: defaultOptions.fix,
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = Object.assign({}, defaultOptions, context.options[0]);
const blocks = options.block || [];
const focus = options.focus || [];
const fix = !!options.fix;
meta: {
type: "problem",
docs: {
description: "Disallow focused tests",
category: "Possible Errors",
recommended: true,
url: "https://github.com/levibuzolic/eslint-plugin-no-only-tests",
},
fixable: "code",
schema: [
{
type: "object",
properties: {
matchers: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
default: defaultOptions.matchers,
},
fix: {
type: "boolean",
default: false,
},
block: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
default: [],
deprecated: "Use `matchers` option instead",
},
focus: {
type: "array",
items: {
type: "string",
},
uniqueItems: true,
default: [],
deprecated: "Use `matchers` option instead",
},
},
additionalProperties: false,
default: {},
},
],
},
create(context) {
console.log("==========", context.options[0]);
return {};
const providedOptions = context.options[0];

if (providedOptions?.block || providedOptions?.focus) {
const blocks = providedOptions.block ?? [
"describe",
"it",
"context",
"test",
"tape",
"fixture",
"serial",
"Feature",
"Scenario",
"Given",
"And",
"When",
"Then",
];
const focus = providedOptions.focus ?? ["only"];

console.log("==========", providedOptions);

const migratedConfig = {
fix: providedOptions?.fix,
matchers: [
// ...(providedOptions?.matchers ?? []),
[matcherGroup(blocks), matcherGroup(focus)]
.filter(Boolean)
.join("."),
],
};
console.warn(
`The \`block\` and \`focus\` options of \`eslint-no-only-tests\` are deprecated, please use the \`matchers\` option instead. This should provide a more expressive way to define the test methods you want to prevent. Here’s the equivalent of your current configuration:\n${JSON.stringify(migratedConfig)}`,
);
}

const options = Object.assign({}, defaultOptions, providedOptions);

return {
Identifier(node) {
const parentObject = node.parent && node.parent.object;
if (parentObject == null) return;
if (focus.indexOf(node.name) === -1) return;
const fix = !!options.fix;

const callPath = getCallPath(node.parent).join('.');
const blocks = [];
const focus = [];

// comparison guarantees that matching is done with the beginning of call path
if (
blocks.find(block => {
// Allow wildcard tail matching of blocks when ending in a `*`
if (block.endsWith('*')) return callPath.startsWith(block.replace(/\*$/, ''));
return callPath.startsWith(`${block}.`);
})
) {
context.report({
node,
message: callPath + ' not permitted',
fix: fix ? fixer => fixer.removeRange([node.range[0] - 1, node.range[1]]) : undefined,
});
}
},
};
},
return {
Identifier(node) {
const parentObject = node.parent?.object;
if (parentObject == null) return;
if (focus.indexOf(node.name) === -1) return;

const callPath = getCallPath(node.parent).join(".");

// comparison guarantees that matching is done with the beginning of call path
if (
blocks.find((block) => {
// Allow wildcard tail matching of blocks when ending in a `*`
if (block.endsWith("*"))
return callPath.startsWith(block.replace(/\*$/, ""));
return callPath.startsWith(`${block}.`);
})
) {
context.report({
node,
message: `${callPath} not permitted`,
fix: fix
? (fixer) => fixer.removeRange([node.range[0] - 1, node.range[1]])
: undefined,
});
}
},
};
},
};

function getCallPath(node, path = []) {
if (node) {
const nodeName = node.name || (node.property && node.property.name);
if (node.object) return getCallPath(node.object, [nodeName, ...path]);
if (node.callee) return getCallPath(node.callee, path);
return [nodeName, ...path];
}
return path;
if (node) {
const nodeName = node.name || node.property?.name;
if (node.object) return getCallPath(node.object, [nodeName, ...path]);
if (node.callee) return getCallPath(node.callee, path);
return [nodeName, ...path];
}
return path;
}

/**
* @param {string[]} items
* @returns {string | undefined}
*/
function matcherGroup(items) {
const uniqueItems = [...new Set(items)];
if (uniqueItems.length <= 1) return uniqueItems[0];
return `{${uniqueItems.join(",")}}`;
}
49 changes: 26 additions & 23 deletions tests.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const rules = require('./index').rules;
const RuleTester = require('eslint').RuleTester;
const rules = require("./index").rules;
const RuleTester = require("eslint").RuleTester;
const ruleTester = new RuleTester();

ruleTester.run('no-only-tests', rules['no-only-tests'], {
valid: [
'describe("Some describe block", function() {});',
ruleTester.run("no-only-tests", rules["no-only-tests"], {
valid: [
{ options: [], code: 'describe("Some describe block", function() {});' },
/*
'it("Some assertion", function() {});',
'xit.only("Some assertion", function() {});',
'xdescribe.only("Some describe block", function() {});',
Expand All @@ -16,22 +17,23 @@ ruleTester.run('no-only-tests', rules['no-only-tests'], {
'var args = {only: "test"};',
'it("should pass meta only through", function() {});',
'obscureTestBlock.only("An obscure testing library test works unless options are supplied", function() {});',
{
options: [{block: ['it']}],
code: 'test.only("Options will exclude this from being caught", function() {});',
},
{
options: [{focus: ['focus']}],
code: 'test.only("Options will exclude this from being caught", function() {});',
},
],

invalid: [
{
code: 'describe.only("Some describe block", function() {});',
output: 'describe.only("Some describe block", function() {});',
errors: [{message: 'describe.only not permitted'}],
},
*/
{
options: [{ block: ["it"] }],
code: 'test.only("Options will exclude this from being caught", function() {});',
},
{
options: [{ focus: ["focus"] }],
code: 'test.only("Options will exclude this from being caught", function() {});',
},
],
invalid: [
{
code: 'describe.only("Some describe block", function() {});',
output: 'describe.only("Some describe block", function() {});',
errors: [{ message: "describe.only not permitted" }],
},
/*
{
code: 'it.only("Some assertion", function() {});',
output: 'it.only("Some assertion", function() {});',
Expand Down Expand Up @@ -194,7 +196,8 @@ ruleTester.run('no-only-tests', rules['no-only-tests'], {
output: 'Then("Some assertion", function() {});',
errors: [{message: 'Then.only not permitted'}],
},
],
*/
],
});

console.log('Tests completed successfully');
console.log("Tests completed successfully");
20 changes: 20 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"checkJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"module": "preserve",
"noEmit": true,
"lib": ["es2022"],
"types": ["node"],
}
}
Loading

0 comments on commit 99b19bb

Please sign in to comment.