Skip to content

Commit 59b2804

Browse files
feat(naming): add opt-in allowedWords for Roblox names (#662)
strictCamelCase and StrictPascalCase reject two capitals in a row, so a CFrame had to be stored in targetCframe and the identifier stopped matching the type it held. flawless 1.7.0 adds allowedWords, which lets listed words keep their own casing while the rest of the name is still checked. Bumps flawless 1.5.1 -> 1.7.0 and adds `naming: { allowedWords }`, off by default. `true` uses the generated Roblox list, an array uses exactly those words. Set once as settings.flawless.namingConvention.allowedWords on isentinel/naming/setup, so it reaches the TS and TSX blocks and any user selectors; a selector opts out with its own `allowedWords: []`. The list is generated, not hand-maintained. Only names holding two capitals in a row can trip the strict formats, which is 82 of the 1534 names @rbxts/types declares. Pruning names a shorter word already resolves - CFrame absorbs CFrameValue, CFrameConstructor and UserCFrame, UDim absorbs UDim2 - leaves 67. roblox-allowed-words-shared.ts mirrors flawless's own applyAllowedWords so the pruning asks the question the rule asks at lint time. @rbxts/types is a devDependency and the scrape runs at build time: a consumer may pin a different version, or none, and parsing ~4MB of .d.ts on every config load to answer a question that only changes on a bump is not worth it. `nr check:roblox-words` runs in CI before the build, since `pnpm gen` would otherwise repair a stale snapshot in place and the drift would never be reported. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent c2ed278 commit 59b2804

14 files changed

Lines changed: 615 additions & 25 deletions

.github/workflows/ci.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ jobs:
7373
- name: Check type-aware snapshot
7474
run: nr check:type-aware
7575

76+
# Same reason again. A stale word list silently narrows what
77+
# `naming: { allowedWords: true }` lets through.
78+
- name: Check Roblox allowed-words snapshot
79+
run: nr check:roblox-words
80+
7681
- name: Build
7782
run: nr build
7883

CLAUDE.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ files that have to sit next to the code they augment, and
9696
`src/cli/constants-generated.ts`). Nothing in `src/generated/` is hand-edited,
9797
and both root lint configs ignore it.
9898

99-
`pnpm gen` runs eight generators in `scripts/`: `typegen.ts`
99+
`pnpm gen` runs nine generators in `scripts/`: `typegen.ts`
100100
(`src/typegen.d.ts` - ESLint rule types and config names), `typegen-oxlint.ts`
101101
(`src/oxlint/typegen.d.ts`, plus `src/generated/oxlint-native.ts` and
102102
`src/generated/oxlint-capabilities.ts` - the native and jsPlugin capability sets
@@ -105,7 +105,10 @@ severities, used by the redundancy check), `versiongen.ts`
105105
(`src/cli/constants-generated.ts`), `stylistic-gen.ts`
106106
(`src/generated/stylistic.ts` - `@stylistic` rule names), `type-aware-gen.ts`
107107
(`src/generated/type-aware.ts` - the `requiresTypeChecking` snapshot the
108-
type-aware split reads) and `gen-package-extensions.ts` (the `packageExtensions`
108+
type-aware split reads), `roblox-allowed-words-gen.ts`
109+
(`src/generated/roblox-allowed-words.ts` - the Roblox names
110+
`naming: { allowedWords: true }` hands to `flawless/naming-convention`, scraped
111+
from `@rbxts/types`) and `gen-package-extensions.ts` (the `packageExtensions`
109112
block of `pnpm-workspace.yaml`). Run it after modifying configs.
110113

111114
`typegen-oxlint.ts` boots the real ESLint factory to learn which rules the
@@ -115,10 +118,11 @@ the committed output changes depending on who ran it.
115118

116119
`typegen.ts` and `type-aware-gen.ts` share one list of config modules
117120
(`scripts/config-factories.ts`); add new modules there or they escape both
118-
generators. Both `gen-package-extensions.ts` and `type-aware-gen.ts` take
119-
`--check` (`nr check:extensions`, `nr check:type-aware`), which CI runs _before_
120-
the build - `pnpm gen` would otherwise repair a stale file in place and the
121-
drift would never be reported.
121+
generators. `gen-package-extensions.ts`, `type-aware-gen.ts` and
122+
`roblox-allowed-words-gen.ts` all take `--check` (`nr check:extensions`,
123+
`nr check:type-aware`, `nr check:roblox-words`), which CI runs _before_ the
124+
build - `pnpm gen` would otherwise repair a stale file in place and the drift
125+
would never be reported.
122126

123127
### CLI Tools
124128

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,49 @@ selectors are simply added. A less specific entry cannot override a more
771771
specific default. The `overridesTypeAware` escape hatch still replaces the whole
772772
rule if you need full control.
773773

774+
##### Roblox names
775+
776+
`strictCamelCase` and `StrictPascalCase` reject two capitals in a row, so a
777+
`CFrame` has to be stored in `targetCframe` and the identifier stops matching
778+
the type it holds. `allowedWords` lets listed words keep their own casing inside
779+
a name, while the rest of the name is still checked. It is off by default:
780+
781+
```ts
782+
// eslint.config.ts
783+
import isentinel from "@isentinel/eslint-config";
784+
785+
export default isentinel({
786+
naming: {
787+
// Use ROBLOX_ALLOWED_WORDS, generated from `@rbxts/types`
788+
allowedWords: true,
789+
},
790+
});
791+
```
792+
793+
```ts
794+
const targetCFrame = new CFrame(); // ok
795+
const listLayoutUIPadding = 0; // ok
796+
const target_CFrame = 0; // still an error
797+
```
798+
799+
Pass an array to use exactly those words instead. Spread `ROBLOX_ALLOWED_WORDS`
800+
to extend the list rather than replace it:
801+
802+
```ts
803+
import isentinel, { ROBLOX_ALLOWED_WORDS } from "@isentinel/eslint-config";
804+
805+
export default isentinel({
806+
naming: {
807+
allowedWords: [...ROBLOX_ALLOWED_WORDS, "HTTP", "URL"],
808+
},
809+
});
810+
```
811+
812+
The list reaches every selector, including your own `selectors` entries. A
813+
selector opts out with its own `allowedWords: []`. Only the two strict formats
814+
honour it, so it can never loosen `snake_case` or `UPPER_CASE`, and a word only
815+
matches at a hump boundary, so it cannot split an existing hump.
816+
774817
#### Oxlint
775818

776819
The config can run alongside (or be replaced by)

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@
5757
"build:inspector": "pnpm build && pnpm dlx @eslint/config-inspector build --config eslint-inspector.config.ts --out-dir ./.eslint-config-inspector",
5858
"check:extensions": "node scripts/check-package-extensions.ts && node scripts/gen-package-extensions.ts --check",
5959
"check:published": "node scripts/check-published-extensions.ts",
60+
"check:roblox-words": "node scripts/roblox-allowed-words-gen.ts --check",
6061
"check:type-aware": "node scripts/type-aware-gen.ts --check",
6162
"dev": "npx @eslint/config-inspector --config eslint-inspector.config.ts",
62-
"gen": "node scripts/typegen.ts && node scripts/typegen-oxlint.ts && node scripts/typegen-defaults.ts && node scripts/typegen-defaults-oxlint.ts && node scripts/versiongen.ts && node scripts/stylistic-gen.ts && node scripts/type-aware-gen.ts && node scripts/gen-package-extensions.ts",
63+
"gen": "node scripts/typegen.ts && node scripts/typegen-oxlint.ts && node scripts/typegen-defaults.ts && node scripts/typegen-defaults-oxlint.ts && node scripts/versiongen.ts && node scripts/stylistic-gen.ts && node scripts/type-aware-gen.ts && node scripts/roblox-allowed-words-gen.ts && node scripts/gen-package-extensions.ts",
6364
"postgen": "echo 'Generation complete!'",
6465
"lint": "isentinel-lint",
6566
"oxlint": "oxlint",
@@ -134,6 +135,7 @@
134135
"@eslint/config-inspector": "catalog:dev",
135136
"@isentinel/eslint-config": "workspace:*",
136137
"@isentinel/tsconfig": "catalog:dev",
138+
"@rbxts/types": "catalog:dev",
137139
"@stylistic/eslint-plugin-migrate": "catalog:dev",
138140
"@total-typescript/shoehorn": "catalog:dev",
139141
"@types/node": "catalog:dev",

pnpm-lock.yaml

Lines changed: 28 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ignoreWorkspaceRootCheck: true
55

66
minimumReleaseAgeExclude:
77
- "@pobammer-ts/small-rules@2.14.0"
8+
- "@rbxts/types@1.0.943"
89
- eslint-plugin-flawless
910
- eslint-plugin-jest@29.16.0
1011

@@ -36,6 +37,7 @@ catalogs:
3637
"@eslint-react/shared": 5.17.3
3738
"@eslint/config-inspector": 3.1.0
3839
"@isentinel/tsconfig": 1.2.0
40+
"@rbxts/types": 1.0.943
3941
"@stylistic/eslint-plugin-migrate": 4.4.1
4042
"@total-typescript/shoehorn": 0.1.2
4143
"@types/node": 24.1.0
@@ -91,7 +93,7 @@ catalogs:
9193
eslint-plugin-better-max-params: 1.0.0
9294
eslint-plugin-comment-length: 2.3.1
9395
eslint-plugin-de-morgan: 2.1.3
94-
eslint-plugin-flawless: 1.5.1
96+
eslint-plugin-flawless: 1.7.0
9597
eslint-plugin-format-lua: 2.0.0
9698
eslint-plugin-import-lite: 0.6.0
9799
eslint-plugin-jsdoc: 63.2.2
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import fs from "node:fs/promises";
2+
import process from "node:process";
3+
4+
import { deriveRobloxAllowedWords } from "./roblox-allowed-words-shared.ts";
5+
6+
/**
7+
* Snapshot the Roblox names that `naming: { allowedWords: true }` hands to
8+
* `flawless/naming-convention` at build time.
9+
*
10+
* The names are scraped out of `@rbxts/types`, which ships ~4MB of `.d.ts`
11+
* across three files. Reading those on every config load - to answer a question
12+
* whose answer only changes when the package is bumped - would cost every
13+
* consumer far more than the snapshot does, and `@rbxts/types` is a
14+
* devDependency here, so a consumer may not have the same version installed (or
15+
* any). `roblox-allowed-words.spec.ts` fails when the snapshot drifts from the
16+
* installed package.
17+
*
18+
* `--check` reports drift instead of writing, for the same reason
19+
* `type-aware-gen.ts` has one: it has to run before anything that runs
20+
* `pnpm gen`, which repairs the file in place and hides the drift.
21+
*/
22+
23+
const OUTPUT = new URL("../src/generated/roblox-allowed-words.ts", import.meta.url);
24+
25+
/**
26+
* Renders the snapshot, or reports the drift when checking.
27+
*
28+
* @returns The process exit code.
29+
*/
30+
async function main(): Promise<number> {
31+
const checking = process.argv.includes("--check");
32+
const words = await deriveRobloxAllowedWords();
33+
const desired = [
34+
"// Generated by scripts/roblox-allowed-words-gen.ts — do not edit.",
35+
`export const ROBLOX_ALLOWED_WORDS: ReadonlyArray<string> = ${JSON.stringify(words, null, 2)};`,
36+
"",
37+
].join("\n");
38+
39+
// An unreadable (or absent) file can never equal `desired`, so it falls
40+
// through to the write or the drift report.
41+
const current = await fs.readFile(OUTPUT, "utf8").catch(() => "");
42+
if (current === desired) {
43+
if (!checking) {
44+
console.log(`[roblox-allowed-words] ${words.length} words.`);
45+
}
46+
47+
return 0;
48+
}
49+
50+
if (checking) {
51+
console.error(
52+
"[roblox-allowed-words] src/generated/roblox-allowed-words.ts is out of " +
53+
"sync with the installed @rbxts/types. Run `pnpm gen` and commit the result.",
54+
);
55+
return 1;
56+
}
57+
58+
await fs.writeFile(OUTPUT, desired);
59+
return 0;
60+
}
61+
62+
process.exit(await main());

0 commit comments

Comments
 (0)