@heyramzi/lint 1.34.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -5
- package/bin/heyramzi-comment-length.js +1 -3
- package/bin/heyramzi-old-names.js +72 -0
- package/bin/heyramzi-push-diff.js +16 -5
- package/bin/heyramzi-ui-rules.js +18 -9
- package/comment-length.js +18 -12
- package/config/oxlintrc.json +2 -3
- package/data/jev-code-bench.js +5 -5
- package/file-tokens.js +26 -13
- package/index.js +4 -6
- package/jev-code.js +1 -1
- package/old-names-mask.js +206 -0
- package/old-names.js +145 -0
- package/package.json +8 -2
- package/push-diff.js +2 -2
- package/repo-rules/packages.js +5 -5
- package/repo-rules.js +2 -2
- package/rules/convex-index-name.js +3 -1
- package/rules/convex-internal-only.js +2 -1
- package/rules/{convex-no-deprecated-crons.js → convex-no-hourly-daily-crons.js} +8 -7
- package/rules/convex-runtime-imports.js +2 -1
- package/rules/{no-legacy-compat.js → no-underscore-reexport.js} +1 -1
- package/ui-rules-css.js +221 -0
- package/ui-rules-markup.js +130 -0
- package/ui-rules.js +94 -23
- package/vendor/old-names-words.js +9 -0
- package/rules/no-relative-type-imports.js +0 -40
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Shared lint rules for Studio projects: one oxlint plugin, the oxlint base config
|
|
|
6
6
|
|
|
7
7
|
- **Oxlint plugin**: the rules below, loaded through `jsPlugins`.
|
|
8
8
|
- **Oxlint base config**: `@heyramzi/lint/oxlint`.
|
|
9
|
-
- **Gates**: `heyramzi-commit-msg`, `heyramzi-comment-length`, `heyramzi-file-tokens`, `heyramzi-inline-path`, `heyramzi-jev-code`, `heyramzi-repo-rules`, `heyramzi-slop` and `heyramzi-svelte-markup`.
|
|
9
|
+
- **Gates**: `heyramzi-commit-msg`, `heyramzi-comment-length`, `heyramzi-file-tokens`, `heyramzi-inline-path`, `heyramzi-jev-code`, `heyramzi-old-names`, `heyramzi-repo-rules`, `heyramzi-slop` and `heyramzi-svelte-markup`.
|
|
10
10
|
|
|
11
11
|
There's no ESLint side. The Studio stack doesn't run ESLint, so the flat configs and the
|
|
12
12
|
`heyramzi-lint` pre-commit CLI are gone.
|
|
@@ -31,7 +31,6 @@ deleted on 14 Sep 2026.
|
|
|
31
31
|
| `no-class` | Prevents the use of classes in favor of functional composition. |
|
|
32
32
|
| `no-default-export` | Enforces named exports for better discoverability and refactoring. |
|
|
33
33
|
| `no-relative-imports` | Error in every preset. Blocks relative specifiers in `import`, `export … from`, `import()`, `require` and `vi.mock`; only `./$types` is exempt. `--fix` rewrites each to the most specific alias covering the target: package.json `imports` first, then tsconfig `paths` for files that tsconfig includes. `.mjs`, `.cjs` and `*.config.*` files get `imports` only, because they run before a bundler applies aliases. A target no alias covers is reported with no fix: add the alias. |
|
|
34
|
-
| `no-relative-type-imports` | Deprecated and off: `no-relative-imports` covers type imports. |
|
|
35
34
|
| `no-em-dash` | Error. Em and en dashes in comments, strings, template text and Svelte text. A spaced hyphen (` - `) fails there too, except where it isn't a dash: a string whose whole value is one em or en dash (an empty-value placeholder), a string or template that is code judged on content (SQL keywords like `SELECT … FROM` or `WHERE`, CSS `calc(` or `var(--`, GLSL `gl_` or `vec2`, JS `function x(`, `=>` or `const x =`), a markdown bullet at the start of a template line or comment line, `<style>`, and `sql`/`css`/`gql`/`html`/`raw` templates. Test and fixture files (`*.test.*`, `*.spec.*`, `__fixtures__/`, `fixtures/`) pass a spaced hyphen because their strings are inputs under test. A real em or en dash fails everywhere except a bare placeholder and a tagged template. |
|
|
36
35
|
| `no-svelte4-syntax` | Error. `export let` and `$:` in a `.svelte` file. Svelte 4 markup is the markup gate's job. |
|
|
37
36
|
| `no-app-stores` | Error. An import from `$app/stores`; read `page` from `$app/state`. |
|
|
@@ -42,7 +41,7 @@ deleted on 14 Sep 2026.
|
|
|
42
41
|
| `no-redundant-prop-fallback` | Error. A `\|\|` or `??` fallback repeating the default a prop already has, in parameters, `$props()` and markup. |
|
|
43
42
|
| `no-vitest-import-in-vite-plus` | Error. An import from `vitest` in a Vite+ package with no `vitest` dependency of its own; import from `vite-plus/test`. |
|
|
44
43
|
| `no-skipped-or-focused-tests` | Error. `.only`, `.skip("title", fn)`, `xit`, `fit` and kin. A Playwright conditional skip, `test.skip(cond, "reason")`, passes. |
|
|
45
|
-
| `convex-*` | 10 rules, an `overrides` entry on `**/convex/**` in the base config. Error: `require-args` (and the old function syntax), `no-
|
|
44
|
+
| `convex-*` | 10 rules, an `overrides` entry on `**/convex/**` in the base config. Error: `require-args` (and the old function syntax), `no-hourly-daily-crons`, `internal-only` (crons, scheduler, HTTP actions), `no-get-metadata`, `runtime-imports`. Warn while old hits remain: `require-returns`, `no-filter`, `no-v-any`, `no-date-now-in-query`, `index-name`. `convex-react-client-module-scope` is error everywhere. Replaces the `@convex-dev/eslint-plugin` the skill claimed and no repo installed. |
|
|
46
45
|
|
|
47
46
|
## The alias convention `no-relative-imports` enforces
|
|
48
47
|
|
|
@@ -154,7 +153,7 @@ It's a hard cap. There's no allowlist, no baseline and no `--update`: a file ove
|
|
|
154
153
|
until the comment is cut down, whether it's new or years old.
|
|
155
154
|
|
|
156
155
|
The same gate fails a comment that marks a hack left in place: a `TODO`, `FIXME` or `HACK` tag,
|
|
157
|
-
"for now", a temporary fix, backward compat
|
|
156
|
+
"for now", a temporary fix, backward compat or `@deprecated`. Agents
|
|
158
157
|
copy these along with the hack. Fix what the comment names, then delete it. A word inside
|
|
159
158
|
backticks is a quote and passes. The list is `MARKERS` in `comment-length.js`.
|
|
160
159
|
|
|
@@ -204,7 +203,7 @@ crosses the cap, not at someone else's commit.
|
|
|
204
203
|
|
|
205
204
|
It skips pages, stylesheets and logs (`.html`, `.css`, `.log`), and what no person or agent
|
|
206
205
|
writes by hand: lockfiles, `CHANGELOG.md`, `*.generated.*`, minified bundles, source maps, snapshots, SVGs, binaries, generated and vendored trees (`dist`,
|
|
207
|
-
`_generated`, `vendor`, the `.claude/` projections, …), files whose first 2 KB carry a codegen
|
|
206
|
+
`_generated`, `vendor`, `.obsidian` plugin bundles, the `.claude/` projections, …), files whose first 2 KB carry a codegen
|
|
208
207
|
header (`@generated`, `Do not edit`, `Generated by`), and paths `.gitattributes` marks
|
|
209
208
|
`linguist-generated`. A pure rename passes. There's no config and no allowlist: a hand-written
|
|
210
209
|
file over the cap is split, and the refusal says how.
|
|
@@ -223,6 +222,23 @@ classes in Swift, zoom-blocking viewports, `user-select: none` on body, `outline
|
|
|
223
222
|
`@heyramzi/ui/src`, `adapter-cloudflare` without `paths.relative: false`, and a `static/` file under a route.
|
|
224
223
|
No allowlist and no config.
|
|
225
224
|
|
|
225
|
+
Interface rules, from Jakub Krehel's checklist (in `ui-rules-css.js` and `ui-rules-markup.js`). They judge
|
|
226
|
+
`.svelte`, `.css`, `.ts` and `.tsx`, never `.html` exports, and skip comments and `<code>`:
|
|
227
|
+
|
|
228
|
+
- **CSS and Tailwind:** `will-change-all`, `will-change-layout` (anything but transform, opacity, filter),
|
|
229
|
+
`press-scale-floor` (an `active` scale under 0.95), `keyframe-toggle` (an `animation` on hover or open
|
|
230
|
+
state; the shared collapsible and accordion height keyframes pass), `light-small-text` (font-thin,
|
|
231
|
+
extralight or light with text-xs, sm or base), `line-height-unit` (px or rem), `tracking-px` (px or rem
|
|
232
|
+
letter-spacing), `raw-font-tags` (`wght`, `opsz`, `tnum`, `zero` in the font-*-settings properties),
|
|
233
|
+
`balance-paragraph` (`text-balance` on a `<p>`), `text-justify`. The `line-height: 24px`-style
|
|
234
|
+
declarations aren't read in plain `.ts`, where they're emails and OG images.
|
|
235
|
+
- **Markup, `.svelte` only:** `div-onclick` (a `<div>` or `<span>` with `onclick` and no role),
|
|
236
|
+
`role-no-keys` (role button or tab with `onclick`, no key handler, no `use:` or spread),
|
|
237
|
+
`img-no-alt`, `aria-hidden-focusable`, `input-autocomplete` (email, password or tel), `block-paste`
|
|
238
|
+
(a handler that only cancels the paste).
|
|
239
|
+
- **Any file:** `positive-tabindex`, `locale-less-date` (`toLocaleDateString()`, or `toLocaleTimeString()`,
|
|
240
|
+
with no locale).
|
|
241
|
+
|
|
226
242
|
### Svelte Markup Gate
|
|
227
243
|
|
|
228
244
|
oxlint never reads a `.svelte` template, and skips a file with no `<script>`, so
|
|
@@ -238,6 +254,37 @@ pnpm exec heyramzi-svelte-markup --all # every tracked .svelte file
|
|
|
238
254
|
Generated and vendored trees are skipped (`DEFAULT_IGNORED` in `svelte-markup.js`). It runs in the
|
|
239
255
|
managed `push gates` block. No allowlist and no config.
|
|
240
256
|
|
|
257
|
+
### Old Names Gate
|
|
258
|
+
|
|
259
|
+
"None of our repos should have legacy" (Ramzi, 22 Oct 2026). `heyramzi-old-names` fails a repo
|
|
260
|
+
that keeps a name for how things used to work. The house rule it enforces: a refactor leaves no
|
|
261
|
+
compat layer, so no alias export, no `oldField?` kept for a migration and no shim.
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
pnpm exec heyramzi-old-names # the pushed files, or every tracked one outside a push
|
|
265
|
+
pnpm exec heyramzi-old-names --all # every tracked file, for an audit
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- **The word `legacy`**, in any case and inside a longer word (`fromLegacy`, `LEGACY_KID`,
|
|
269
|
+
`monthlyValueLegacy`): in identifiers, comments, strings, template text and markup, and in file
|
|
270
|
+
and folder names.
|
|
271
|
+
- **`compat`, `deprecated` and `shim`** (and `shims`), as a word part of a name only: an
|
|
272
|
+
identifier, a JSON, YAML or TOML key, a CSS or HTML name, a file or folder name. `toCompat`,
|
|
273
|
+
`deprecatedField` and `api-shim.ts` fail. `compatible`, `shimmer` and the word in a comment or
|
|
274
|
+
a string value pass, because they are different words or prose.
|
|
275
|
+
- **Files read:** `ts tsx mts cts js jsx mjs cjs svelte swift py sql sh bash zsh css html json
|
|
276
|
+
jsonc yaml yml toml`. Prose (`md`, `mdx`, `txt`) is out of scope.
|
|
277
|
+
- **Skipped:** what the file-token gate already treats as a tool's output (lockfiles,
|
|
278
|
+
`*.generated.*`, minified bundles, `dist`, `_generated`, `vendor`, `.svelte-kit`, `.obsidian`
|
|
279
|
+
plugin bundles, the `.claude` projections, …), a file whose first 2 KB carry a codegen header (`worker-configuration.d.ts`),
|
|
280
|
+
a path `.gitattributes` marks `linguist-generated`, and any `migrations/` or `drizzle/` folder,
|
|
281
|
+
because an applied migration is history the database already ran.
|
|
282
|
+
|
|
283
|
+
Each hit prints the file, line and token, then says to delete the old path and move every caller,
|
|
284
|
+
or rename the thing to what it is now. No allowlist, no baseline, no config, no ignore marker.
|
|
285
|
+
Installed with the other push gates by `bash ai-doc/scripts/pre-push-hooks.sh`. The source spells
|
|
286
|
+
the banned words in pieces, so it passes its own gate.
|
|
287
|
+
|
|
241
288
|
### Repo Rules Gate
|
|
242
289
|
|
|
243
290
|
`heyramzi-repo-rules` judges staged files, and `--all` every tracked file for an audit. Each
|
|
@@ -109,9 +109,7 @@ function gate(files) {
|
|
|
109
109
|
console.error(`${file}:${line}: ${label}`);
|
|
110
110
|
}
|
|
111
111
|
if (markers.length > 0) {
|
|
112
|
-
console.error(
|
|
113
|
-
"\nFix what the comment names, or delete it. No legacy name survives a refactor.",
|
|
114
|
-
);
|
|
112
|
+
console.error("\nFix what the comment names, or delete it. No old name survives a refactor.");
|
|
115
113
|
}
|
|
116
114
|
exit(1);
|
|
117
115
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The old-names gate over pushed files (every tracked one outside a push), `--all`, or named files.
|
|
4
|
+
* Usage: this package's README, "Old Names Gate".
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { exit } from "node:process";
|
|
11
|
+
|
|
12
|
+
import { gateFiles } from "#lint/gate-files.js";
|
|
13
|
+
import { check, OLD_NAME_EXTENSIONS, report, selectFiles } from "#lint/old-names.js";
|
|
14
|
+
|
|
15
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
16
|
+
// Run from a product inside a monorepo, the gate reads that product's files, not its siblings'.
|
|
17
|
+
const scope = execFileSync("git", ["rev-parse", "--show-prefix"], { encoding: "utf8" })
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/\/$/, "");
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
if (args.includes("--all")) delete process.env.PUSH_FILES;
|
|
23
|
+
const targets = args.filter((arg) => !arg.startsWith("-"));
|
|
24
|
+
|
|
25
|
+
const candidates =
|
|
26
|
+
targets.length > 0
|
|
27
|
+
? targets
|
|
28
|
+
: process.env.PUSH_FILES !== undefined
|
|
29
|
+
? process.env.PUSH_FILES.split("\n").filter(Boolean)
|
|
30
|
+
: repoFiles();
|
|
31
|
+
const files = notMarkedGenerated(gateFiles(repoRoot, selectFiles(candidates)));
|
|
32
|
+
|
|
33
|
+
const hits = check(
|
|
34
|
+
Object.fromEntries(files.map((file) => [file, readFileSync(path.join(repoRoot, file), "utf8")])),
|
|
35
|
+
);
|
|
36
|
+
if (hits.length === 0) {
|
|
37
|
+
console.log(`old-names: ${files.length} file(s), no old name`);
|
|
38
|
+
} else {
|
|
39
|
+
console.error(report(hits));
|
|
40
|
+
exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Tracked files plus new ones git hasn't seen yet, minus any deleted from the working tree. */
|
|
44
|
+
function repoFiles() {
|
|
45
|
+
const patterns = OLD_NAME_EXTENSIONS.map((ext) => `*.${ext}`);
|
|
46
|
+
const args = ["ls-files", "--cached", "--others", "--exclude-standard", "--deduplicate"];
|
|
47
|
+
return execFileSync("git", [...args, "--full-name", "--", ...patterns], {
|
|
48
|
+
cwd: path.join(repoRoot, scope),
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
51
|
+
})
|
|
52
|
+
.trim()
|
|
53
|
+
.split("\n")
|
|
54
|
+
.filter((file) => file && existsSync(path.join(repoRoot, file)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Drops the files `.gitattributes` marks linguist-generated, the way the file-token gate does. */
|
|
58
|
+
function notMarkedGenerated(list) {
|
|
59
|
+
if (list.length === 0) return list;
|
|
60
|
+
const out = execFileSync("git", ["check-attr", "-z", "--stdin", "linguist-generated"], {
|
|
61
|
+
cwd: repoRoot,
|
|
62
|
+
input: `${list.join("\0")}\0`,
|
|
63
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
64
|
+
})
|
|
65
|
+
.toString("utf8")
|
|
66
|
+
.split("\0");
|
|
67
|
+
const generated = new Set();
|
|
68
|
+
for (let index = 0; index + 2 < out.length; index += 3) {
|
|
69
|
+
if (out[index + 2] === "set" || out[index + 2] === "true") generated.add(out[index]);
|
|
70
|
+
}
|
|
71
|
+
return list.filter((file) => !generated.has(file));
|
|
72
|
+
}
|
|
@@ -59,11 +59,17 @@ function mergeBase() {
|
|
|
59
59
|
|
|
60
60
|
/** New path to old path for every added, copied, modified or renamed file. A pure move is R100 and changes nothing. */
|
|
61
61
|
function changedFiles() {
|
|
62
|
-
const fields = git([
|
|
63
|
-
"
|
|
64
|
-
|
|
62
|
+
const fields = git([
|
|
63
|
+
"diff",
|
|
64
|
+
"--name-status",
|
|
65
|
+
"-M",
|
|
66
|
+
"-z",
|
|
67
|
+
"--diff-filter=ACMR",
|
|
68
|
+
base,
|
|
69
|
+
"HEAD",
|
|
70
|
+
]).split("\0");
|
|
65
71
|
const map = new Map();
|
|
66
|
-
for (let index = 0; index < fields.length - 1;
|
|
72
|
+
for (let index = 0; index < fields.length - 1;) {
|
|
67
73
|
const status = fields[index];
|
|
68
74
|
const paired = status.startsWith("R") || status.startsWith("C");
|
|
69
75
|
const target = fields[index + (paired ? 2 : 1)];
|
|
@@ -87,7 +93,12 @@ function registryFiles(candidates) {
|
|
|
87
93
|
"--",
|
|
88
94
|
...code.slice(index, index + 200),
|
|
89
95
|
]);
|
|
90
|
-
found.push(
|
|
96
|
+
found.push(
|
|
97
|
+
...out
|
|
98
|
+
.split("\n")
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.map((line) => line.replace(/^HEAD:/, "")),
|
|
101
|
+
);
|
|
91
102
|
}
|
|
92
103
|
return found.filter((file) => hasStorageKeys(tryGit(["show", `HEAD:${file}`])));
|
|
93
104
|
}
|
package/bin/heyramzi-ui-rules.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* The UI-rules gate over staged or pushed files
|
|
4
|
-
*
|
|
5
|
-
* heyramzi-ui-rules # staged files, or PUSH_FILES when the pre-push hook sets it
|
|
6
|
-
* heyramzi-ui-rules --all # every tracked file
|
|
7
|
-
* heyramzi-ui-rules a.svelte # named files
|
|
3
|
+
* The UI-rules gate over staged or pushed files, `--all` for every tracked file, or named files.
|
|
4
|
+
* Usage: this package's README, "UI Rules Gate".
|
|
8
5
|
*/
|
|
9
6
|
|
|
10
7
|
import { execFileSync } from "node:child_process";
|
|
@@ -13,10 +10,18 @@ import path from "node:path";
|
|
|
13
10
|
import { exit } from "node:process";
|
|
14
11
|
|
|
15
12
|
import { gateFiles } from "#lint/gate-files.js";
|
|
16
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
checkFile,
|
|
15
|
+
checkProject,
|
|
16
|
+
durationNames,
|
|
17
|
+
ignoredPath,
|
|
18
|
+
report,
|
|
19
|
+
resolvedDurations,
|
|
20
|
+
} from "#lint/ui-rules.js";
|
|
17
21
|
|
|
18
22
|
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
19
|
-
const git = (args) =>
|
|
23
|
+
const git = (args) =>
|
|
24
|
+
execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
20
25
|
const lines = (text) => text.split("\n").filter(Boolean);
|
|
21
26
|
|
|
22
27
|
const args = process.argv.slice(2);
|
|
@@ -33,7 +38,10 @@ const candidates = named.length
|
|
|
33
38
|
? lines(git(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]))
|
|
34
39
|
: lines(process.env.PUSH_FILES);
|
|
35
40
|
|
|
36
|
-
const files = gateFiles(
|
|
41
|
+
const files = gateFiles(
|
|
42
|
+
repoRoot,
|
|
43
|
+
candidates.filter((file) => !ignoredPath(file)),
|
|
44
|
+
);
|
|
37
45
|
const read = (file) => {
|
|
38
46
|
const full = path.join(repoRoot, file);
|
|
39
47
|
return existsSync(full) ? readFileSync(full, "utf8") : null;
|
|
@@ -46,7 +54,8 @@ for (const file of files) {
|
|
|
46
54
|
for (const hit of checkFile(file, source)) hits.push({ file, ...hit });
|
|
47
55
|
}
|
|
48
56
|
hits.push(...checkProject(files, tracked, read));
|
|
49
|
-
if (hits.some((hit) => hit.rule === "duration-bare"))
|
|
57
|
+
if (hits.some((hit) => hit.rule === "duration-bare"))
|
|
58
|
+
hits = resolvedDurations(hits, durationNames(tracked, read));
|
|
50
59
|
|
|
51
60
|
if (hits.length === 0) {
|
|
52
61
|
console.log("ui-rules: no violations");
|
package/comment-length.js
CHANGED
|
@@ -150,10 +150,13 @@ function mergeRuns(src, spans) {
|
|
|
150
150
|
return merged;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
153
|
+
/**
|
|
154
|
+
* The comments of a file in `extension`'s language, as spans. Pass `strings` and the string and
|
|
155
|
+
* heredoc spans of Swift, Python, shell, CSS and SQL land in it.
|
|
156
|
+
*/
|
|
157
|
+
export function commentSpansFor(src, extension, strings) {
|
|
155
158
|
const syntax = syntaxOf(extension);
|
|
156
|
-
return syntax ? simpleSpans(src, syntax) : commentSpans(src);
|
|
159
|
+
return syntax ? simpleSpans(src, syntax, strings) : commentSpans(src);
|
|
157
160
|
}
|
|
158
161
|
|
|
159
162
|
function skipString(src, i, syntax) {
|
|
@@ -202,14 +205,16 @@ function skipHeredocs(src, i, pending) {
|
|
|
202
205
|
return j;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
function simpleSpans(src, syntax) {
|
|
208
|
+
function simpleSpans(src, syntax, strings) {
|
|
206
209
|
const spans = [];
|
|
207
210
|
const pending = [];
|
|
208
211
|
let i = 0;
|
|
209
212
|
while (i < src.length) {
|
|
210
213
|
const c = src[i];
|
|
211
214
|
if (c === "\n" && pending.length) {
|
|
215
|
+
const bodyStart = i;
|
|
212
216
|
i = skipHeredocs(src, i, pending.splice(0));
|
|
217
|
+
strings?.push({ start: bodyStart, end: i });
|
|
213
218
|
} else if (c === "\\" && syntax.escapes) {
|
|
214
219
|
i += 2;
|
|
215
220
|
} else if (
|
|
@@ -238,6 +243,7 @@ function simpleSpans(src, syntax) {
|
|
|
238
243
|
spans.push({ kind: "block", start: i, end: j });
|
|
239
244
|
i = j;
|
|
240
245
|
} else if (syntax.raw && c === "#" && rawStringEnd(src, i) !== null) {
|
|
246
|
+
strings?.push({ start: i, end: rawStringEnd(src, i) });
|
|
241
247
|
i = rawStringEnd(src, i);
|
|
242
248
|
} else if (
|
|
243
249
|
syntax.url &&
|
|
@@ -245,9 +251,13 @@ function simpleSpans(src, syntax) {
|
|
|
245
251
|
!/^url\(\s*["']/.test(src.slice(i, i + 12))
|
|
246
252
|
) {
|
|
247
253
|
const close = src.indexOf(")", i);
|
|
248
|
-
|
|
254
|
+
const stop = close === -1 ? src.length : close + 1;
|
|
255
|
+
strings?.push({ start: i, end: stop });
|
|
256
|
+
i = stop;
|
|
249
257
|
} else if (syntax.quotes.includes(c)) {
|
|
250
|
-
|
|
258
|
+
const stop = skipString(src, i, syntax);
|
|
259
|
+
strings?.push({ start: i, end: stop });
|
|
260
|
+
i = stop;
|
|
251
261
|
} else {
|
|
252
262
|
// `<<<` is a here-string, and its 2nd and 3rd `<` must not read as a heredoc opener.
|
|
253
263
|
const opener =
|
|
@@ -265,13 +275,13 @@ const REGEX_AFTER = new Set("(,=:[!&|?{};+-*%<>~^");
|
|
|
265
275
|
const REGEX_KEYWORD = /\b(?:return|typeof|case|in|of|delete|void|throw|new|else|do|yield|await)$/;
|
|
266
276
|
|
|
267
277
|
/** A `/` opens a regex after an operator or keyword, never after a value, so `a / b` stays division. */
|
|
268
|
-
function regexStarts(src, i) {
|
|
278
|
+
export function regexStarts(src, i) {
|
|
269
279
|
const before = src.slice(0, i).trimEnd();
|
|
270
280
|
return before === "" || REGEX_AFTER.has(before[before.length - 1]) || REGEX_KEYWORD.test(before);
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
/** Where a regex literal ends, flags included. A newline first means it was division after all. */
|
|
274
|
-
function regexEnd(src, i) {
|
|
284
|
+
export function regexEnd(src, i) {
|
|
275
285
|
let j = i + 1;
|
|
276
286
|
let inClass = false;
|
|
277
287
|
while (j < src.length) {
|
|
@@ -322,10 +332,6 @@ export const MARKERS = [
|
|
|
322
332
|
test: /\btemporar(?:y|ily)\s+(?:fix|workaround|hack|patch|solution)\b/i,
|
|
323
333
|
},
|
|
324
334
|
{ label: "backward compatibility", test: /\bbackwards?[\s-]*compat/i },
|
|
325
|
-
{
|
|
326
|
-
label: "a legacy name or path",
|
|
327
|
-
test: /\blegacy\s+(?:alias|synonym|shim|wrapper|fallback|export|import|path|name|field|support|code|compat)\b/i,
|
|
328
|
-
},
|
|
329
335
|
{ label: "a @deprecated tag", test: /@deprecated\b/ },
|
|
330
336
|
];
|
|
331
337
|
|
package/config/oxlintrc.json
CHANGED
|
@@ -58,8 +58,7 @@
|
|
|
58
58
|
"@heyramzi/lint/no-inline-svg": "error",
|
|
59
59
|
"@heyramzi/lint/no-inline-path": "error",
|
|
60
60
|
"@heyramzi/lint/no-relative-imports": "error",
|
|
61
|
-
"@heyramzi/lint/no-
|
|
62
|
-
"@heyramzi/lint/no-legacy-compat": "error",
|
|
61
|
+
"@heyramzi/lint/no-underscore-reexport": "error",
|
|
63
62
|
"@heyramzi/lint/convex-react-client-module-scope": "error",
|
|
64
63
|
"@heyramzi/lint/no-chained-type-assertions": "warn",
|
|
65
64
|
"@heyramzi/lint/no-conditional-empty-object-spread": "warn",
|
|
@@ -73,7 +72,7 @@
|
|
|
73
72
|
"@heyramzi/lint/convex-index-name": "warn",
|
|
74
73
|
"@heyramzi/lint/convex-internal-only": "error",
|
|
75
74
|
"@heyramzi/lint/convex-no-date-now-in-query": "warn",
|
|
76
|
-
"@heyramzi/lint/convex-no-
|
|
75
|
+
"@heyramzi/lint/convex-no-hourly-daily-crons": "error",
|
|
77
76
|
"@heyramzi/lint/convex-no-filter": "warn",
|
|
78
77
|
"@heyramzi/lint/convex-no-get-metadata": "error",
|
|
79
78
|
"@heyramzi/lint/convex-no-v-any": "warn",
|
package/data/jev-code-bench.js
CHANGED
|
@@ -13,12 +13,12 @@ export const TRAIN = [
|
|
|
13
13
|
code: `export async function load({ fetch }) {\n const body = await (await fetch("/api/me")).json();\n // @ts-ignore\n return { name: body.profile.name as string };\n}`,
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
rule: "
|
|
16
|
+
rule: "oldname",
|
|
17
17
|
file: "src/lib/billing.ts",
|
|
18
18
|
code: `export function chargeCustomer(id: string, cents: number) {\n return stripe.charges.create({ customer: id, amount: cents });\n}\n\n/** @deprecated use chargeCustomer */\nexport const charge = chargeCustomer;`,
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
rule: "
|
|
21
|
+
rule: "oldname",
|
|
22
22
|
file: "src/lib/user.ts",
|
|
23
23
|
code: `export function displayName(u: User) {\n const first = u.firstName ?? u.first_name;\n const last = u.lastName ?? u.last_name;\n return \`\${first} \${last}\`;\n}`,
|
|
24
24
|
},
|
|
@@ -95,7 +95,7 @@ export const TRAIN = [
|
|
|
95
95
|
{
|
|
96
96
|
rule: "leftover",
|
|
97
97
|
file: "src/lib/checkout.ts",
|
|
98
|
-
code: `export function checkout(cart: Cart) {\n console.log("cart here", cart);\n // const old =
|
|
98
|
+
code: `export function checkout(cart: Cart) {\n console.log("cart here", cart);\n // const old = oldCheckout(cart);\n // return old;\n return submit(cart);\n}`,
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
rule: "leftover",
|
|
@@ -206,7 +206,7 @@ export const HELDOUT = [
|
|
|
206
206
|
code: `export function handle(evt: Stripe.Event) {\n const session = evt.data.object as unknown as CheckoutSession;\n return session.customer_email!.toLowerCase();\n}`,
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
|
-
rule: "
|
|
209
|
+
rule: "oldname",
|
|
210
210
|
file: "src/lib/index.ts",
|
|
211
211
|
code: `export { renderCard } from "./card";\n// kept so old imports keep working\nexport { renderCard as drawCard } from "./card";`,
|
|
212
212
|
},
|
|
@@ -248,7 +248,7 @@ export const HELDOUT = [
|
|
|
248
248
|
{
|
|
249
249
|
rule: "leftover",
|
|
250
250
|
file: "Sources/Feed.swift",
|
|
251
|
-
code: `func refresh() async {\n print("DEBUG items", items.count)\n // items = try await
|
|
251
|
+
code: `func refresh() async {\n print("DEBUG items", items.count)\n // items = try await oldFetch()\n items = try await api.feed()\n}`,
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
rule: "workaround",
|
package/file-tokens.js
CHANGED
|
@@ -8,8 +8,8 @@ export const MAX_TOKENS = 5000;
|
|
|
8
8
|
export const MARKDOWN_MAX_TOKENS = 8000;
|
|
9
9
|
export const BYTES_PER_TOKEN = 4;
|
|
10
10
|
|
|
11
|
-
/** Generated, vendored or projected trees: nobody hand-writes these, so nobody splits them. */
|
|
12
|
-
export const
|
|
11
|
+
/** Generated, vendored or projected trees, and the vendored Obsidian plugin bundles: nobody hand-writes these, so nobody splits them. */
|
|
12
|
+
export const GENERATED_TREES = [
|
|
13
13
|
"node_modules",
|
|
14
14
|
"dist",
|
|
15
15
|
"build",
|
|
@@ -31,6 +31,11 @@ export const DEFAULT_IGNORED = [
|
|
|
31
31
|
".codex",
|
|
32
32
|
".gemini",
|
|
33
33
|
".opencode",
|
|
34
|
+
".obsidian",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_IGNORED = [
|
|
38
|
+
...GENERATED_TREES,
|
|
34
39
|
"raw",
|
|
35
40
|
// Boards are one document per subject by the playground skill's rule, and `playground check` gates them.
|
|
36
41
|
"playground",
|
|
@@ -84,23 +89,31 @@ export function tokens(bytes) {
|
|
|
84
89
|
return Math.ceil(bytes / BYTES_PER_TOKEN);
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
export function ignoredPath(file) {
|
|
89
|
-
const parts = file.split("/");
|
|
90
|
-
const name = parts.at(-1);
|
|
91
|
-
if (LOCKFILES.has(name) || DATA_EXTENSIONS.test(name) || UNCAPPED_EXTENSIONS.test(name))
|
|
92
|
-
return true;
|
|
93
|
-
if (GENERATED_NAMES.some((pattern) => pattern.test(name))) return true;
|
|
94
|
-
if (IGNORED_PREFIXES.some((prefix) => file.startsWith(prefix))) return true;
|
|
92
|
+
function inTrees(parts, trees) {
|
|
95
93
|
// Match the name as written (`.claude`) and with a symbol prefix stripped; stripping alone dropped every dot-folder.
|
|
96
94
|
return parts.slice(0, -1).some((part) => {
|
|
97
95
|
const lower = part.toLowerCase();
|
|
98
|
-
return (
|
|
99
|
-
DEFAULT_IGNORED.includes(lower) || DEFAULT_IGNORED.includes(lower.replace(/^\W+\s*/u, ""))
|
|
100
|
-
);
|
|
96
|
+
return trees.includes(lower) || trees.includes(lower.replace(/^\W+\s*/u, ""));
|
|
101
97
|
});
|
|
102
98
|
}
|
|
103
99
|
|
|
100
|
+
/** True when the path says a tool wrote or vendored the file: a lockfile, a generated name or a generated tree. */
|
|
101
|
+
export function generatedPath(file) {
|
|
102
|
+
const parts = file.split("/");
|
|
103
|
+
const name = parts.at(-1);
|
|
104
|
+
if (LOCKFILES.has(name)) return true;
|
|
105
|
+
if (GENERATED_NAMES.some((pattern) => pattern.test(name))) return true;
|
|
106
|
+
if (IGNORED_PREFIXES.some((prefix) => file.startsWith(prefix))) return true;
|
|
107
|
+
return inTrees(parts, GENERATED_TREES);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** True when the path says nobody caps the file: generated, data, a page, or a tree of boards and raw sources. */
|
|
111
|
+
export function ignoredPath(file) {
|
|
112
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
113
|
+
if (DATA_EXTENSIONS.test(name) || UNCAPPED_EXTENSIONS.test(name)) return true;
|
|
114
|
+
return generatedPath(file) || inTrees(file.split("/"), DEFAULT_IGNORED);
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
/** True when the content says a machine wrote it: a NUL byte (binary, as git decides) or a codegen header. */
|
|
105
118
|
export function generatedContent(head) {
|
|
106
119
|
if (head.includes(0)) return true;
|
package/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Code quality rules (from @upsys/config)
|
|
2
2
|
import noRelativeImports from "#lint/rules/no-relative-imports.js";
|
|
3
|
-
import noRelativeTypeImports from "#lint/rules/no-relative-type-imports.js";
|
|
4
3
|
import noDeepPhosphorImports from "#lint/rules/no-deep-phosphor-imports.js";
|
|
5
4
|
import noClass from "#lint/rules/no-class.js";
|
|
6
5
|
import noDefaultExport from "#lint/rules/no-default-export.js";
|
|
@@ -10,13 +9,13 @@ import maxObjectMethods from "#lint/rules/max-object-methods.js";
|
|
|
10
9
|
import maxFunctionLines from "#lint/rules/max-function-lines.js";
|
|
11
10
|
import noSnakeCaseProps from "#lint/rules/no-snake-case-props.js";
|
|
12
11
|
import noHardcodedModels from "#lint/rules/no-hardcoded-models.js";
|
|
13
|
-
import
|
|
12
|
+
import noUnderscoreReexport from "#lint/rules/no-underscore-reexport.js";
|
|
14
13
|
|
|
15
14
|
// Convex rules (the checks @convex-dev/eslint-plugin never ran here, plus the house rules)
|
|
16
15
|
import convexIndexName from "#lint/rules/convex-index-name.js";
|
|
17
16
|
import convexInternalOnly from "#lint/rules/convex-internal-only.js";
|
|
18
17
|
import convexNoDateNowInQuery from "#lint/rules/convex-no-date-now-in-query.js";
|
|
19
|
-
import
|
|
18
|
+
import convexNoHourlyDailyCrons from "#lint/rules/convex-no-hourly-daily-crons.js";
|
|
20
19
|
import convexNoFilter from "#lint/rules/convex-no-filter.js";
|
|
21
20
|
import convexNoGetMetadata from "#lint/rules/convex-no-get-metadata.js";
|
|
22
21
|
import convexNoVAny from "#lint/rules/convex-no-v-any.js";
|
|
@@ -60,7 +59,6 @@ export default {
|
|
|
60
59
|
rules: {
|
|
61
60
|
// Code quality
|
|
62
61
|
"no-relative-imports": noRelativeImports,
|
|
63
|
-
"no-relative-type-imports": noRelativeTypeImports,
|
|
64
62
|
"no-deep-phosphor-imports": noDeepPhosphorImports,
|
|
65
63
|
"no-class": noClass,
|
|
66
64
|
"no-default-export": noDefaultExport,
|
|
@@ -70,13 +68,13 @@ export default {
|
|
|
70
68
|
"max-function-lines": maxFunctionLines,
|
|
71
69
|
"no-snake-case-props": noSnakeCaseProps,
|
|
72
70
|
"no-hardcoded-models": noHardcodedModels,
|
|
73
|
-
"no-
|
|
71
|
+
"no-underscore-reexport": noUnderscoreReexport,
|
|
74
72
|
|
|
75
73
|
// Convex
|
|
76
74
|
"convex-index-name": convexIndexName,
|
|
77
75
|
"convex-internal-only": convexInternalOnly,
|
|
78
76
|
"convex-no-date-now-in-query": convexNoDateNowInQuery,
|
|
79
|
-
"convex-no-
|
|
77
|
+
"convex-no-hourly-daily-crons": convexNoHourlyDailyCrons,
|
|
80
78
|
"convex-no-filter": convexNoFilter,
|
|
81
79
|
"convex-no-get-metadata": convexNoGetMetadata,
|
|
82
80
|
"convex-no-v-any": convexNoVAny,
|
package/jev-code.js
CHANGED