Skip to content

Commit

Permalink
wip: lint call signature arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
schoero committed Dec 9, 2023
1 parent 508288b commit a85af83
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 161 deletions.
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"lib/**"
],
"words": [
"callees",
"clsx",
"eptm",
"estree",
"quasis"
Expand Down
34 changes: 34 additions & 0 deletions src/rules/tailwind-no-unnecessary-whitespace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,38 @@ describe(`${tailwindNoUnnecessaryWhitespace.name}`, () => {
})).toBeUndefined();
});

it("should also work in defined call signature arguments", () => {

const uncleanedNestedCallExpression = tsx`test(" f e ");`;
const cleanedNestedCallExpression = tsx`test(" f e ");`;

const uncleanedMultilineTemplateLiteralWithNestedCallExpression = `
b a
d c ${uncleanedNestedCallExpression} h g
j i
`;
const cleanedMultilineTemplateLiteralWithNestedCallExpression = `
b a
d c ${cleanedNestedCallExpression} h g
j i
`;

expect(void lint(tailwindNoUnnecessaryWhitespace, {
invalid: [
{
code: tsx`test(" b a ");`,
errors: 1,
options: [{ callees: ["test"] }],
output: tsx`test("b a");`
},
{
code: `const Test = () => <div class={\`${uncleanedMultilineTemplateLiteralWithNestedCallExpression}\`} />;`,
errors: 1,
options: [{ callees: ["test"] }],
output: `const Test = () => <div class={\`${cleanedMultilineTemplateLiteralWithNestedCallExpression}\`} />;`
}
]
})).toBeUndefined();
});

});
124 changes: 77 additions & 47 deletions src/rules/tailwind-no-unnecessary-whitespace.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEFAULT_CLASS_NAMES } from "eptm:utils:config.js";
import { getClassAttributeLiterals, isJSXAttribute } from "eptm:utils:jsx.js";
import { DEFAULT_CALLEE_NAMES, DEFAULT_CLASS_NAMES } from "eptm:utils:config.js";
import { getCallExpressionLiterals, getClassAttributeLiterals, isJSXAttribute } from "eptm:utils:jsx.js";
import { combineClasses, splitClasses, splitWhitespace } from "eptm:utils:utils.js";

import type { Rule } from "eslint";
Expand All @@ -8,10 +8,13 @@ import type { JSXAttribute, JSXOpeningElement } from "estree-jsx";
import type { Parts } from "src/types/ast.js";
import type { ESLintRule } from "src/types/rule.js";

import type { Literals } from "eptm:utils:jsx.js";


export type Options = [
{
allowMultiline?: boolean;
callees?: string[];
classAttributes?: string[];
}
];
Expand All @@ -21,66 +24,86 @@ export const tailwindNoUnnecessaryWhitespace: ESLintRule<Options> = {
rule: {
create(ctx) {

return {
const { allowMultiline, callees } = getOptions(ctx);

JSXOpeningElement(node: Node) {

const jsxNode = node as JSXOpeningElement;
const lintLiterals = (ctx: Rule.RuleContext, literals: Literals, node: Node) => {

const attributes = getClassAttributes(ctx, jsxNode);
for(const literal of literals){

const { allowMultiline } = getOptions(ctx);
if(literal === undefined){ continue; }

for(const attribute of attributes){
const parts = createParts(ctx, literal);
const classChunks = splitClasses(ctx, literal.content);
const whitespaceChunks = splitWhitespace(ctx, literal.content);

const literals = getClassAttributeLiterals(ctx, attribute);
const classes: string[] = [];

for(const literal of literals){
if(allowMultiline){
for(let i = 0; i < Math.max(classChunks.length, whitespaceChunks.length); i++){
if(whitespaceChunks[i] && whitespaceChunks[i].includes("\n")){
classes.push(whitespaceChunks[i]);
classChunks[i] && classes.push(classChunks[i]);
} else {
if(classChunks[i] && i > 0){ classes.push(" "); }
classChunks[i] && classes.push(classChunks[i]);
}
}
} else {
for(let i = 0; i < classChunks.length; i++){
if(i > 0){ classes.push(" "); }
classes.push(classChunks[i]);
}
}

if(literal === undefined){ continue; }
const combinedClasses = combineClasses(ctx, classes, parts);

const parts = createParts(ctx, literal);
const classChunks = splitClasses(ctx, literal.content);
const whitespaceChunks = splitWhitespace(ctx, literal.content);
if(literal.raw === combinedClasses){
return;
}

const classes: string[] = [];
ctx.report({
data: {
unnecessaryWhitespace: literal.content
},
fix(fixer) {
return fixer.replaceText(literal, combinedClasses);
},
message: "Unnecessary whitespace: {{ unnecessaryWhitespace }}.",
node
});

if(allowMultiline){
for(let i = 0; i < Math.max(classChunks.length, whitespaceChunks.length); i++){
if(whitespaceChunks[i] && whitespaceChunks[i].includes("\n")){
classes.push(whitespaceChunks[i]);
classChunks[i] && classes.push(classChunks[i]);
} else {
if(classChunks[i] && i > 0){ classes.push(" "); }
classChunks[i] && classes.push(classChunks[i]);
}
}
} else {
for(let i = 0; i < classChunks.length; i++){
if(i > 0){ classes.push(" "); }
classes.push(classChunks[i]);
}
}
}

const combinedClasses = combineClasses(ctx, classes, parts);
};

if(literal.raw === combinedClasses){
return;
}

ctx.report({
data: {
unnecessaryWhitespace: literal.content
},
fix(fixer) {
return fixer.replaceText(literal, combinedClasses);
},
message: "Unnecessary whitespace: {{ unnecessaryWhitespace }}.",
node
});
return {

}
CallExpression(node) {

const { callee } = node;

if(callee.type !== "Identifier"){ return; }
if(!callees.includes(callee.name)){ return; }

const literals = getCallExpressionLiterals(ctx, node.arguments);

lintLiterals(ctx, literals, node);

},

JSXOpeningElement(node: Node) {

const jsxNode = node as JSXOpeningElement;

const attributes = getClassAttributes(ctx, jsxNode);

for(const attribute of attributes){
const literals = getClassAttributeLiterals(ctx, attribute);
lintLiterals(ctx, literals, node);
}

}

};
Expand All @@ -99,6 +122,12 @@ export const tailwindNoUnnecessaryWhitespace: ESLintRule<Options> = {
allowMultiline: {
type: "boolean"
},
callees: {
items: {
type: "string"
},
type: "array"
},
classAttributes: {
items: {
type: "string"
Expand All @@ -114,7 +143,6 @@ export const tailwindNoUnnecessaryWhitespace: ESLintRule<Options> = {
}
};


export function getClassAttributes(ctx: Rule.RuleContext, node: JSXOpeningElement): JSXAttribute[] {

const { classAttributes } = getOptions(ctx);
Expand Down Expand Up @@ -154,9 +182,11 @@ function getOptions(ctx: Rule.RuleContext) {

const classAttributes = options.classAttributes ?? DEFAULT_CLASS_NAMES;
const allowMultiline = options.allowMultiline ?? true;
const callees = options.callees ?? DEFAULT_CALLEE_NAMES;

return {
allowMultiline,
callees,
classAttributes
};

Expand Down
102 changes: 67 additions & 35 deletions src/rules/tailwind-sort-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import setupContextUtils from "tailwindcss/lib/lib/setupContextUtils.js";
import loadConfig from "tailwindcss/loadConfig.js";
import resolveConfig from "tailwindcss/resolveConfig.js";

import { DEFAULT_CLASS_NAMES } from "eptm:utils:config.js";
import { getClassAttributeLiterals, isJSXAttribute } from "eptm:utils:jsx.js";
import { DEFAULT_CALLEE_NAMES, DEFAULT_CLASS_NAMES } from "eptm:utils:config.js";
import { getCallExpressionLiterals, getClassAttributeLiterals, isJSXAttribute } from "eptm:utils:jsx.js";
import { combineClasses, splitClasses, splitWhitespace } from "eptm:utils:utils.js";

import type { Rule } from "eslint";
Expand All @@ -16,9 +16,12 @@ import type { Parts } from "src/types/ast.js";
import type { ESLintRule } from "src/types/rule.js";
import type { Config } from "tailwindcss/types/config.js";

import type { Literals } from "eptm:utils:jsx.js";


export type Options = [
{
callees?: string[];
classAttributes?: string[];
order?: "asc" | "desc" | "official" ;
tailwindConfig?: string;
Expand All @@ -30,57 +33,78 @@ export const tailwindSortClasses: ESLintRule<Options> = {
rule: {
create(ctx) {

const { callees } = getOptions(ctx);

const tailwindConfig = findTailwindConfig(ctx);
const tailwindContext = createTailwindContext(tailwindConfig);

return {

JSXOpeningElement(node: Node) {
const lintLiterals = (ctx: Rule.RuleContext, literals: Literals, node: Node) => {

const jsxNode = node as JSXOpeningElement;
for(const literal of literals){

const attributes = getClassAttributes(ctx, jsxNode);
if(literal === undefined){ continue; }

for(const attribute of attributes){
const parts = createParts(ctx, literal);
const classChunks = splitClasses(ctx, literal.content);
const whitespaceChunks = splitWhitespace(ctx, literal.content);

const literals = getClassAttributeLiterals(ctx, attribute);
const sortedClassChunks = sortClasses(ctx, tailwindContext, classChunks);

for(const literal of literals){
const classes: string[] = [];

if(literal === undefined){ continue; }
for(let i = 0; i < Math.max(sortedClassChunks.length, whitespaceChunks.length); i++){
whitespaceChunks[i] && classes.push(whitespaceChunks[i]);
sortedClassChunks[i] && classes.push(sortedClassChunks[i]);
}

const parts = createParts(ctx, literal);
const classChunks = splitClasses(ctx, literal.content);
const whitespaceChunks = splitWhitespace(ctx, literal.content);
const combinedClasses = combineClasses(ctx, classes, parts);

const sortedClassChunks = sortClasses(ctx, tailwindContext, classChunks);
if(literal.raw === combinedClasses){
return;
}

const classes: string[] = [];
ctx.report({
data: {
notSorted: literal.content
},
fix(fixer) {
return fixer.replaceText(literal, combinedClasses);
},
message: "Invalid class order: {{ notSorted }}.",
node
});

for(let i = 0; i < Math.max(sortedClassChunks.length, whitespaceChunks.length); i++){
whitespaceChunks[i] && classes.push(whitespaceChunks[i]);
sortedClassChunks[i] && classes.push(sortedClassChunks[i]);
}
}
};

const combinedClasses = combineClasses(ctx, classes, parts);

if(literal.raw === combinedClasses){
return;
}
return {

ctx.report({
data: {
notSorted: literal.content
},
fix(fixer) {
return fixer.replaceText(literal, combinedClasses);
},
message: "Invalid class order: {{ notSorted }}.",
node
});
CallExpression(node) {

}
const { callee } = node;

if(callee.type !== "Identifier"){ return; }
if(!callees.includes(callee.name)){ return; }

const literals = getCallExpressionLiterals(ctx, node.arguments);

lintLiterals(ctx, literals, node);

},

JSXOpeningElement(node: Node) {

const jsxNode = node as JSXOpeningElement;

const attributes = getClassAttributes(ctx, jsxNode);

for(const attribute of attributes){
const literals = getClassAttributeLiterals(ctx, attribute);
lintLiterals(ctx, literals, node);
}

}

};
Expand All @@ -96,6 +120,12 @@ export const tailwindSortClasses: ESLintRule<Options> = {
{
additionalProperties: false,
properties: {
callees: {
items: {
type: "string"
},
type: "array"
},
classAttributes: {
items: {
type: "string"
Expand Down Expand Up @@ -214,12 +244,14 @@ function getOptions(ctx: Rule.RuleContext) {

const options: Options[0] = ctx.options[0] ?? {};

const classAttributes = options.classAttributes ?? DEFAULT_CLASS_NAMES;
const order = options.order ?? "official";
const classAttributes = options.classAttributes ?? DEFAULT_CLASS_NAMES;
const callees = options.callees ?? DEFAULT_CALLEE_NAMES;

const tailwindConfig = options.tailwindConfig;

return {
callees,
classAttributes,
order,
tailwindConfig
Expand Down
Loading

0 comments on commit a85af83

Please sign in to comment.