# eslint-plugin-gameface

Check the full documentation [here](https://frontend-tools.coherent-labs.com/esling-plugin-gameface/).

ESLint plugin for [Gameface](https://coherent-labs.com/). It ships a **flat preset** that runs [**@html-eslint**](https://html-eslint.org/) on `*.html`, [**@eslint/css**](https://github.com/eslint/css) on `*.css`, and **data-driven `gameface/*` rules** backed by JSON under [`gameface-features/`](./gameface-features/) (regenerated by your feature extractor).

## Install

```bash
npm install --save-dev eslint eslint-plugin-gameface
```

Requires **ESLint >= 9.15** (flat config). HTML/CSS parsers and the TypeScript parser used by the preset are **dependencies of this package**—no separate install. Node.js LTS is recommended; see `@eslint/css` `engines` if you hit environment issues.

## Use the preset

```js
// eslint.config.js
import gameface from "eslint-plugin-gameface";

export default [
  { ignores: ["**/node_modules/**"] },
  ...gameface.configs["flat/recommended"],
];
```

### Settings

The preset sets `settings.gameface.modelsDir` to `"Gameface-models"` (resolved relative to the ESLint `cwd`) for **`gameface/html-databind-model-properties`** and other tooling.

- **`settings.gameface.customDataBindAttributes`** (optional `string[]`): extra allowed `data-bind-*` attribute names for **`gameface/html-databind-spelling`** (same idea as `.cohtmllintrc` `customDataBindAttributes` in [HTMLLint](g:/frontend-tools/eslint-gameface/HTMLLint)).

- **`settings.gameface.version`** (optional string): which Gameface feature catalog to use. Defaults to **`"latest"`** (JSON shipped under `gameface-features/` next to `css/`, `html/`, etc.). If you add a directory **`gameface-features/versions/<version>/`** with the same layout (`css/`, `html/`, `selectors/`, `js/`), set `version` to that `<version>` string and rules load those files instead. Values containing `..`, `/`, or `\` are ignored (falls back to latest). Set `settings.gameface.version` on ESLint `files` globs that run Gameface rules (HTML, CSS, JSX) when you use a versioned catalog.

The JSON-driven rules pick up `version` automatically. Collector false positives are corrected internally in [`src/data/catalog-whitelists.js`](./src/data/catalog-whitelists.js) (not configurable in ESLint).

## `gameface-features/` data

| Path | Used by |
| --- | --- |
| `gameface-features/css/{supported,partial,unsupported}.json` | `gameface/css-no-unsupported-properties`, `gameface/css-partial-property-values`, HTML embedded/inline CSS rules |
| `gameface-features/functions/{supported,unsupported}.json` | `gameface/css-no-unsupported-functions`, HTML embedded/inline and JSX inline CSS function rules (`missing` rows only; `unknown` rows such as `url` are excluded) |
| `gameface-features/html/{supported,partial,unsupported}.json` | `gameface/html-parsed-no-impl`, `gameface/html-partial-features` |
| `gameface-features/selectors/{supported,partial,unsupported}.json` | `gameface/css-no-unsupported-selectors`, `gameface/css-partial-selectors` |
| `gameface-features/js/*.json` | `gameface/js-no-unsupported-globals`, `gameface/js-partial-member-access` |

`summary.json` files are informational only; rules read the array catalogs.

Versioned catalogs live under **`gameface-features/versions/<version>/`** (same subpaths as above). See **`settings.gameface.version`** in [Settings](#settings).

**Publishing:** `package.json` `"files"` includes `gameface-features` so npm tarballs ship the JSON.

## Rules

| Rule id | Default in `flat/recommended` | Description |
| --- | --- | --- |
| `gameface/css-no-unsupported-properties` | `error` (CSS) | Property in `css/unsupported.json` (missing/unsupported). |
| `gameface/css-no-unsupported-functions` | `error` (CSS/SCSS/HTML/JSX inline) | CSS function name in `functions/unsupported.json` with `status: "missing"` (e.g. `clamp`, `min`). Rows with `status: "unknown"` are not linted. |
| `gameface/css-partial-property-values` | `error` (CSS) | Keyword values from partial catalog (`unsupportedValues` ∪ `logRejectedValues` minus `supportedValues`). Length/unit values (e.g. `width: 10px`) are not flagged when the catalog row lists keyword gates or `supportedUnits` only. Declaration-level `partialPropertyCatalog` applies only to rows **without** keyword lists (e.g. non-suppressed `probe` / `note`); `value-accepted-but-not-computed` probes are skipped. |
| `gameface/html-embedded-css-no-unsupported-functions` | `error` (HTML) | Same function catalog as `css-no-unsupported-functions` inside `<style>` blocks. |
| `gameface/html-inline-css-no-unsupported-functions` | `error` (HTML) | Same function catalog on `style="..."` attributes. |
| `gameface/jsx-inline-css-no-unsupported-functions` | `error` (JSX) | Same function catalog on JSX `style` string values. |
| `gameface/css-no-var-in-keyframes` | `error` (CSS/SCSS/HTML/JSX) | Doc constraint: `var()` inside `@keyframes`. |
| `gameface/css-no-calc-in-keyframes` | `error` (CSS/SCSS/HTML/JSX) | Doc constraint: `calc()` inside `@keyframes`. |
| `gameface/css-var-no-fallback` | `error` (CSS/SCSS/HTML/JSX) | Doc constraint: `var(--name, fallback)` second argument. |
| `gameface/css-calc-no-mixed-percent-units` | `error` (CSS/SCSS/HTML/JSX) | Doc constraint: `calc()` mixing `%` with other length units (e.g. `50% - 20px`). |
| `gameface/html-embedded-css-no-var-in-keyframes` | `error` (HTML) | Same as `css-no-var-in-keyframes` in `<style>`. |
| `gameface/html-embedded-css-no-calc-in-keyframes` | `error` (HTML) | Same as `css-no-calc-in-keyframes` in `<style>`. |
| `gameface/html-embedded-css-var-no-fallback` | `error` (HTML) | Same as `css-var-no-fallback` in `<style>`. |
| `gameface/html-embedded-css-calc-no-mixed-percent-units` | `error` (HTML) | Same as `css-calc-no-mixed-percent-units` in `<style>`. |
| `gameface/html-inline-css-no-var-in-keyframes` | `error` (HTML) | Same keyframes `var()` check on inline `style` when parseable as stylesheet. |
| `gameface/html-inline-css-no-calc-in-keyframes` | `error` (HTML) | Same keyframes `calc()` check on inline `style`. |
| `gameface/html-inline-css-var-no-fallback` | `error` (HTML) | Same `var()` fallback check on inline `style`. |
| `gameface/html-inline-css-calc-no-mixed-percent-units` | `error` (HTML) | Same `calc()` mixed-% check on inline `style`. |
| `gameface/jsx-inline-css-no-var-in-keyframes` | `error` (JSX) | Same keyframes `var()` check on JSX string `style`. |
| `gameface/jsx-inline-css-no-calc-in-keyframes` | `error` (JSX) | Same keyframes `calc()` check on JSX string `style`. |
| `gameface/jsx-inline-css-var-no-fallback` | `error` (JSX) | Same `var()` fallback check on JSX string `style`. |
| `gameface/jsx-inline-css-calc-no-mixed-percent-units` | `error` (JSX) | Same `calc()` mixed-% check on JSX string `style`. |
| `gameface/css-no-unsupported-selectors` | `error` (CSS) | Pseudo text **exactly** matches `selectors/unsupported.json` `name` (e.g. `:any-link`). |
| `gameface/css-partial-selectors` | `warn` (CSS) | Pseudo **source text** (including arguments, e.g. `:nth-child(2 of .x)`) exactly matches `selectors/partial.json` `name`. Simple `:nth-child(2)` is not listed and will not warn. |
| `gameface/html-parsed-no-impl` | `error` (HTML) | Tag is `parsed-no-impl` in `html/unsupported.json`. Default **`scope: "curated"`** only reports a focused set (e.g. `<select>`, table model, `<iframe>`, …) so `<h1>` / `<p>` are not flagged. Use **`scope: "all"`** to report every parsed-no-impl tag (still minus `ignoreTags`). |
| `gameface/html-partial-features` | `warn` (HTML) | Default `mode: "attribute-checks"`: for tags in `html/partial.json` with `evidence.checks`, warns only if you use an **HTML attribute** mapped to a `checks` entry that is `false` (e.g. `<img alt>` when `altProperty` is false). Use `mode: "tag-warn"` for the old blanket partial warning. `mode: "off"` disables. `warnAllowlist: true` warns on any tag not in `html/supported.json`. |
| `gameface/html-databind-spelling` | `error` (HTML) | Any attribute containing `data-bind` must be a known name: static list + `data-bind-style-<css>` where `<css>` is **not** listed as unsupported in `css/unsupported.json`, plus legacy Coherent style binds and `customDataBindAttributes`. |
| `gameface/html-databind-curly-brackets` | `error` (HTML) | `data-bind*` attribute values must contain `{{` and `}}`. |
| `gameface/html-databind-property-accessors` | `error` (HTML) | Catches broken `{{Model.}}` / split-brace / bracket-in-brace patterns (ported from HTMLLint). |
| `gameface/html-databind-bind-for` | `error` (HTML) | `iter:{{collection}}` or `index, iter:{{collection}}` (optional `[ {{a}}, {{b}} ]`). Rejects `;` and malformed iterators. |
| `gameface/html-databind-class-toggle` | `error` (HTML) | Each `;`-separated entry must be `class-name:condition` with `{{…}}` in the condition. |
| `gameface/html-databind-model-properties` | `error` (HTML) | Model paths in the opening tag must match JSON files under `settings.gameface.modelsDir` (ported from HTMLLint `model-properties`). |
| `gameface/jsx-databind-spelling` | `error` (JSX) | Same allowlist as `html-databind-spelling` on `data-bind*` JSX attributes. |
| `gameface/jsx-databind-curly-brackets` | `error` (JSX) | Same as `html-databind-curly-brackets`. |
| `gameface/jsx-databind-property-accessors` | `error` (JSX) | Same as `html-databind-property-accessors`. |
| `gameface/jsx-databind-bind-for` | `error` (JSX) | Same as `html-databind-bind-for`. |
| `gameface/jsx-databind-class-toggle` | `error` (JSX) | Same as `html-databind-class-toggle`. |
| `gameface/jsx-databind-model-properties` | `error` (JSX) | Same as `html-databind-model-properties` on the opening element. |
| `gameface/js-no-unsupported-globals` | `error` (JS/JSX/TS/TSX) | Flags references to globals listed in `js/unsupported.json` (e.g. `Chrome`, `new AbortController()`). Only flags unbound globals; skips bindings, imports, and non-computed member keys. |
| `gameface/js-partial-member-access` | `warn` (JS/JSX/TS/TSX) | Flags static `Type.member` where `member` is listed under `evidence.missing` for that type in `js/partial.json` (or on `js/unsupported.json` rows). Limited to `Identifier.member` shape; dynamic and computed access are not analyzed. |
| `gameface/html-svg-no-unsupported-elements` | `error` (HTML) | Inside `<svg>`, disallows unsupported tags and SMIL (`<animate>`, `<foreignObject>`, …) per [SVG Support](https://docs.coherent-labs.com/cpp-gameface/content_development/supported_features_tables/svgsupport/). |
| `gameface/jsx-svg-no-unsupported-elements` | `error` (JSX/TSX) | Same denylist for JSX `<svg>` trees. |
| `gameface/html-svg-mask-clip-path-conflict` | `warn` (HTML) | Warns when both `mask` and `clip-path` use `url(#…)` on the same SVG element (clip-path wins). |
| `gameface/jsx-svg-mask-clip-path-conflict` | `warn` (JSX/TSX) | JSX parity. |
| `gameface/html-svg-tspan-ignored` | `off` | Optional warn: `<tspan>` text is merged into parent `<text>` without separate styling. |
| `gameface/css-svg-keyframes-sizing-units` | `error` (CSS/SCSS) | In `@keyframes`, `width` / `height` / `font-size` must include units for Gameface SVG CSS animation. |
| `gameface/html-embedded-css-svg-keyframes-sizing-units` | `error` (HTML) | Same in `<style>` blocks. |
| `gameface/html-inline-css-svg-keyframes-sizing-units` | `error` (HTML) | Same in `style="..."` when parseable. |
| `gameface/jsx-inline-css-svg-keyframes-sizing-units` | `error` (JSX) | Same in JSX string `style`. |
| `gameface/css-svg-keyframes-path-arc-animation` | `warn` (CSS/SCSS) | Warns when `@keyframes` animates `d` with elliptical arc (`A`/`a`) commands. |
| `gameface/html-embedded-css-svg-keyframes-path-arc-animation` | `warn` (HTML) | Same in `<style>`. |
| `gameface/html-inline-css-svg-keyframes-path-arc-animation` | `warn` (HTML) | Same in inline `style`. |
| `gameface/jsx-inline-css-svg-keyframes-path-arc-animation` | `warn` (JSX) | Same in JSX string `style`. |
| `gameface/html-svg-stroke-dash-non-path` | `error` (HTML) | `stroke-dasharray` / `stroke-dashoffset` only on `<path>`, not other SVG shapes (engine limit). |
| `gameface/jsx-svg-stroke-dash-non-path` | `error` (JSX/TSX) | JSX parity (attributes and inline `style`). |
| `gameface/css-svg-stroke-dash-non-path` | `error` (CSS/SCSS) | Same properties in rules that target non-path SVG tags (e.g. `circle { stroke-dasharray: … }`). |
| `gameface/html-embedded-css-svg-stroke-dash-non-path` | `error` (HTML) | Same in `<style>` blocks. |
| `gameface/css-svg-keyframes-stroke-dash-path-only` | `warn` (CSS/SCSS) | `@keyframes` that animate `stroke-dasharray` / `stroke-dashoffset` only run on `<path>`. |
| `gameface/html-embedded-css-svg-keyframes-stroke-dash-path-only` | `warn` (HTML) | Same in `<style>`. |
| `gameface/html-inline-css-svg-keyframes-stroke-dash-path-only` | `warn` (HTML) | Same in inline `style`. |
| `gameface/jsx-inline-css-svg-keyframes-stroke-dash-path-only` | `warn` (JSX) | Same in JSX string `style`. |

## Data binding (HTMLLint parity)

The legacy [HTMLLint](g:/frontend-tools/eslint-gameface/HTMLLint) package (HTMLHint + `.cohtmllintrc`) is superseded on the ESLint path by the **`gameface/html-databind-*`** and **`gameface/jsx-databind-*`** rules above. You can keep HTMLLint for non-ESLint workflows; for ESLint + `@html-eslint`, use the preset rules and the same settings (`modelsDir`, `customDataBindAttributes`).

- **HTML partial:** Only attributes covered by the `CHECK_KEY_TO_HTML_ATTR` map in [`html-partial-features.js`](./src/rules/html-partial-features.js) are checked; JS-only gaps (`toDataURL`, `sheetProperty`, etc.) cannot be seen from markup alone.
- **CSS selectors:** Matching is **exact string** on the pseudo node source; authors using different spacing or equivalent forms may not match catalog keys.
- **CSS functions:** Only `functions/unsupported.json` rows with `status: "missing"` are reported. `unknown` rows (`url`, `image-set`, `cross-fade`) are omitted because the catalog could not auto-probe them. Nested calls (e.g. `calc(min(…))`) are checked per function node. JSX `style={{ … }}` object literals are not analyzed for functions (string `style` values only).
- **CSS doc restrictions:** `css-no-var-in-keyframes`, `css-no-calc-in-keyframes`, `css-var-no-fallback`, and `css-calc-no-mixed-percent-units` (and HTML/JSX variants) encode Gameface documentation limits that are separate from the function catalog. They are not inferred from `functions/supported.json` probes. JSX object `style` props are not analyzed for these rules either.
- **SVG:** Rules under `gameface/*-svg-*` follow the [SVG Support](https://docs.coherent-labs.com/cpp-gameface/content_development/supported_features_tables/svgsupport/) doc ([`gameface-svg-support.js`](./src/data/gameface-svg-support.js)), plus engine limits such as path-only `stroke-dash*` ([`svg-stroke-dash.js`](./src/utils/svg-stroke-dash.js)). HTML `<a>` outside `<svg>` is allowed; only SVG-namespace `<a>` is flagged. Standalone `.svg` asset files are not linted in v1.
- **JS API checks:** `gameface/js-no-unsupported-globals` and `gameface/js-partial-member-access` are **catalog-driven and static**. They only see references resolved as unbound globals (no enclosing binding) and member access of the shape `Identifier.member` (non-computed). Dynamic patterns like `obj.effect`, `globalThis['Chrome']`, or members reached through aliases are not analyzed. TypeScript files use `@typescript-eslint/parser` without type information (no `parserOptions.project`), so type inference is not used.
- **SCSS (`.scss`):** The preset uses `@eslint/css` with `languageOptions.tolerant: true` on `**/*.scss` so Sass syntax (`$variables`, `&` nesting, etc.) does not fail with parse errors. Gameface rules still run on declarations the parser recovers. Values that contain variables (e.g. `width: $component-width`) are not resolved, so keyword/unit checks may be skipped for those declarations. Native SCSS parsing in `@eslint/css` is still in progress ([eslint/css#90](https://github.com/eslint/css/issues/90)); for Sass-specific linting (naming, mixins, etc.) you can keep Stylelint alongside ESLint.
- **Coherent / vendor CSS properties (`coh-*`, etc.):** `gameface/css-no-unsupported-properties` only flags names in `css/unsupported.json`; it does **not** treat Coherent props as “unknown”. If you still see **unknown property** diagnostics, they usually come from **`@eslint/css` `css/no-invalid-properties`** (when enabled) or the **editor** (e.g. VS Code `css.lint.unknownProperties` / `unknownVendorSpecificProperties`). This plugin cannot change those engines. Mitigations: set `"css/no-invalid-properties": "off"` (or avoid enabling `@eslint/css` recommended rules that turn it on) for Gameface CSS globs, add [VS Code `css.customData`](https://code.visualstudio.com/docs/languages/css#_css-specific-settings) for your vendor properties, or relax editor CSS lint settings.

## Development

```bash
# From repo root eslint-gameface/
npm run lint
npm test
```

`npm test` runs [`tests/feature-index-smoke.mjs`](./tests/feature-index-smoke.mjs), [`tests/html-embedded-inline-jsx.mjs`](./tests/html-embedded-inline-jsx.mjs), [`tests/html-databind-rules.mjs`](./tests/html-databind-rules.mjs), [`tests/jsx-databind-rules.mjs`](./tests/jsx-databind-rules.mjs), [`tests/js-api-rules.mjs`](./tests/js-api-rules.mjs), and [`tests/catalog-lint-matrix.mjs`](./tests/catalog-lint-matrix.mjs). Fixture notes: [`tests/fixtures/README.md`](./tests/fixtures/README.md).

### Catalog-driven lint matrix

After updating `gameface-features/` JSON, regenerate the sampled test matrix and run catalog lint checks:

```bash
# From repo root eslint-gameface/
npm run test:extract-matrix   # writes tests/generated/test-matrix.json
npm run test:catalog          # ESLint lintText per matrix row (isolated rule config)
npm run test:matrix:check     # CI: fail if committed matrix ≠ fresh extract (ignores generatedAt)
```

The extractor ([`scripts/extract-catalog-test-matrix.mjs`](./scripts/extract-catalog-test-matrix.mjs)) samples catalog rows with a fixed seed (`42`) and per-category caps, plus fixed databind scenarios and control rows. Each matrix row carries `ruleId`, `snippet`, and `expect.reports` so [`tests/catalog-lint-matrix.mjs`](./tests/catalog-lint-matrix.mjs) can assert the intended rule fires (or stays silent for controls).

## Legacy migration (optional)

Older HTMLHint / Stylelint workflows can be replaced incrementally.

## License

MIT
