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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,28 @@ jobs:

- name: Check for unused dependencies
run: npm run knip

sync-verify:
name: Registry & README in sync
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Run sync script
run: npm run sync

- name: Verify no diff
run: |
if ! git diff --exit-code -- src/rules/index.ts README.md; then
echo "::error::scripts/sync.ts produced a diff. Run 'npm run sync' locally and commit the result."
exit 1
fi
25 changes: 19 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,25 @@ laint is an AI Agent Lint Rules SDK — a programmatic API for linting JSX/TSX c

## Adding a new rule

1. Create `src/rules/my-rule.ts` exporting a `RuleFunction`
2. Register it in `src/rules/index.ts`
3. Create `tests/my-rule.test.ts`
4. **Update the rule count assertion in `tests/config-modes.test.ts`** — the `getAllRuleNames` test has `expect(ruleNames.length).toBe(...)` that must match the total number of registered rules
5. **Document the rule in `README.md`** — CI checks that every registered rule name appears in the README
6. Run `npm test` to verify
1. Create `src/rules/my-rule.ts` exporting BOTH a `RuleFunction` and a `meta` object:

```ts
export const meta = {
name: 'my-rule',
severity: 'error' as const,
platforms: ['expo', 'web'] as Platform[] | null,
category: 'React / JSX',
description: 'One-line summary shown in README table',
};

export function myRule(ast: File, _code: string): LintResult[] { ... }
```

2. Create `tests/my-rule.test.ts`
3. Run `npm run sync` — regenerates `src/rules/index.ts` and the README rule tables from the per-rule `meta` exports
4. Run `npm test` to verify

That's it. The registry, README rule tables, platform tags, and rule count are all derived from the rule files. CI verifies `npm run sync` was run (no diff).

## Code style

Expand Down
97 changes: 46 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@ By default, all 45 rules run. To customize, create a `laint.config.json` in your

```json
// Only run these specific rules (include mode)
{
"rules": ["no-relative-paths", "expo-image-import", "fetch-response-ok-check"]
}
{ "rules": ["no-relative-paths", "expo-image-import", "fetch-response-ok-check"] }
```

```json
// Run all rules except these (exclude mode)
{
"rules": ["no-tailwind-animation-classes", "no-stylesheet-create"],
"exclude": true
}
{ "rules": ["no-tailwind-animation-classes", "no-stylesheet-create"], "exclude": true }
```

```json
Expand Down Expand Up @@ -122,82 +117,87 @@ const webRules = getRulesForPlatform('web');
const backendRules = getRulesForPlatform('backend');
```

## Available Rules (54 total)
## Available Rules (55 total)

<!-- AUTOGEN:RULES — managed by scripts/sync.ts; run `npm run sync` to update. -->

### Expo Router Rules

| Rule | Severity | Platform | Description |
| -------------------- | -------- | --------- | -------------------------------------------------------- |
| `no-relative-paths` | error | expo, web | Use absolute paths in router.navigate/push and Link href |
| `header-shown-false` | warning | expo | (tabs) Screen in root layout needs `headerShown: false` |
| `no-relative-paths` | error | expo, web | Use absolute paths in router.navigate/push and Link href |

### React Native / Expo Rules

| Rule | Severity | Platform | Description |
| ---------------------------------- | -------- | -------- | ---------------------------------------------------- |
| `no-stylesheet-create` | warning | expo | Use inline styles instead of StyleSheet.create() |
| `no-safeareaview` | warning | expo | Use useSafeAreaInsets() hook instead of SafeAreaView |
| `expo-font-loaded-check` | error | expo | useFonts() must check loaded before rendering |
| `expo-image-import` | warning | expo | Import Image from expo-image, not react-native |
| `native-tabs-bottom-padding` | warning | expo | NativeTabs screens need 64px bottom padding |
| `no-safeareaview` | warning | expo | Use useSafeAreaInsets() hook instead of SafeAreaView |
| `no-stylesheet-create` | warning | expo | Use inline styles instead of StyleSheet.create() |
| `no-tab-bar-height` | error | expo | Never set explicit height in tabBarStyle |
| `scrollview-horizontal-flexgrow` | warning | expo | Horizontal ScrollView needs `flexGrow: 0` |
| `expo-font-loaded-check` | error | expo | useFonts() must check loaded before rendering |
| `tabs-screen-options-header-shown` | warning | expo | Tabs screenOptions should have `headerShown: false` |
| `native-tabs-bottom-padding` | warning | expo | NativeTabs screens need 64px bottom padding |
| `textinput-keyboard-avoiding` | warning | expo | TextInput should be inside KeyboardAvoidingView |

### Liquid Glass Rules (expo-glass-effect)

| Rule | Severity | Platform | Description |
| ---------------------------- | -------- | -------- | ----------------------------------------------------- |
| `no-border-width-on-glass` | error | expo | No borderWidth on GlassView (breaks borderRadius) |
| `glass-needs-fallback` | warning | expo | Check isLiquidGlassAvailable() before using GlassView |
| `glass-interactive-prop` | warning | expo | GlassView in pressables needs `isInteractive={true}` |
| `glass-needs-fallback` | warning | expo | Check isLiquidGlassAvailable() before using GlassView |
| `glass-no-opacity-animation` | warning | expo | No opacity animations on GlassView |
| `no-border-width-on-glass` | error | expo | No borderWidth on GlassView (breaks borderRadius) |

### Next.js Rules

| Rule | Severity | Platform | Description |
| ---------------------------- | -------- | -------- | ----------------------------------------------------------------- |
| `require-use-client` | error | web | Files using client-only features must have "use client" directive |
| `no-module-level-new` | error | web | Avoid module-level constructors that execute during SSR |
| `no-react-native-in-web` | error | web | Do not import React Native modules from web code |
| `no-server-import-in-client` | error | web | "use client" files must not import server-only modules |
| `ssr-browser-api-guard` | error | web | Browser globals in server components crash during SSR |
| `require-use-client` | error | web | Files using client-only features must have "use client" directive |
| `ssr-browser-api-guard` | error | web | Guard browser-only APIs in files that run during SSR |

### React / JSX Rules

| Rule | Severity | Platform | Description |
| ---------------------------- | -------- | ------------ | --------------------------------------------- |
| `no-class-components` | warning | expo, web | Use function components with hooks |
| `no-inline-script-code` | error | web | Script tags should use template literals |
| `no-react-query-missing` | warning | expo, web | Use @tanstack/react-query for data fetching |
| `browser-api-in-useeffect` | warning | web | window/localStorage only in useEffect for SSR |
| `fetch-response-ok-check` | warning | web, backend | Check response.ok when using fetch |
| `no-class-components` | warning | expo, web | Use function components with hooks |
| `no-complex-jsx-expressions` | warning | expo, web | Avoid IIFEs and complex expressions in JSX |
| `no-inline-script-code` | error | web | Script tags should use template literals |
| `no-react-query-missing` | warning | expo, web | Use @tanstack/react-query for data fetching |

### Screen Transitions Rules (react-native-screen-transitions)

| Rule | Severity | Platform | Description |
| -------------------------------- | -------- | -------- | ------------------------------------------------------------------------- |
| `transition-worklet-directive` | error | expo | screenStyleInterpolator functions must include "worklet" directive |
| `transition-progress-range` | warning | expo | interpolate() should cover full [0, 1, 2] range including exit phase |
| `transition-gesture-scrollview` | warning | expo | Use Transition.ScrollView/FlatList instead of regular versions |
| `transition-shared-tag-mismatch` | warning | expo | sharedBoundTag on Transition.Pressable must have matching Transition.View |
| `transition-prefer-blank-stack` | warning | expo | Use Blank Stack instead of enableTransitions on Native Stack |
| `transition-progress-range` | warning | expo | interpolate() should cover full [0, 1, 2] range including exit phase |
| `transition-shared-tag-mismatch` | warning | expo | sharedBoundTag on Transition.Pressable must have matching Transition.View |
| `transition-worklet-directive` | error | expo | screenStyleInterpolator functions must include "worklet" directive |

### Tailwind CSS Rules

| Rule | Severity | Platform | Description |
| ------------------------------- | -------- | -------- | ------------------------------------------------------ |
| `no-tailwind-animation-classes` | warning | web | Avoid animate-\* classes, use style jsx global instead |
| `no-inline-styles` | warning | web | Avoid inline styles, use Tailwind CSS classes instead |
| Rule | Severity | Platform | Description |
| ------------------------------- | -------- | --------- | ------------------------------------------------------ |
| `no-inline-styles` | warning | universal | Avoid inline styles, use Tailwind CSS classes instead |
| `no-tailwind-animation-classes` | warning | web | Avoid animate-\* classes, use style jsx global instead |

### Backend / SQL Rules

| Rule | Severity | Platform | Description |
| ---------------------------- | -------- | -------- | ------------------------------------------------------------- |
| `no-require-statements` | error | backend | Use ES imports, not CommonJS require |
| `no-response-json-lowercase` | warning | backend | Use Response.json() instead of new Response(JSON.stringify()) |
| `sql-no-nested-calls` | error | backend | Don't nest sql template tags |
| `no-sync-fs` | error | backend | Use fs.promises or fs/promises instead of sync fs methods |
| Rule | Severity | Platform | Description |
| ------------------------------------ | -------- | -------- | ------------------------------------------------------------- |
| `no-require-statements` | error | backend | Use ES imports, not CommonJS require |
| `no-response-json-lowercase` | warning | backend | Use Response.json() instead of new Response(JSON.stringify()) |
| `no-sync-fs` | error | backend | Use fs.promises or fs/promises instead of sync fs methods |
| `no-unrestricted-loop-in-serverless` | error | backend | Avoid unbounded loops that can time out serverless functions |
| `sql-no-nested-calls` | error | backend | Don't nest sql template tags |

### URL Rules

Expand All @@ -215,33 +215,28 @@ const backendRules = getRulesForPlatform('backend');

| Rule | Severity | Platform | Description |
| ------------------------ | -------- | --------- | ---------------------------------------------------------------- |
| `prefer-guard-clauses` | warning | universal | Use early returns instead of nesting if statements |
| `no-type-assertion` | warning | universal | Avoid `as` type casts; use type narrowing or proper types |
| `safe-json-parse` | warning | universal | Wrap JSON.parse in try-catch to handle malformed input |
| `logger-error-with-err` | warning | universal | logger.error() must include { err: Error } for stack traces |
| `no-emoji-icons` | warning | universal | Use icons from lucide-react instead of emoji characters |
| `no-loose-equality` | warning | universal | Use === and !== instead of == and != (except == null) |
| `no-magic-env-strings` | warning | universal | Use centralized enum for env variable names, not magic strings |
| `no-manual-retry-loop` | warning | universal | Use a retry library instead of manual retry/polling loops |
| `no-nested-try-catch` | warning | universal | Avoid nested try-catch blocks, extract to separate functions |
| `no-string-coerce-error` | warning | universal | Use JSON.stringify instead of String() for unknown caught errors |
| `logger-error-with-err` | warning | universal | logger.error() must include { err: Error } for stack traces |
| `no-optional-props` | warning | universal | Use `prop: T \| null` instead of `prop?: T` in interfaces |
| `no-silent-skip` | warning | universal | Add else branch with logging instead of silently skipping |
| `no-manual-retry-loop` | warning | universal | Use a retry library instead of manual retry/polling loops |
| `no-emoji-icons` | warning | universal | Use icons from lucide-react instead of emoji characters |
| `no-string-coerce-error` | warning | universal | Use JSON.stringify instead of String() for unknown caught errors |
| `no-type-assertion` | warning | universal | Avoid `as` type casts; use type narrowing or proper types |
| `prefer-guard-clauses` | warning | universal | Use early returns instead of nesting if statements |
| `prefer-named-params` | warning | universal | Use object destructuring instead of positional parameters |
| `prefer-promise-all` | warning | universal | Prefer Promise.all for independent async work in loops |
| `safe-json-parse` | warning | universal | Wrap JSON.parse in try-catch to handle malformed input |

### General Rules

| Rule | Severity | Platform | Description |
| ------------------------------------ | -------- | --------- | ---------------------------------------------------------------- |
| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons |
| `no-react-native-in-web` | error | web | Don't import react-native in web modules (causes ESM failures) |
| `no-module-level-new` | error | web | Don't use `new` at module scope (crashes during SSR) |
| `no-require-statements` | error | backend | Use ES imports, not CommonJS require |
| `no-response-json-lowercase` | warning | backend | Use Response.json() instead of new Response(JSON.stringify()) |
| `sql-no-nested-calls` | error | backend | Don't nest sql template tags |
| `no-sync-fs` | error | backend | Use fs.promises or fs/promises instead of sync fs methods |
| `no-unrestricted-loop-in-serverless` | error | backend | Unbounded loops (while(true), for(;;)) cause serverless timeouts |
| `prefer-promise-all` | warning | universal | Use Promise.all instead of sequential await in for...of loops |
| Rule | Severity | Platform | Description |
| --------------------- | -------- | --------- | --------------------------------------------- |
| `prefer-lucide-icons` | warning | expo, web | Prefer lucide-react/lucide-react-native icons |

<!-- /AUTOGEN:RULES -->

---

Expand Down
Loading
Loading