Skip to content
Merged
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
33 changes: 15 additions & 18 deletions .pkgs/configs/tsl.config.base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { globSync } from "tinyglobby";
import { defineConfig } from "tsl";
import { core, defineConfig } from "tsl";
import { noDuplicateExports, noDuplicateImports, noMultilineTemplateExpressionWithoutAutoDedent, noUnsafeAs, nullish } from "tsl-dx";
export function buildConfig(cwd) {
return defineConfig({
Expand All @@ -10,23 +10,20 @@ export function buildConfig(cwd) {
"/build/",
],
rules: [
// ...core.all(),
// core.strictBooleanExpressions({
// allowAny: false,
// allowNullableBoolean: false,
// allowNullableEnum: false,
// allowNullableNumber: false,
// allowNullableObject: false,
// allowNullableString: false,
// allowNumber: true,
// allowString: false,
// }),
// core.noConfusingVoidExpression("off"),
// core.preferOptionalChain("off"),
// core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements
// core.switchExhaustivenessCheck({
// considerDefaultExhaustiveForUnions: true,
// }),
...core.all(),
core.strictBooleanExpressions({
allowAny: false,
allowNullableBoolean: false,
allowNullableEnum: false,
allowNullableNumber: false,
allowNullableObject: false,
allowNullableString: false,
allowNumber: true,
allowString: false,
}),
core.noConfusingVoidExpression("off"),
core.preferOptionalChain("off"),
core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements
nullish({
runtimeLibrary: "@eslint-react/eff",
}),
Expand Down
33 changes: 15 additions & 18 deletions .pkgs/configs/tsl.config.base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { globSync } from "tinyglobby";
import { defineConfig } from "tsl";
import { core, defineConfig } from "tsl";
import { noDuplicateExports, noDuplicateImports, noMultilineTemplateExpressionWithoutAutoDedent, noUnsafeAs, nullish } from "tsl-dx";

export function buildConfig(cwd: string) {
Expand All @@ -11,23 +11,20 @@ export function buildConfig(cwd: string) {
"/build/",
],
rules: [
// ...core.all(),
// core.strictBooleanExpressions({
// allowAny: false,
// allowNullableBoolean: false,
// allowNullableEnum: false,
// allowNullableNumber: false,
// allowNullableObject: false,
// allowNullableString: false,
// allowNumber: true,
// allowString: false,
// }),
// core.noConfusingVoidExpression("off"),
// core.preferOptionalChain("off"),
// core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements
// core.switchExhaustivenessCheck({
// considerDefaultExhaustiveForUnions: true,
// }),
...core.all(),
core.strictBooleanExpressions({
allowAny: false,
allowNullableBoolean: false,
allowNullableEnum: false,
allowNullableNumber: false,
allowNullableObject: false,
allowNullableString: false,
allowNumber: true,
allowString: false,
}),
core.noConfusingVoidExpression("off"),
core.preferOptionalChain("off"),
core.switchExhaustivenessCheck("off"), // This rule has a issue with `switch (true)` statements
nullish({
runtimeLibrary: "@eslint-react/eff",
}),
Expand Down
3 changes: 0 additions & 3 deletions .pkgs/samples/src/booleanPropNaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct
"Program:exit"(prog) {
const comps = query.all(prog);

// ─── Iterate Components ────────────────────────
for (const comp of comps) {
const [propsParam] = comp.node.params;
if (propsParam == null) continue;
Expand All @@ -40,7 +39,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct
const propsType = chk.getTypeAtLocation(tsNode);
const declaredProps = propsType.getProperties();

// ─── Iterate Props ─────────────────────────────
for (const prop of declaredProps) {
const propType = chk.getTypeOfSymbolAtLocation(prop, tsNode);

Expand All @@ -57,7 +55,6 @@ export function booleanPropNaming(options?: BooleanPropNamingOptions): RuleFunct
if (decl == null) continue;

const declNode = srv.tsNodeToESTreeNodeMap.get(decl);
if (declNode == null) continue;

const node = "key" in declNode ? declNode.key : declNode;

Expand Down
3 changes: 1 addition & 2 deletions .pkgs/samples/src/forbidComponentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export function forbidComponentProps(options: ForbidComponentPropsOptions): Rule
if (propName == null || !forbidden.includes(propName)) return;

// Verify context is JSX opening element
const parent = node.parent;
if (parent?.type !== "JSXOpeningElement") return;
const { parent } = node;

// Extract element name
const elemName = parent.name.type === "JSXIdentifier" ? parent.name.name : null;
Expand Down
3 changes: 1 addition & 2 deletions .pkgs/samples/src/forbidDomProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export function forbidDomProps(options: ForbidDomPropsOptions): RuleFunction {
if (propName == null || !forbidden.includes(propName)) return;

// Verify context is JSX opening element
const parent = node.parent;
if (parent?.type !== "JSXOpeningElement") return;
const { parent } = node;

// Extract element name
const elemName = parent.name.type === "JSXIdentifier" ? parent.name.name : null;
Expand Down
10 changes: 3 additions & 7 deletions .pkgs/samples/src/functionComponentDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export function functionComponentDefinition(): RuleFunction {
visitor,
{
"Program:exit"(program) {
// ─── Iterate all components ────────────────────
for (const { node } of query.all(program)) {
// Guard: must not already be arrow function
if (node.type === "ArrowFunctionExpression") continue;
Expand All @@ -27,24 +26,21 @@ export function functionComponentDefinition(): RuleFunction {
if (node.generator) return null;

const prefix = node.async ? "async " : "";
const typeParams = node.typeParameters ? src.getText(node.typeParameters) : "";
const typeParams = node.typeParameters != null ? src.getText(node.typeParameters) : "";
const params = `(${node.params.map((p) => src.getText(p)).join(", ")})`;
const returnType = node.returnType ? src.getText(node.returnType) : "";
const returnType = node.returnType != null ? src.getText(node.returnType) : "";
const body = src.getText(node.body);

// ─── Case: function declaration ──────────────
if (node.type === "FunctionDeclaration" && node.id) {
if (node.type === "FunctionDeclaration" && node.id != null) {
// dprint-ignore
return fixer.replaceText(node, `const ${node.id.name} = ${prefix}${typeParams}${params}${returnType} => ${body};`);
}

// ─── Case: function expression in variable ───
if (node.type === "FunctionExpression" && node.parent.type === "VariableDeclarator") {
// dprint-ignore
return fixer.replaceText(node, `${prefix}${typeParams}${params}${returnType} => ${body}`);
}

// ─── Case: object method shorthand ───────────
if (node.type === "FunctionExpression" && node.parent.type === "Property") {
// dprint-ignore
return fixer.replaceText(node.parent, `${src.getText(node.parent.key)}: ${prefix}${typeParams}${params}${returnType} => ${body}`);
Expand Down
12 changes: 3 additions & 9 deletions .pkgs/samples/src/jsxFragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export type JsxsFragmentsOptions = {
export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction {
const { mode = "syntax" } = options;
return (context) => {
// ── Helpers ─────────────────────────────────────

function reportSyntaxPreferred(node: TSESTree.JSXOpeningElement, pattern: "React.Fragment" | "Fragment") {
// Guard: has key prop (legitimate use of standard form)
const hasAttributes = node.attributes.length > 0;
Expand All @@ -22,31 +20,27 @@ export function jsxFragments(options: JsxsFragmentsOptions = {}): RuleFunction {
node,
message: `Use shorthand fragment syntax '<>...</>' instead of '<${pattern}>...</${pattern}'.`,
fix(fixer) {
const closing = node.parent?.closingElement;
if (!closing) return null;
const closing = node.parent.closingElement;
if (closing == null) return null;
return [fixer.replaceText(node, "<>"), fixer.replaceText(closing, "</>")];
},
});
}

// ── Listeners ────────────────────────────────────

return {
JSXOpeningElement(node) {
const name = node.name;

// ─── Handle <Fragment> (JSXIdentifier) ───────
if (name.type === "JSXIdentifier" && name.name === "Fragment") {
if (mode === "syntax") {
reportSyntaxPreferred(node, "Fragment");
}
return;
}

// ─── Handle <React.Fragment> (JSXMemberExpression) ─
if (name.type !== "JSXMemberExpression") return;
if (name.object.type !== "JSXIdentifier" || name.object.name !== "React") return;
if (name.property.type !== "JSXIdentifier" || name.property.name !== "Fragment") return;
if (name.property.name !== "Fragment") return;

if (mode === "syntax") {
reportSyntaxPreferred(node, "React.Fragment");
Expand Down
3 changes: 1 addition & 2 deletions .pkgs/samples/src/jsxHandlerNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ export function jsxHandlerNames(options: JsxHandlerNamesOptions = {}): RuleFunct
if (!EVENT_HANDLER_REGEX.test(propName)) return;

const value = node.value;
if (!value) return;
if (value == null) return;

// ─── Check expression value ────────────────────
if (value.type === "JSXExpressionContainer") {
const expression = ast.unwrap(value.expression);

Expand Down
6 changes: 2 additions & 4 deletions .pkgs/samples/src/jsxMaxDepth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ export function jsxMaxDepth(options: JsxMaxDepthOptions): RuleFunction {
return (context) => ({
JSXElement(node) {
let depth = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let parent: any = node.parent;
let parent: typeof node.parent | null | undefined = node.parent;

// ─── Walk up the tree ──────────────────────────
while (parent) {
while (parent != null) {
if (parent.type === "JSXElement") {
depth++;
}
Expand Down
1 change: 0 additions & 1 deletion .pkgs/samples/src/jsxNoBind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export function jsxNoBind(): RuleFunction {

const expr = ast.unwrap(value.expression);

// ─── Detect forbidden patterns ─────────────────
switch (true) {
case expr.type === "ArrowFunctionExpression":
case expr.type === "FunctionExpression":
Expand Down
1 change: 0 additions & 1 deletion .pkgs/samples/src/jsxNoDuplicateProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export function jsxNoDuplicateProps(options: JsxNoDuplicatePropsOptions = {}): R
JSXOpeningElement(node) {
const seen = new Map<string, string>();

// ─── Check each attribute ──────────────────────
for (const attr of node.attributes) {
if (attr.type !== "JSXAttribute") continue;
if (attr.name.type !== "JSXIdentifier") continue;
Expand Down
6 changes: 0 additions & 6 deletions .pkgs/samples/src/jsxNoLiterals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction
const { noStrings = false, allowedStrings = [], ignoreProps = true } = options;
const allowedSet = new Set(allowedStrings);
return (context) => ({
// ─── Check literal text children ───────────────
Literal(node) {
if (typeof node.value !== "string") return;
const text = node.value.trim();
if (text === "" || allowedSet.has(text)) return;

const parent = node.parent;
if (!parent) return;

// ─── Case: prop value ────────────────────────
if (parent.type === "JSXAttribute") {
if (!ignoreProps) {
context.report({
Expand All @@ -35,10 +32,8 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction
return;
}

// ─── Case: already wrapped ───────────────────
if (parent.type === "JSXExpressionContainer") return;

// ─── Case: child of element/fragment ─────────
if (parent.type === "JSXElement" || parent.type === "JSXFragment") {
if (noStrings) {
context.report({
Expand All @@ -54,7 +49,6 @@ export function jsxNoLiterals(options: JsxNoLiteralsOptions = {}): RuleFunction
}
},

// ─── Check JSX text nodes ──────────────────────
JSXText(node) {
const text = node.value.trim();
if (text === "" || allowedSet.has(text)) return;
Expand Down
3 changes: 0 additions & 3 deletions .pkgs/samples/src/jsxPascalCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction

const componentName = name.name;

// ─── Handle leading underscore ───────────────
if (componentName.startsWith("_")) {
if (!allowLeadingUnderscore) {
context.report({
Expand All @@ -37,7 +36,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction
if (firstChar == null) return;
if (firstChar === firstChar.toLowerCase()) return;

// ─── Handle all-caps ─────────────────────────
if (componentName === componentName.toUpperCase()) {
if (!allowAllCaps) {
context.report({
Expand All @@ -48,7 +46,6 @@ export function jsxPascalCase(options: JsxPascalCaseOptions = {}): RuleFunction
return;
}

// ─── Validate PascalCase ─────────────────────
if (!pascalCaseRegex.test(componentName)) {
context.report({
node: name,
Expand Down
1 change: 0 additions & 1 deletion .pkgs/samples/src/jsxPropsNoSpreadMulti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export function jsxPropsNoSpreadMulti(): RuleFunction {
JSXOpeningElement(node) {
const seen = new Set<string>();

// ─── Check each spread attribute ───────────────
for (const attr of node.attributes) {
if (attr.type !== "JSXSpreadAttribute") continue;

Expand Down
1 change: 0 additions & 1 deletion .pkgs/samples/src/noAdjacentInlineElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function noAdjacentInlineElements(): RuleFunction {
JSXElement(node) {
const children = node.children;

// ─── Check adjacent pairs ──────────────────────
for (let i = 0; i < children.length - 1; i++) {
const current = children[i];
const next = children[i + 1];
Expand Down
1 change: 0 additions & 1 deletion .pkgs/samples/src/noMultiComp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export function noMultiComp(): RuleFunction {
"Program:exit"(program) {
const components = query.all(program);

// ─── Report excess components ──────────────────
for (const { node, name } of components.slice(1)) {
context.report({
node,
Expand Down
4 changes: 3 additions & 1 deletion .pkgs/samples/src/noUnnecessaryUsePrefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export function noUnnecessaryUsePrefix(): RuleFunction {
if (hook.hookCalls.length === 0) {
context.report({
node: hook.node,
message: `Custom hook "${hook.name}" doesn't call any hooks. A custom hook should use at least one hook, otherwise it's just a regular function.`,
message: `Custom hook "${
hook.name ?? "unknown"
}" doesn't call any hooks. A custom hook should use at least one hook, otherwise it's just a regular function.`,
});
}
}
Expand Down
Loading
Loading