# Linting

Catch invalid web styles, conflicting JSX props, and unused definitions with Zyzz's Oxlint plugin.

## Overview

The `zyzz/oxlint` plugin checks source without evaluating application code or loading imported configuration modules. Keep compilation and TypeScript checking in the workflow, since linting cannot validate every style or token contract.

Each of the five [rules](#rules) reports diagnostics without automatic fixes, and linting preserves declaration order. Review each change before removing a definition or merging expressions that could change evaluation order.

Registration alone enables no Zyzz rules, so enable the ones that fit the project. This configuration treats invalid styles and competing JSX props as errors, and unused namespace definitions as warnings:

```json title=".oxlintrc.json"
{
  "jsPlugins": [{ "name": "zyzz", "specifier": "zyzz/oxlint" }],
  "rules": {
    "zyzz/no-conflicting-props": "error",
    "zyzz/no-unused": "warn",
    "zyzz/valid-styles": "error"
  }
}
```

## Walkthrough

### Install the Linter

With [Zyzz set up](/docs/introduction/getting-started), add Oxlint as a development dependency:

```sh
npm install --save-dev oxlint
```

If the project already uses Vite Plus, use the [Vite Plus configuration](#use-vite-plus) below instead.

### Register the Plugin

Save the [Overview](#overview) configuration as `.oxlintrc.json`, or merge its `jsPlugins` and `rules` into the existing configuration. [Logical properties](#use-logical-properties) and [project restrictions](#restricted-properties) remain opt-in.

### Run Linting

Run the linter from the project directory:

```sh
npx oxlint .
```

Errors produce a failing exit status. To fail on warnings too, run `npx oxlint --deny-warnings .`. Add the command to the project's checks alongside its type check and build.

> [!NOTE]
> Oxlint's [JavaScript plugin
> API](https://oxc.rs/docs/guide/usage/linter/js-plugins.html) is alpha. Zyzz's
> integration fixtures use Oxlint 1.72.0 and Vite Plus 0.2.2. These are tested
> versions, not minimum supported versions.

## Recipes

### Use Vite Plus

Put the same plugin registration and rules under `lint` in the existing `vite.config.ts`. Preserve its build plugins and other configuration.

```ts title="vite.config.ts"
import { defineConfig } from 'vite-plus'

export default defineConfig({
  lint: {
    jsPlugins: [{ name: 'zyzz', specifier: 'zyzz/oxlint' }],
    rules: {
      'zyzz/no-conflicting-props': 'error',
      'zyzz/no-unused': 'warn',
      'zyzz/valid-styles': 'error',
    },
  },
})
```

Run `vp lint`. The Zyzz rules do not require type-aware linting. See [Vite Plus lint configuration](https://viteplus.dev/guide/lint) for the surrounding workflow.

### Recognize Configured Helpers

The rules recognize imports from `zyzz`, `zyzz/default`, and modules whose final filename matches `zyzz.config.*`, such as `./zyzz.config.ts`, `../zyzz.config.mjs`, or `@/zyzz.config.js`. These filenames need no extra settings. For a config module with another name, add its exact import specifier:

```json title=".oxlintrc.json"
{
  "settings": {
    "zyzz": {
      "imports": ["@/styles.js"]
    }
  }
}
```

These entries supplement automatic recognition. Matching uses the import text without filesystem resolution. Config modules are assumed to expose `style` and `variants`. Only imports from `zyzz` expose recognized `cx` and `Config` helpers.

Named import aliases, namespace imports, immutable local aliases, and helpers destructured from local `Config.create()` calls are supported. Lexical shadowing is respected. The linter does not resolve arbitrary re-exports, imported style definitions, token contracts, or custom shorthand mappings across files.

### Scope Web Rules

Declaration rules inspect `style()` objects and callback returns, variant bases and choices, compound styles, selectors, conditions, and `targets.web`. They exclude variant names, selection metadata, variable assignment containers, arbitrary callback expressions, and native target branches.

In a mixed web and native project, enable these rules only for web authoring files through [Oxlint overrides](https://oxc.rs/docs/guide/usage/linter/config.html). Adapt the file pattern to the project:

```json title=".oxlintrc.json"
{
  "jsPlugins": [{ "name": "zyzz", "specifier": "zyzz/oxlint" }],
  "overrides": [
    {
      "files": ["src/web/**/*.{ts,tsx}"],
      "rules": {
        "zyzz/no-conflicting-props": "error",
        "zyzz/no-unused": "warn",
        "zyzz/valid-styles": "error"
      }
    }
  ]
}
```

### Suppress Diagnostics

All rules support standard Oxlint suppression comments. Keep exceptions narrow and explain why the declaration is intentional:

```ts title="styles.ts"
import { style } from 'zyzz'

export namespace styles {
  export const overlay = style({
    position: 'absolute',
    // Keep this overlay aligned to the physical left edge.
    // oxlint-disable-next-line zyzz/use-logical-properties
    left: '0px',
  })
}
```

## Rules

None of the rules needs type-aware linting or applies automatic fixes, and only `restricted-properties` takes options. The [Overview](#overview) configuration enables the first three, and the last two are opt-in:

* **[`zyzz/valid-styles`](#valid-styles):** Malformed declarations and values.
* **[`zyzz/no-conflicting-props`](#no-conflicting-props):** JSX props that overwrite applied styles.
* **[`zyzz/no-unused`](#no-unused):** Style definitions that nothing applies.
* **[`zyzz/use-logical-properties`](#use-logical-properties):** Physical properties with logical equivalents.
* **[`zyzz/restricted-properties`](#restricted-properties):** Properties and values the project restricts.

### Valid Styles

Reports unknown properties in token-free styles, malformed value markers, empty or sparse fallback arrays, nonscalar declaration values, and literal structures that cannot describe styles. Unknown-property checks apply only to imports from `zyzz`, since configured helpers can define shorthand properties.

The rule leaves unresolved expressions to the compiler. It does not validate complete CSS grammars, configured token membership, or every compiler restriction.

#### Incorrect

```ts title="styles.ts"
import { style } from 'zyzz'

export namespace styles {
  export const card = style({
    colr: 'red',
    // zyzz(valid-styles): Unknown CSS property 'colr'.
    color: 'red!important',
    // zyzz(valid-styles): Value markers require " !custom" followed by optional " !important", or " !important" alone.
    display: [],
    // zyzz(valid-styles): Fallback arrays must be nonempty.
  })
}
```

#### Correct

```ts title="styles.ts"
import { style } from 'zyzz'

export namespace styles {
  export const card = style({
    color: 'red !important',
    display: ['block', 'flex'],
  })
}
```

### No Conflicting Props

Reports explicit `className` or `style` attributes beside a known Zyzz application spread, in either order. Multiple known application spreads also conflict, since a later JSX spread overwrites props from an earlier one.

Pass overrides into the style call, and compose applications with `cx`. Detection targets React-style web props and locally resolved definitions or `cx` applications. Unknown spreads and style definitions imported from another module are skipped.

#### Incorrect

```tsx title="Card.tsx"
import { style } from 'zyzz'

export function Card() {
  return (
    <section {...styles.card()} className="external">
    // zyzz(no-conflicting-props): Pass 'className' into the Zyzz style call to merge styling props.
      <span {...styles.label()} {...styles.selected()}>Selected</span>
      // zyzz(no-conflicting-props): Compose Zyzz applications with cx() instead of overwriting them with multiple JSX spreads.
    </section>
  )
}

namespace styles {
  export const card = style({ padding: '16px' })

  export const label = style({ fontWeight: 500 })

  export const selected = style({ opacity: 0.8 })
}
```

#### Correct

```tsx title="Card.tsx"
import { cx, style } from 'zyzz'

export function Card() {
  return (
    <section {...styles.card({ className: 'external' })}>
      <span {...cx(styles.label(), styles.selected())}>Selected</span>
    </section>
  )
}

namespace styles {
  export const card = style({ padding: '16px' })

  export const label = style({ fontWeight: 500 })

  export const selected = style({ opacity: 0.8 })
}
```

### No Unused

Reports unused style and variant definitions in local, top-level TypeScript namespaces. Oxlint's built-in `no-unused-vars` remains responsible for ordinary local bindings.

Exported, merged, or nested namespaces, computed member access, and namespaces passed as values are retained conservatively. Direct references between namespace members count as uses.

#### Incorrect

```tsx title="Card.tsx"
import { style } from 'zyzz'

export function Card() {
  return <section {...styles.card()}>Content</section>
}

namespace styles {
  export const card = style({ padding: '16px' })

  export const unused = style({ color: 'red' })
  // zyzz(no-unused): Style 'styles.unused' is never used.
}
```

#### Correct

```tsx title="Card.tsx"
import { style } from 'zyzz'

export function Card() {
  return <section {...styles.card()}>Content</section>
}

namespace styles {
  export const card = style({ padding: '16px' })
}
```

### Use Logical Properties

Reports the horizontal physical properties and corner radii in Tempo's [logical-property policy](https://github.com/tempoxyz/tempo.xyz/blob/4544c69b7535974bf0ff97a222be31b8f1b2f3f7/config/oxlint/logical-properties.ts), and suggests a logical replacement for horizontal left-to-right layouts. Enable it with `"zyzz/use-logical-properties": "warn"`.

An `allow-physical-property` comment permits an intentional physical edge. The rule does not cover vertical properties, physical keyword values, or shorthand values, so check direction and writing mode before changing a property.

#### Incorrect

```ts title="styles.ts"
import { style } from 'zyzz'

export namespace styles {
  export const card = style({
    marginLeft: '16px',
    // zyzz(use-logical-properties): Use a CSS logical property instead of 'marginLeft', such as 'marginInlineStart' for horizontal LTR layouts.
    paddingRight: '8px',
    // zyzz(use-logical-properties): Use a CSS logical property instead of 'paddingRight', such as 'paddingInlineEnd' for horizontal LTR layouts.
  })
}
```

#### Correct

```ts title="styles.ts"
import { style } from 'zyzz'

export namespace styles {
  export const card = style({
    marginInlineStart: '16px',
    paddingInlineEnd: '8px',
  })

  export const overlay = style({
    position: 'absolute',
    // allow-physical-property
    left: '0px',
  })
}
```

### Restricted Properties

Bans properties, or limits their statically resolved values, keyed by exact authored property names. Each restriction accepts a `reason` for the diagnostic and a `values` allowlist of strings or numbers. Omitting `values` bans the property, including dynamic values.

```json title=".oxlintrc.json"
{
  "rules": {
    "zyzz/restricted-properties": [
      "error",
      {
        "color": { "reason": "Use color tokens.", "values": [] },
        "padding": {
          "reason": "Use the spacing scale.",
          "values": [0, "4px", "8px"]
        },
        "zIndex": { "reason": "Use a stacking context." }
      }
    ]
  }
}
```

Every statically resolved fallback value must be allowed. Unresolved expressions, such as imported token references, pass an allowlist without proving that they are tokens, so an empty `values` array rejects only known literals.

#### Incorrect

```ts title="styles.ts"
import { style } from 'zyzz'

export namespace styles {
  export const modal = style({
    color: '#ff0000',
    // zyzz(restricted-properties): Property 'color' is restricted. Use color tokens.
    padding: '7px',
    // zyzz(restricted-properties): Property 'padding' is restricted. Use the spacing scale.
    zIndex: 10,
    // zyzz(restricted-properties): Property 'zIndex' is restricted. Use a stacking context.
  })
}
```

#### Correct

```ts title="styles.ts"
import { style } from 'zyzz'
import { vars } from 'zyzz/default'

export namespace styles {
  export const modal = style({
    color: vars.color.foreground,
    padding: '4px',
  })
}
```

## More

[Testing & Troubleshooting](/docs/guides/testing)

Test rendered styles, trace compilation failures, and check CSS delivery.

[Styling](/docs/guides/styling)

Compose definitions, override styles, and bind dynamic values.

[Oxlint Plugins](https://oxc.rs/docs/guide/usage/linter/js-plugins.html)

Configure JavaScript plugins, overrides, and suppression comments in Oxlint.

[Plugin Tests](https://github.com/wevm/zyzz/blob/main/src/oxlint/index.test.ts)

Exercise each rule through real Oxlint and Vite Plus processes.
