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

feat!: regex names #63

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
3 changes: 2 additions & 1 deletion docs/concepts/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ It is possible that you never have to change this configuration, but if you do n

## Name

The simplest form to define string literals to lint is by their name. Callees, variables or class attributes with that name will be linted.
The simplest form to define string literals to lint is by their name. Callees, variables or class attributes with that name will be linted. The name can be a string or a regular expression.

<br/>

Expand Down Expand Up @@ -171,6 +171,7 @@ Matchers are the most powerful way to match string literals. They allow finer co
This allows additional filtering, such as literals in conditions or logical expressions. This opens up the possibility to lint any string that may contain tailwindcss classes while also reducing the number of false positives.

Matchers are defined as a tuple of a name and a list of configurations for predefined matchers.
The name can be a string or a regular expression.

<br/>

Expand Down
66 changes: 66 additions & 0 deletions src/parsers/es.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it } from "vitest";

import { tailwindNoUnnecessaryWhitespace } from "readable-tailwind:rules:tailwind-no-unnecessary-whitespace.js";
import { lint, TEST_SYNTAXES } from "readable-tailwind:tests:utils.js";


describe("es", () => {

it("should match callees names via regex", () => {
lint(tailwindNoUnnecessaryWhitespace, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
jsx: `testStyles(" lint ");`,
jsxOutput: `testStyles("lint");`,
options: [{
callees: ["^.*Styles$"]
}],
svelte: `<script>testStyles(" lint ");</script>`,
svelteOutput: `<script>testStyles("lint");</script>`,
vue: `<script>testStyles(" lint ");</script>`,
vueOutput: `<script>testStyles("lint");</script>`
}
]
});
});

it("should match variable names via regex", () => {
lint(tailwindNoUnnecessaryWhitespace, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
jsx: `const testStyles = " lint ";`,
jsxOutput: `const testStyles = "lint";`,
options: [{
variables: ["^.*Styles$"]
}],
svelte: `<script>const testStyles = " lint ";</script>`,
svelteOutput: `<script>const testStyles = "lint";</script>`,
vue: `<script>const testStyles = " lint ";</script>`,
vueOutput: `<script>const testStyles = "lint";</script>`
}
]
});
});

it("should match classAttributes via regex", () => {
lint(tailwindNoUnnecessaryWhitespace, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
jsx: `<img testStyles=" lint " />`,
jsxOutput: `<img testStyles="lint" />`,
options: [{
classAttributes: ["^.*Styles$"]
}],
svelte: `<img testStyles=" lint " />`,
svelteOutput: `<img testStyles="lint" />`,
vue: `<template><img testStyles=" lint " /> </template>`,
vueOutput: `<template><img testStyles="lint" /> </template>`
}
]
});
});

});
10 changes: 5 additions & 5 deletions src/parsers/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
matchesPathPattern
} from "readable-tailwind:utils:matchers.js";
import { getLiteralsByESNodeAndRegex } from "readable-tailwind:utils:regex.js";
import { deduplicateLiterals, getQuotes, getWhitespace } from "readable-tailwind:utils:utils.js";
import { deduplicateLiterals, getQuotes, getWhitespace, matchesName } from "readable-tailwind:utils:utils.js";

import type { Rule } from "eslint";
import type {
Expand Down Expand Up @@ -41,12 +41,12 @@ export function getLiteralsByESVariableDeclarator(ctx: Rule.RuleContext, node: E
if(!isESVariableSymbol(node.id)){ return literals; }

if(isVariableName(variable)){
if(variable !== node.id.name){ return literals; }
if(!matchesName(variable, node.id.name)){ return literals; }
literals.push(...getLiteralsByESExpression(ctx, [node.init]));
} else if(isVariableRegex(variable)){
literals.push(...getLiteralsByESNodeAndRegex(ctx, node, variable));
} else if(isVariableMatchers(variable)){
if(variable[0] !== node.id.name){ return literals; }
if(!matchesName(variable[0], node.id.name)){ return literals; }
literals.push(...getLiteralsByESMatchers(ctx, node.init, variable[1]));
}

Expand All @@ -63,12 +63,12 @@ export function getLiteralsByESCallExpression(ctx: Rule.RuleContext, node: ESCal
if(!isESCalleeSymbol(node.callee)){ return literals; }

if(isCalleeName(callee)){
if(callee !== node.callee.name){ return literals; }
if(!matchesName(callee, node.callee.name)){ return literals; }
literals.push(...getLiteralsByESExpression(ctx, node.arguments));
} else if(isCalleeRegex(callee)){
literals.push(...getLiteralsByESNodeAndRegex(ctx, node, callee));
} else if(isCalleeMatchers(callee)){
if(callee[0] !== node.callee.name){ return literals; }
if(!matchesName(callee[0], node.callee.name)){ return literals; }
literals.push(...getLiteralsByESMatchers(ctx, node, callee[1]));
}

Expand Down
6 changes: 3 additions & 3 deletions src/parsers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
isClassAttributeRegex
} from "readable-tailwind:utils:matchers.js";
import { getLiteralsByESNodeAndRegex } from "readable-tailwind:utils:regex.js";
import { deduplicateLiterals } from "readable-tailwind:utils:utils.js";
import { deduplicateLiterals, matchesName } from "readable-tailwind:utils:utils.js";

import type { Rule } from "eslint";
import type { TemplateLiteral as ESTemplateLiteral } from "estree";
Expand All @@ -30,12 +30,12 @@ export function getLiteralsByJSXClassAttribute(ctx: Rule.RuleContext, attribute:
if(!value){ return literals; }

if(isClassAttributeName(classAttribute)){
if(typeof attribute.name.name !== "string" || classAttribute.toLowerCase() !== attribute.name.name.toLowerCase()){ return literals; }
if(typeof attribute.name.name !== "string" || !matchesName(classAttribute.toLowerCase(), attribute.name.name.toLowerCase())){ return literals; }
literals.push(...getLiteralsByJSXAttributeValue(ctx, value));
} else if(isClassAttributeRegex(classAttribute)){
literals.push(...getLiteralsByESNodeAndRegex(ctx, attribute, classAttribute));
} else if(isClassAttributeMatchers(classAttribute)){
if(typeof attribute.name.name !== "string" || classAttribute[0].toLowerCase() !== attribute.name.name.toLowerCase()){ return literals; }
if(typeof attribute.name.name !== "string" || !matchesName(classAttribute[0].toLowerCase(), attribute.name.name.toLowerCase())){ return literals; }
literals.push(...getLiteralsByESMatchers(ctx, value, classAttribute[1]));
}

Expand Down
6 changes: 3 additions & 3 deletions src/parsers/svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
matchesPathPattern
} from "readable-tailwind:utils:matchers.js";
import { getLiteralsByESNodeAndRegex } from "readable-tailwind:utils:regex.js";
import { deduplicateLiterals, getQuotes, getWhitespace } from "readable-tailwind:utils:utils.js";
import { deduplicateLiterals, getQuotes, getWhitespace, matchesName } from "readable-tailwind:utils:utils.js";

import type { Rule } from "eslint";
import type { BaseNode as ESBaseNode, Node as ESNode } from "estree";
Expand Down Expand Up @@ -62,12 +62,12 @@ export function getLiteralsBySvelteClassAttribute(ctx: Rule.RuleContext, attribu

const literals = classAttributes.reduce<Literal[]>((literals, classAttribute) => {
if(isClassAttributeName(classAttribute)){
if(classAttribute.toLowerCase() !== attribute.key.name.toLowerCase()){ return literals; }
if(!matchesName(classAttribute.toLowerCase(), attribute.key.name.toLowerCase())){ return literals; }
literals.push(...getLiteralsBySvelteLiteralNode(ctx, value));
} else if(isClassAttributeRegex(classAttribute)){
literals.push(...getLiteralsByESNodeAndRegex(ctx, attribute, classAttribute));
} else if(isClassAttributeMatchers(classAttribute)){
if(classAttribute[0].toLowerCase() !== attribute.key.name.toLowerCase()){ return literals; }
if(!matchesName(classAttribute[0].toLowerCase(), attribute.key.name.toLowerCase())){ return literals; }
literals.push(...getLiteralsBySvelteMatchers(ctx, value, classAttribute[1]));
}

Expand Down
13 changes: 13 additions & 0 deletions src/parsers/vue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,17 @@ describe(tailwindSortClasses.name, () => {
});
});

it("should match bound classes via regex", () => {
lint(tailwindSortClasses, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
options: [{ classAttributes: [[":.*Styles$", [{ match: MatcherType.String }]]], order: "asc" }],
vue: `<template><img v-bind:testStyles="['c b a']" /></template>`,
vueOutput: `<template><img v-bind:testStyles="['a b c']" /></template>`
}
]
});
});

});
6 changes: 3 additions & 3 deletions src/parsers/vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
matchesPathPattern
} from "readable-tailwind:utils:matchers.js";
import { getLiteralsByESNodeAndRegex } from "readable-tailwind:utils:regex.js";
import { deduplicateLiterals, getQuotes, getWhitespace } from "readable-tailwind:utils:utils.js";
import { deduplicateLiterals, getQuotes, getWhitespace, matchesName } from "readable-tailwind:utils:utils.js";

import type { Rule } from "eslint";
import type { BaseNode as ESBaseNode, Node as ESNode } from "estree";
Expand All @@ -42,12 +42,12 @@ export function getLiteralsByVueClassAttribute(ctx: Rule.RuleContext, attribute:

const literals = classAttributes.reduce<Literal[]>((literals, classAttribute) => {
if(isClassAttributeName(classAttribute)){
if(getVueAttributeName(attribute)?.toLowerCase() !== getVueBoundName(classAttribute).toLowerCase()){ return literals; }
if(!matchesName(getVueBoundName(classAttribute).toLowerCase(), getVueAttributeName(attribute)?.toLowerCase())){ return literals; }
literals.push(...getLiteralsByVueLiteralNode(ctx, value));
} else if(isClassAttributeRegex(classAttribute)){
literals.push(...getLiteralsByESNodeAndRegex(ctx, attribute, classAttribute));
} else if(isClassAttributeMatchers(classAttribute)){
if(getVueAttributeName(attribute)?.toLowerCase() !== getVueBoundName(classAttribute[0]).toLowerCase()){ return literals; }
if(!matchesName(getVueBoundName(classAttribute[0]).toLowerCase(), getVueAttributeName(attribute)?.toLowerCase())){ return literals; }
literals.push(...getLiteralsByVueMatchers(ctx, value, classAttribute[1]));
}

Expand Down
57 changes: 57 additions & 0 deletions src/utils/matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,63 @@ describe("matchers", () => {

});

it("should match callees names via regex", () => {
lint(tailwindNoUnnecessaryWhitespace, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
jsx: `testStyles(" lint ");`,
jsxOutput: `testStyles("lint");`,
options: [{
callees: [["^.*Styles$", [{ match: MatcherType.String }]]]
}],
svelte: `<script>testStyles(" lint ");</script>`,
svelteOutput: `<script>testStyles("lint");</script>`,
vue: `<script>testStyles(" lint ");</script>`,
vueOutput: `<script>testStyles("lint");</script>`
}
]
});
});

it("should match variable names via regex", () => {
lint(tailwindNoUnnecessaryWhitespace, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
jsx: `const testStyles = " lint ";`,
jsxOutput: `const testStyles = "lint";`,
options: [{
variables: [["^.*Styles$", [{ match: MatcherType.String }]]]
}],
svelte: `<script>const testStyles = " lint ";</script>`,
svelteOutput: `<script>const testStyles = "lint";</script>`,
vue: `<script>const testStyles = " lint ";</script>`,
vueOutput: `<script>const testStyles = "lint";</script>`
}
]
});
});

it("should match classAttributes via regex", () => {
lint(tailwindNoUnnecessaryWhitespace, TEST_SYNTAXES, {
invalid: [
{
errors: 1,
jsx: `<img testStyles=" lint " />`,
jsxOutput: `<img testStyles="lint" />`,
options: [{
classAttributes: [["^.*Styles$", [{ match: MatcherType.String }]]]
}],
svelte: `<img testStyles=" lint " />`,
svelteOutput: `<img testStyles="lint" />`,
vue: `<template><img testStyles=" lint " /> </template>`,
vueOutput: `<template><img testStyles="lint" /> </template>`
}
]
});
});

});

describe("variables", () => {
Expand Down
6 changes: 6 additions & 0 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export function isLiteral(node: Node): node is Literal {
return node.type === "Literal";
}

export function matchesName(pattern: string, name: string | undefined): boolean {
if(!name){ return false; }

return new RegExp(pattern).test(name);
}

export function deduplicateLiterals(literals: Literal[]): Literal[] {
return literals.filter((l1, index) => {
return literals.findIndex(l2 => {
Expand Down
Loading