lanekeep 0.8.1 → 0.9.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 +19 -14
- package/index.d.ts +276 -6
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
[](https://pypi.org/project/lanekeep/)
|
|
8
8
|
[](https://github.com/fmsouza/lanekeep/actions/workflows/ci.yml)
|
|
9
9
|
[](#license)
|
|
10
|
+
[](Cargo.toml)
|
|
10
11
|
|
|
11
12
|
lanekeep enforces the conventions that live in your team's heads and your reviewers' comments —
|
|
12
13
|
the ones a language model cannot infer from the code it is shown. Every rule is a codified answer
|
|
@@ -67,20 +68,18 @@ Three things follow from who reads the output:
|
|
|
67
68
|
the sandbox withholds the clock and randomness, so two runs over identical input produce
|
|
68
69
|
byte-identical output. An agent reading it twice must not see reordering as change.
|
|
69
70
|
- **It runs in the inner loop.** Agents and developers invoke it after every edit, so a warm run
|
|
70
|
-
is measured in tens of milliseconds
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`lanekeep init` scaffolds one of those four, so that is what a new TypeScript project meets
|
|
76
|
-
first. [`docs/architecture.md`](docs/architecture.md) §15 has the table and what is owed.
|
|
71
|
+
is measured in tens of milliseconds. The built-ins that ship as WebAssembly components are all
|
|
72
|
+
under 115 KB, so loading them is noise — a 12.4 MiB compiled-TypeScript component that once
|
|
73
|
+
cost new TypeScript projects ~6.5 seconds on their first run was reverted for exactly that
|
|
74
|
+
reason.
|
|
75
|
+
[`docs/architecture.md`](docs/architecture.md) §15 has the ledger.
|
|
77
76
|
|
|
78
77
|
**Rules are authored in TypeScript whatever language they check** — that is the form to start
|
|
79
78
|
from, and it is the one most teams already have someone who writes. A rule may also be a
|
|
80
|
-
WebAssembly component, which is how
|
|
81
|
-
written in Go
|
|
82
|
-
|
|
83
|
-
names a rule rather than its implementation. **Configuration is neither** — `lanekeep.json` is
|
|
79
|
+
WebAssembly component, which is how four of the sixteen built-ins ship — two written in Rust
|
|
80
|
+
and two written in Go; the other twelve run as QuickJS modules, three of them checking five of
|
|
81
|
+
the six supported languages from a single source (every one but JavaScript). Every form reaches the same host API and is held to
|
|
82
|
+
the same limits, and a config names a rule rather than its implementation. **Configuration is neither** — `lanekeep.json` is
|
|
84
83
|
plain data, so a Go, Python or Rust team never writes a `.ts` file except when authoring an
|
|
85
84
|
actual rule.
|
|
86
85
|
|
|
@@ -92,7 +91,7 @@ lanekeep check --staged # only what is about to be committed
|
|
|
92
91
|
lanekeep check --since main # only what changed against a ref
|
|
93
92
|
lanekeep check --watch # re-check on every change, until Ctrl-C
|
|
94
93
|
lanekeep check --fix # apply the safe fixes, report what is left
|
|
95
|
-
lanekeep check --profile # where the
|
|
94
|
+
lanekeep check --profile # per rule: where the time went, and what it looked at
|
|
96
95
|
lanekeep rules # what this project has configured
|
|
97
96
|
lanekeep explain <rule-id> # one rule's card, without opening its source
|
|
98
97
|
lanekeep server # LSP for an editor, or --protocol mcp for an agent host
|
|
@@ -183,7 +182,9 @@ Known gaps, stated rather than implied:
|
|
|
183
182
|
- **Two of the three performance budgets in [`docs/architecture.md`](docs/architecture.md)
|
|
184
183
|
§15 are not met.** The cold budget is; they are targets, and that section says by how much
|
|
185
184
|
and where the remaining time goes.
|
|
186
|
-
- **No type
|
|
185
|
+
- **No general type inference**, by design. Name resolution is syntactic; a rule that opts in
|
|
186
|
+
(`requires: ['types']`) gets a bounded within-file oracle that answers `undefined` rather than
|
|
187
|
+
guess — see §1 non-goals and §6.10.
|
|
187
188
|
|
|
188
189
|
## Documentation
|
|
189
190
|
|
|
@@ -204,7 +205,11 @@ In-repo, versioned with the code:
|
|
|
204
205
|
| [`docs/architecture.md`](docs/architecture.md) | The full design: execution model, host API, cache, milestones |
|
|
205
206
|
| [`docs/built-in-rules.md`](docs/built-in-rules.md) | The rules lanekeep ships with, and their options |
|
|
206
207
|
| [`docs/cross-file-rules.md`](docs/cross-file-rules.md) | Writing a rule that needs a whole-corpus view |
|
|
207
|
-
| [`docs/
|
|
208
|
+
| [`docs/obligation-rules.md`](docs/obligation-rules.md) | Writing a rule that needs a resource released on every path |
|
|
209
|
+
| [`docs/type-aware-rules.md`](docs/type-aware-rules.md) | Writing a rule that needs to know what a value's type is |
|
|
210
|
+
| [`docs/authoring-rust-rules.md`](docs/authoring-rust-rules.md) | Writing a rule in Rust, shipped as a WebAssembly component |
|
|
211
|
+
| [`docs/authoring-go-rules.md`](docs/authoring-go-rules.md) | Writing a rule in Go |
|
|
212
|
+
| [`docs/authoring-python-rules.md`](docs/authoring-python-rules.md) | Writing a rule in Python |
|
|
208
213
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Setup, commands, and the pull request process |
|
|
209
214
|
| [`AGENTS.md`](AGENTS.md) | How to work in this repository — for coding agents and humans alike |
|
|
210
215
|
| [`SECURITY.md`](SECURITY.md) | Threat model and how to report a vulnerability |
|
package/index.d.ts
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
* Node: `defineRule` and `defineConfig` are identity functions whose only job is to give the
|
|
9
9
|
* compiler something to check against, and `RuleContext` is provided by lanekeep at run time.
|
|
10
10
|
* The world is the single source of truth for every member the renderer emits straight from it.
|
|
11
|
-
*
|
|
12
|
-
* from `RuleContext` because QuickJS exposes it as a conditional property rather than a
|
|
13
|
-
* a shape this renderer cannot state honestly from the world;
|
|
11
|
+
* Three members deviate from the world on purpose, and all three are QuickJS-shaped: `today` is
|
|
12
|
+
* omitted from `RuleContext` because QuickJS exposes it as a conditional property rather than a
|
|
13
|
+
* callable, a shape this renderer cannot state honestly from the world; `facts` is added to
|
|
14
14
|
* `RuleContext` because QuickJS hands a per-file rule `facts` that the world declares only on
|
|
15
|
-
* `reduce-context
|
|
15
|
+
* `reduce-context`; and `types` is added to `RuleContext` because `ctx.types` — the bounded
|
|
16
|
+
* type oracle — is QuickJS-only and has no presence in `world.wit` at all: a component rule
|
|
17
|
+
* cannot declare `requires`, so there is nothing for the world to say about it. Nothing else is
|
|
18
|
+
* added or omitted by hand.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -78,7 +81,23 @@ export interface RuleCard {
|
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Cheap rejections applied before a file is read or parsed.
|
|
86
|
+
*
|
|
87
|
+
* A gate is declared, not derived: nothing here is computed from the rule's `query`, and
|
|
88
|
+
* nothing checks the two against each other. So a gate can change what the rule reports — a
|
|
89
|
+
* file it rejects is a file the rule never runs on, and a violation there is never found.
|
|
90
|
+
*
|
|
91
|
+
* A gate is neutral when it admits every file the rule would have reported on — yours to
|
|
92
|
+
* keep, and not something the engine can check. The safe way to keep it is to gate wider
|
|
93
|
+
* than the query, which is sufficient rather than necessary: a rule whose handler filters
|
|
94
|
+
* may gate far narrower and still be neutral. `--profile` prints, per rule, how many files
|
|
95
|
+
* each gate rejected and how many the rule actually parsed, which is where a suspected gate
|
|
96
|
+
* is settled. A nonzero `cached` means the columns to its right are
|
|
97
|
+
* incomplete for that run, since a cache hit returns before the content gates are consulted;
|
|
98
|
+
* pair `--profile` with `--no-cache` to read them. `path-gated` is unaffected, because a path
|
|
99
|
+
* gate runs before the cache is consulted at all.
|
|
100
|
+
*/
|
|
82
101
|
export interface Gates {
|
|
83
102
|
/**
|
|
84
103
|
* Glob patterns a file's path must match for the rule to consider it.
|
|
@@ -191,6 +210,151 @@ export interface StructureFingerprint {
|
|
|
191
210
|
nodes: number
|
|
192
211
|
}
|
|
193
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Where a name came from. Returned by {@link TypeApi.symbolOf} directly, and nested under a
|
|
215
|
+
* {@link TypeInfo} whose `symbol` field is set.
|
|
216
|
+
*
|
|
217
|
+
* There is deliberately no `file` naming the declaring path, even now that declarations are
|
|
218
|
+
* read across files. It would be where a package happens to be installed —
|
|
219
|
+
* `node_modules/@types/money/index.d.ts` on one machine and a workspace path on another —
|
|
220
|
+
* so a rule branching on it would give different answers for one program. `module` is the
|
|
221
|
+
* stable identity, and `exported` is the name that module declares it under.
|
|
222
|
+
*/
|
|
223
|
+
export interface SymbolInfo {
|
|
224
|
+
/**
|
|
225
|
+
* The name as it appears at the use site, not at the declaration. For a renamed import —
|
|
226
|
+
* `import { Decimal as Money }` — this is the local alias `Money`. It is the spelling to
|
|
227
|
+
* quote in a message, because it is the spelling the reader has in front of them; compare
|
|
228
|
+
* `exported` when the question is which export of a module this is.
|
|
229
|
+
*/
|
|
230
|
+
name: string
|
|
231
|
+
/**
|
|
232
|
+
* The name the module exports this under. For a named import — `import { Decimal }` and
|
|
233
|
+
* `import { Decimal as Money }` alike — this is `Decimal` when `m`'s declaration file is
|
|
234
|
+
* unreadable, and the name that file actually declares (following any re-export chain)
|
|
235
|
+
* when it is readable, which need not be `Decimal` at all. Copied even when nothing was
|
|
236
|
+
* renamed, so a comparison never needs a fallback to `name` that would silently accept
|
|
237
|
+
* every plain import if it were forgotten. For a default import this is the name the
|
|
238
|
+
* declaration file declares the default export under when that file is readable — `Big`
|
|
239
|
+
* for a package whose default export is `Big`, whatever the local binding is called — and
|
|
240
|
+
* falls back to the placeholder `default` only when the declaration is not readable.
|
|
241
|
+
* Absent for a namespace import, which binds the module object and has no single exported
|
|
242
|
+
* name, and absent for a local declaration, which was not imported at all.
|
|
243
|
+
*/
|
|
244
|
+
exported?: string
|
|
245
|
+
/**
|
|
246
|
+
* The module it was imported from. Absent for a local declaration — that absence is what
|
|
247
|
+
* distinguishes an imported `Decimal` from a local class that happens to share the name.
|
|
248
|
+
*/
|
|
249
|
+
module?: string
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* What the oracle established about an expression, from {@link TypeApi.typeOf}.
|
|
254
|
+
*
|
|
255
|
+
* At most one of `primitive`, `symbol` or `union` is set, matching which kind of type this
|
|
256
|
+
* is — a `union`'s members are already flattened one level and in canonical order. `text` is
|
|
257
|
+
* set alongside whichever it is, but it is **display-only**: what TypeScript itself would
|
|
258
|
+
* call the type, for a message a rule builds. Branch on `primitive` and `symbol`, never on
|
|
259
|
+
* `text`'s wording.
|
|
260
|
+
*
|
|
261
|
+
* All three can be unset at once: a *nominal* type whose name the resolver could not
|
|
262
|
+
* attribute — an unresolvable, global or ambient type such as `Date` used with no local
|
|
263
|
+
* declaration or import — carries only `text`. That is not a gap to code around; it is
|
|
264
|
+
* another shape of the same "I could not be sure" answer this whole surface is built on,
|
|
265
|
+
* the same posture `typeOf` itself takes by returning `undefined` rather than guessing. Do
|
|
266
|
+
* not assume the final branch of `if (primitive) … else if (symbol) … else` is unreachable
|
|
267
|
+
* — for this shape, it is not.
|
|
268
|
+
*
|
|
269
|
+
* There is deliberately no `complete` field on a *type*. Whether the oracle had a full view
|
|
270
|
+
* is a property of the file rather than of any one answer — `TypeApi.complete()` is where it
|
|
271
|
+
* is asked — and putting it here would invite a rule to read it per type and conclude
|
|
272
|
+
* something different on each.
|
|
273
|
+
*/
|
|
274
|
+
export interface TypeInfo {
|
|
275
|
+
/** What TypeScript would call this type. Display-only — branch on the fields below instead. */
|
|
276
|
+
text: string
|
|
277
|
+
/** Set when this is a primitive — exactly the set TypeScript itself recognizes as one. */
|
|
278
|
+
primitive?: 'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'null' | 'undefined'
|
|
279
|
+
/**
|
|
280
|
+
* Set when this is a named type and the oracle could resolve where the name came from.
|
|
281
|
+
* Absent on an unresolvable, global or ambient nominal type — see the interface doc above.
|
|
282
|
+
*/
|
|
283
|
+
symbol?: SymbolInfo
|
|
284
|
+
/** Set when this is a union. */
|
|
285
|
+
union?: TypeInfo[]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The type oracle, reached through `ctx.types`.
|
|
290
|
+
*
|
|
291
|
+
* Every question can come back with no answer, and no answer is a first-class result rather
|
|
292
|
+
* than a failure to work around: the oracle is conservative on purpose, and it would rather
|
|
293
|
+
* say nothing than say something wrong, because a rule reporting on a wrong type accuses
|
|
294
|
+
* correct code. A rule is expected to check for `undefined` and quietly stay silent, the same
|
|
295
|
+
* posture the rest of the navigation surface already takes on a dead handle.
|
|
296
|
+
*
|
|
297
|
+
* It reads the file in front of it **and the declaration files that file imports**, through
|
|
298
|
+
* the same tracked, confined reads `ctx.readFile` uses. Nothing above the project root is read,
|
|
299
|
+
* ever — so point lanekeep at the workspace root, the directory `node_modules` lives in,
|
|
300
|
+
* rather than at a package inside it. `--config` does not move the root.
|
|
301
|
+
*/
|
|
302
|
+
export interface TypeApi {
|
|
303
|
+
/**
|
|
304
|
+
* The type of the expression at `n`. `undefined` is that first-class no-answer, not a
|
|
305
|
+
* failure.
|
|
306
|
+
*/
|
|
307
|
+
typeOf(n: Node): TypeInfo | undefined
|
|
308
|
+
/** Where the identifier at `n` was declared. `undefined` on the same terms as `typeOf`. */
|
|
309
|
+
symbolOf(n: Node): SymbolInfo | undefined
|
|
310
|
+
/**
|
|
311
|
+
* What calling the function at `n` yields.
|
|
312
|
+
*
|
|
313
|
+
* Separate from `typeOf` because a function declaration is not an expression. Accepts a
|
|
314
|
+
* call expression, a function-like declaration, or an identifier bound to one. An
|
|
315
|
+
* annotated signature answers its annotation; an unannotated one with a single `return`
|
|
316
|
+
* answers that expression's type; several returns answer their union when every member is
|
|
317
|
+
* known and `undefined` otherwise; a function with no `return` answers `undefined` rather
|
|
318
|
+
* than guessing `void`. Type arguments are dropped throughout — `useQuery<Balance[]>`
|
|
319
|
+
* answers by the result type's name, never by the argument.
|
|
320
|
+
*/
|
|
321
|
+
returnTypeOf(n: Node): TypeInfo | undefined
|
|
322
|
+
/**
|
|
323
|
+
* Whether the type at `n` is the type `module` exports as `name`, or inherits from it.
|
|
324
|
+
*
|
|
325
|
+
* Nominal, never structural: a class reaches it through `extends` or `implements`, an
|
|
326
|
+
* interface through `extends`, an alias by being transparent, and a union only when *every*
|
|
327
|
+
* member does. A primitive answers `false` — but only when `module` and `name` resolve to a
|
|
328
|
+
* declaration; a target that does not resolve answers `undefined` whatever `n` is.
|
|
329
|
+
*
|
|
330
|
+
* **`undefined` is not `false`.** `false` means the walk completed and reached nothing;
|
|
331
|
+
* `undefined` means a link could not be read — an unresolvable import, a package that is
|
|
332
|
+
* not installed, a type with no symbol at all. A rule treating the two alike reports on
|
|
333
|
+
* code the oracle never saw.
|
|
334
|
+
*/
|
|
335
|
+
isAssignableTo(n: Node, module: string, name: string): boolean | undefined
|
|
336
|
+
/**
|
|
337
|
+
* Whether every import in the file being checked resolved to something readable.
|
|
338
|
+
*
|
|
339
|
+
* `false` is the honest label on a partial view: some name in this file came from a module
|
|
340
|
+
* the oracle could not open, so an `undefined` anywhere in it may be ignorance rather than
|
|
341
|
+
* a considered answer. A rule that reports only on what it established does not need to
|
|
342
|
+
* ask; a rule that wants to say "I could not check this file" does.
|
|
343
|
+
*
|
|
344
|
+
* An import that resolves to a file which does not *parse* counts as unreadable too: the
|
|
345
|
+
* names outside the broken span still answer, the ones inside it come back `undefined`, and
|
|
346
|
+
* nothing on either answer says which. An import of something that is not code — a
|
|
347
|
+
* stylesheet, a JSON asset, an image — is not counted at all, since it is not a module the
|
|
348
|
+
* oracle reads.
|
|
349
|
+
*
|
|
350
|
+
* **The verdict is the whole file's, and a parse fault is the whole declaration file's.**
|
|
351
|
+
* One `ERROR` node anywhere in a fifty-thousand-line `@types` bundle makes every file that
|
|
352
|
+
* imports it `false`, however far that span is from the names the rule asked about. Silence
|
|
353
|
+
* is the safe direction; a narrower verdict is a refinement rather than a promise.
|
|
354
|
+
*/
|
|
355
|
+
complete(): boolean
|
|
356
|
+
}
|
|
357
|
+
|
|
194
358
|
/** A rule's RuleContext surface. */
|
|
195
359
|
export interface RuleContext {
|
|
196
360
|
readonly filePath: string
|
|
@@ -219,6 +383,19 @@ export interface RuleContext {
|
|
|
219
383
|
report(at: Node, message?: string | ReportOptions): void
|
|
220
384
|
/** Facts emitted so far, optionally filtered by `kind`. */
|
|
221
385
|
facts(kind?: string): EmittedFact[]
|
|
386
|
+
/**
|
|
387
|
+
* The type oracle, present only for a rule that declared `requires: ['types']`.
|
|
388
|
+
* Bounded by a fixed depth rather than by file: it follows an import into the
|
|
389
|
+
* declaration file that answers it, and the file after that.
|
|
390
|
+
*
|
|
391
|
+
* Typed as always present because there is no way to spell "present when
|
|
392
|
+
* this rule's own `requires` says so" as a type — so a rule that forgets the
|
|
393
|
+
* declaration still compiles. It finds out at the first call instead:
|
|
394
|
+
* `ctx.types` is `undefined` at run time, and `ctx.types.typeOf(...)` throws a
|
|
395
|
+
* `TypeError` rather than returning a quietly wrong answer. That loudness is
|
|
396
|
+
* deliberate.
|
|
397
|
+
*/
|
|
398
|
+
types: TypeApi
|
|
222
399
|
}
|
|
223
400
|
|
|
224
401
|
/** A violation the reduce phase reports, which has no node to point at. */
|
|
@@ -238,6 +415,49 @@ export interface ReduceContext {
|
|
|
238
415
|
report(at: ReduceLocation, message?: string | ReportOptions): void
|
|
239
416
|
}
|
|
240
417
|
|
|
418
|
+
/** Queries whose named captures drive the taint analysis. */
|
|
419
|
+
export type FlowSpec = {
|
|
420
|
+
/** Queries whose `@source` capture marks a tainted origin. */
|
|
421
|
+
sources: string[]
|
|
422
|
+
/** Queries whose `@sink` capture marks a forbidden destination. */
|
|
423
|
+
sinks: string[]
|
|
424
|
+
/** Queries whose `@sanitizer` capture clears taint from the value it produces. */
|
|
425
|
+
sanitizers?: string[]
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** One tainted flow: a source whose value reaches a sink with no intervening sanitizer. */
|
|
429
|
+
export type FlowPath = {
|
|
430
|
+
readonly source: Node
|
|
431
|
+
readonly sink: Node
|
|
432
|
+
/** The assignments and calls between source and sink, in flow order. One canonical path. */
|
|
433
|
+
readonly steps: readonly Node[]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* A typestate obligation: an acquired resource must reach a release on every
|
|
438
|
+
* path out of `scope`. Requires `requires: ['dataflow']`.
|
|
439
|
+
*/
|
|
440
|
+
export type ObligationSpec = {
|
|
441
|
+
/** Queries whose `@acquire` capture starts an obligation on the captured value. */
|
|
442
|
+
acquire: string[]
|
|
443
|
+
/** Queries whose `@release` capture discharges it. */
|
|
444
|
+
release: string[]
|
|
445
|
+
/**
|
|
446
|
+
* `'function'` — every path out of the enclosing function, `return`/`throw` included.
|
|
447
|
+
* `'block'` — every path out of the block the acquire is in.
|
|
448
|
+
*/
|
|
449
|
+
scope: 'function' | 'block'
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** An acquire some path leaves undischarged, passed to `checkObligation`. */
|
|
453
|
+
export type UnmetObligation = {
|
|
454
|
+
readonly acquire: Node
|
|
455
|
+
/** The exit the value escapes through — a `return`, a `throw`, or the implicit end. */
|
|
456
|
+
readonly exit: Node
|
|
457
|
+
/** Whether any path *did* discharge it: partial coverage reads differently to none. */
|
|
458
|
+
readonly partial: boolean
|
|
459
|
+
}
|
|
460
|
+
|
|
241
461
|
/** A rule, as `defineRule` takes it. */
|
|
242
462
|
export interface Rule {
|
|
243
463
|
/**
|
|
@@ -256,6 +476,15 @@ export interface Rule {
|
|
|
256
476
|
* or Rust rule means it silently never fires.
|
|
257
477
|
*/
|
|
258
478
|
language?: LanguageId | LanguageId[]
|
|
479
|
+
/**
|
|
480
|
+
* Host analyses this rule needs before it can run.
|
|
481
|
+
*
|
|
482
|
+
* Absent means none, which is every rule today. A rule declaring one the engine cannot
|
|
483
|
+
* provide is refused at load rather than run without it: an analysis that silently goes
|
|
484
|
+
* missing makes the rule report nothing, and a rule reporting nothing is indistinguishable
|
|
485
|
+
* from a codebase with nothing to report.
|
|
486
|
+
*/
|
|
487
|
+
requires?: Array<'types' | 'dataflow'>
|
|
259
488
|
/** How serious a violation is, before any config override. */
|
|
260
489
|
severity: Severity
|
|
261
490
|
/** What the rule tells whoever has to act on it. */
|
|
@@ -269,6 +498,11 @@ export interface Rule {
|
|
|
269
498
|
* what keeps a JavaScript rule affordable. Write the narrowest query that captures what
|
|
270
499
|
* you need; `check` then only refines.
|
|
271
500
|
*
|
|
501
|
+
* Optional — required only when `check` is present. A flow-only rule (`checkFlow` with no
|
|
502
|
+
* `check`) declares no top-level query; its file gate comes from the union of its `flow`
|
|
503
|
+
* queries instead (see {@link FlowSpec}). The type cannot express that conditional, so a
|
|
504
|
+
* `check` rule missing `query` type-checks here and is refused by the config loader instead.
|
|
505
|
+
*
|
|
272
506
|
* A single string applies to every declared language. An object maps each declared
|
|
273
507
|
* language to its own query — required when the grammars do not share node vocabulary
|
|
274
508
|
* (Python spells a call `call`, the other supported grammars say `call_expression`).
|
|
@@ -282,13 +516,29 @@ export interface Rule {
|
|
|
282
516
|
* no backreferences or lookaround. `#is?`, `#is-not?`, `#set!`, or an operator the
|
|
283
517
|
* binding does not know is refused at compile time.
|
|
284
518
|
*/
|
|
285
|
-
query
|
|
519
|
+
query?: string | Partial<Record<LanguageId, string>>
|
|
286
520
|
/** A per-invocation budget overriding the default, in milliseconds. */
|
|
287
521
|
timeout?: number
|
|
288
522
|
/** Called once per query match. */
|
|
289
523
|
check?(ctx: RuleContext, match: Match): void
|
|
290
524
|
/** Called once per run, after every file, with facts only. */
|
|
291
525
|
reduce?(ctx: ReduceContext): void
|
|
526
|
+
/**
|
|
527
|
+
* A taint-flow specification: queries whose captures mark tainted origins, forbidden
|
|
528
|
+
* destinations, and the calls that neutralize a value. Requires `requires: ['dataflow']`
|
|
529
|
+
* and a `checkFlow` handler; a rule declaring one without the other is refused at load.
|
|
530
|
+
*/
|
|
531
|
+
flow?: FlowSpec
|
|
532
|
+
/** Called once per canonical tainted flow from a source to a sink. */
|
|
533
|
+
checkFlow?(ctx: RuleContext, path: FlowPath): void
|
|
534
|
+
/**
|
|
535
|
+
* A typestate obligation: queries whose captures acquire an obligation on a value and
|
|
536
|
+
* queries that discharge it. Requires `requires: ['dataflow']` and a `checkObligation`
|
|
537
|
+
* handler; a rule declaring one without the other is refused at load.
|
|
538
|
+
*/
|
|
539
|
+
obligation?: ObligationSpec
|
|
540
|
+
/** Called once per value left with an unmet obligation at the end of its scope. */
|
|
541
|
+
checkObligation?(ctx: RuleContext, unmet: UnmetObligation): void
|
|
292
542
|
}
|
|
293
543
|
|
|
294
544
|
/** A lanekeep configuration, as `defineConfig` takes it. */
|
|
@@ -307,6 +557,13 @@ export interface Config {
|
|
|
307
557
|
rule?: number
|
|
308
558
|
/** Wall-clock, for the whole run. */
|
|
309
559
|
global?: number
|
|
560
|
+
/**
|
|
561
|
+
* Host-side type-provider work, across the whole run.
|
|
562
|
+
*
|
|
563
|
+
* Analysis time rather than elapsed time: only what the provider spends building
|
|
564
|
+
* programs and answering requests is charged against it.
|
|
565
|
+
*/
|
|
566
|
+
analysis?: number
|
|
310
567
|
}
|
|
311
568
|
/** Policy for suppression directives. All off by default. */
|
|
312
569
|
suppressions?: {
|
|
@@ -317,6 +574,19 @@ export interface Config {
|
|
|
317
574
|
/** Any whole-file directive is reported. */
|
|
318
575
|
forbidFileScope?: boolean
|
|
319
576
|
}
|
|
577
|
+
/** Which type oracle answers `ctx.types`. */
|
|
578
|
+
types?: {
|
|
579
|
+
/**
|
|
580
|
+
* `builtin` is lanekeep's own bounded oracle and needs no toolchain. `tsc` drives the
|
|
581
|
+
* project's own `typescript` package through a sidecar process: wider answers, a slower
|
|
582
|
+
* run, and a pre-commit hook that builds the project's program before it checks anything.
|
|
583
|
+
*/
|
|
584
|
+
provider?: 'builtin' | 'tsc'
|
|
585
|
+
/** How to launch the sidecar. `tsc` only. */
|
|
586
|
+
command?: string[]
|
|
587
|
+
/** The `typescript` package the sidecar loads, resolved from the project root. `tsc` only. */
|
|
588
|
+
typescript?: string
|
|
589
|
+
}
|
|
320
590
|
/** The rules to run, in order. */
|
|
321
591
|
rules: Rule[]
|
|
322
592
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanekeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Deterministic, AST-based architectural conformance checking",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"node": ">=18"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@lanekeep/darwin-arm64": "0.
|
|
27
|
-
"@lanekeep/linux-arm64": "0.
|
|
28
|
-
"@lanekeep/linux-x64": "0.
|
|
29
|
-
"@lanekeep/win32-x64": "0.
|
|
26
|
+
"@lanekeep/darwin-arm64": "0.9.0",
|
|
27
|
+
"@lanekeep/linux-arm64": "0.9.0",
|
|
28
|
+
"@lanekeep/linux-x64": "0.9.0",
|
|
29
|
+
"@lanekeep/win32-x64": "0.9.0"
|
|
30
30
|
},
|
|
31
31
|
"main": "index.js",
|
|
32
32
|
"types": "index.d.ts",
|