lanekeep 0.8.1 → 0.10.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 +20 -14
- package/index.d.ts +299 -11
- 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
|
|
|
@@ -90,9 +89,10 @@ actual rule.
|
|
|
90
89
|
lanekeep check # the whole project
|
|
91
90
|
lanekeep check --staged # only what is about to be committed
|
|
92
91
|
lanekeep check --since main # only what changed against a ref
|
|
92
|
+
lanekeep check --file src/a.ts # exactly these files, repeatable, no git involved
|
|
93
93
|
lanekeep check --watch # re-check on every change, until Ctrl-C
|
|
94
94
|
lanekeep check --fix # apply the safe fixes, report what is left
|
|
95
|
-
lanekeep check --profile # where the
|
|
95
|
+
lanekeep check --profile # per rule: where the time went, and what it looked at
|
|
96
96
|
lanekeep rules # what this project has configured
|
|
97
97
|
lanekeep explain <rule-id> # one rule's card, without opening its source
|
|
98
98
|
lanekeep server # LSP for an editor, or --protocol mcp for an agent host
|
|
@@ -183,7 +183,9 @@ Known gaps, stated rather than implied:
|
|
|
183
183
|
- **Two of the three performance budgets in [`docs/architecture.md`](docs/architecture.md)
|
|
184
184
|
§15 are not met.** The cold budget is; they are targets, and that section says by how much
|
|
185
185
|
and where the remaining time goes.
|
|
186
|
-
- **No type
|
|
186
|
+
- **No general type inference**, by design. Name resolution is syntactic; a rule that opts in
|
|
187
|
+
(`requires: ['types']`) gets a bounded within-file oracle that answers `undefined` rather than
|
|
188
|
+
guess — see §1 non-goals and §6.10.
|
|
187
189
|
|
|
188
190
|
## Documentation
|
|
189
191
|
|
|
@@ -204,7 +206,11 @@ In-repo, versioned with the code:
|
|
|
204
206
|
| [`docs/architecture.md`](docs/architecture.md) | The full design: execution model, host API, cache, milestones |
|
|
205
207
|
| [`docs/built-in-rules.md`](docs/built-in-rules.md) | The rules lanekeep ships with, and their options |
|
|
206
208
|
| [`docs/cross-file-rules.md`](docs/cross-file-rules.md) | Writing a rule that needs a whole-corpus view |
|
|
207
|
-
| [`docs/
|
|
209
|
+
| [`docs/obligation-rules.md`](docs/obligation-rules.md) | Writing a rule that needs a resource released on every path |
|
|
210
|
+
| [`docs/type-aware-rules.md`](docs/type-aware-rules.md) | Writing a rule that needs to know what a value's type is |
|
|
211
|
+
| [`docs/authoring-rust-rules.md`](docs/authoring-rust-rules.md) | Writing a rule in Rust, shipped as a WebAssembly component |
|
|
212
|
+
| [`docs/authoring-go-rules.md`](docs/authoring-go-rules.md) | Writing a rule in Go |
|
|
213
|
+
| [`docs/authoring-python-rules.md`](docs/authoring-python-rules.md) | Writing a rule in Python |
|
|
208
214
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Setup, commands, and the pull request process |
|
|
209
215
|
| [`AGENTS.md`](AGENTS.md) | How to work in this repository — for coding agents and humans alike |
|
|
210
216
|
| [`SECURITY.md`](SECURITY.md) | Threat model and how to report a vulnerability |
|
package/index.d.ts
CHANGED
|
@@ -8,11 +8,13 @@
|
|
|
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
|
-
* Two members deviate from the world on purpose, and both are QuickJS-shaped: `today` is
|
|
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; and `
|
|
14
|
-
* `RuleContext` because
|
|
15
|
-
*
|
|
11
|
+
* Two members deviate from the world on purpose, and both 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; and `types` is added
|
|
14
|
+
* to `RuleContext` because `ctx.types` — the bounded
|
|
15
|
+
* type oracle — is QuickJS-only and has no presence in `world.wit` at all: a component rule
|
|
16
|
+
* cannot declare `requires`, so there is nothing for the world to say about it. Nothing else is
|
|
17
|
+
* added or omitted by hand.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -78,7 +80,23 @@ export interface RuleCard {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Cheap rejections applied before a file is read or parsed.
|
|
85
|
+
*
|
|
86
|
+
* A gate is declared, not derived: nothing here is computed from the rule's `query`, and
|
|
87
|
+
* nothing checks the two against each other. So a gate can change what the rule reports — a
|
|
88
|
+
* file it rejects is a file the rule never runs on, and a violation there is never found.
|
|
89
|
+
*
|
|
90
|
+
* A gate is neutral when it admits every file the rule would have reported on — yours to
|
|
91
|
+
* keep, and not something the engine can check. The safe way to keep it is to gate wider
|
|
92
|
+
* than the query, which is sufficient rather than necessary: a rule whose handler filters
|
|
93
|
+
* may gate far narrower and still be neutral. `--profile` prints, per rule, how many files
|
|
94
|
+
* each gate rejected and how many the rule actually parsed, which is where a suspected gate
|
|
95
|
+
* is settled. A nonzero `cached` means the columns to its right are
|
|
96
|
+
* incomplete for that run, since a cache hit returns before the content gates are consulted;
|
|
97
|
+
* pair `--profile` with `--no-cache` to read them. `path-gated` is unaffected, because a path
|
|
98
|
+
* gate runs before the cache is consulted at all.
|
|
99
|
+
*/
|
|
82
100
|
export interface Gates {
|
|
83
101
|
/**
|
|
84
102
|
* Glob patterns a file's path must match for the rule to consider it.
|
|
@@ -117,7 +135,13 @@ export interface Gates {
|
|
|
117
135
|
fileNotContains?: string[]
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* A replacement a rule offers for a violation.
|
|
140
|
+
*
|
|
141
|
+
* A per-file offer alone: a fix names a node, and the reduce phase consumes facts and the
|
|
142
|
+
* file list and nothing else — there is no parse tree there for `node` to name, which is why
|
|
143
|
+
* a reduce report's options carry a message and never a fix.
|
|
144
|
+
*/
|
|
121
145
|
export interface Fix {
|
|
122
146
|
/** The node whose text is replaced. */
|
|
123
147
|
node: Node
|
|
@@ -141,6 +165,20 @@ export interface ReportOptions {
|
|
|
141
165
|
fix?: Fix
|
|
142
166
|
}
|
|
143
167
|
|
|
168
|
+
/** Options for a single reduce report. */
|
|
169
|
+
export interface ReduceReportOptions {
|
|
170
|
+
/** Overrides the card's `message` for this one violation. */
|
|
171
|
+
message?: string
|
|
172
|
+
/**
|
|
173
|
+
* Never present. A fix replaces a node's text, and the reduce phase has no parse tree —
|
|
174
|
+
* `node` has nothing to name, and both hosts throw on a supplied fix rather than drop it.
|
|
175
|
+
* Typed `never` rather than left absent so offering one fails to compile even for an
|
|
176
|
+
* object built before the call, which a fresh literal's excess-property check alone would
|
|
177
|
+
* admit.
|
|
178
|
+
*/
|
|
179
|
+
fix?: never
|
|
180
|
+
}
|
|
181
|
+
|
|
144
182
|
/**
|
|
145
183
|
* A fact a rule emits for the reduce phase.
|
|
146
184
|
*
|
|
@@ -191,6 +229,151 @@ export interface StructureFingerprint {
|
|
|
191
229
|
nodes: number
|
|
192
230
|
}
|
|
193
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Where a name came from. Returned by {@link TypeApi.symbolOf} directly, and nested under a
|
|
234
|
+
* {@link TypeInfo} whose `symbol` field is set.
|
|
235
|
+
*
|
|
236
|
+
* There is deliberately no `file` naming the declaring path, even now that declarations are
|
|
237
|
+
* read across files. It would be where a package happens to be installed —
|
|
238
|
+
* `node_modules/@types/money/index.d.ts` on one machine and a workspace path on another —
|
|
239
|
+
* so a rule branching on it would give different answers for one program. `module` is the
|
|
240
|
+
* stable identity, and `exported` is the name that module declares it under.
|
|
241
|
+
*/
|
|
242
|
+
export interface SymbolInfo {
|
|
243
|
+
/**
|
|
244
|
+
* The name as it appears at the use site, not at the declaration. For a renamed import —
|
|
245
|
+
* `import { Decimal as Money }` — this is the local alias `Money`. It is the spelling to
|
|
246
|
+
* quote in a message, because it is the spelling the reader has in front of them; compare
|
|
247
|
+
* `exported` when the question is which export of a module this is.
|
|
248
|
+
*/
|
|
249
|
+
name: string
|
|
250
|
+
/**
|
|
251
|
+
* The name the module exports this under. For a named import — `import { Decimal }` and
|
|
252
|
+
* `import { Decimal as Money }` alike — this is `Decimal` when `m`'s declaration file is
|
|
253
|
+
* unreadable, and the name that file actually declares (following any re-export chain)
|
|
254
|
+
* when it is readable, which need not be `Decimal` at all. Copied even when nothing was
|
|
255
|
+
* renamed, so a comparison never needs a fallback to `name` that would silently accept
|
|
256
|
+
* every plain import if it were forgotten. For a default import this is the name the
|
|
257
|
+
* declaration file declares the default export under when that file is readable — `Big`
|
|
258
|
+
* for a package whose default export is `Big`, whatever the local binding is called — and
|
|
259
|
+
* falls back to the placeholder `default` only when the declaration is not readable.
|
|
260
|
+
* Absent for a namespace import, which binds the module object and has no single exported
|
|
261
|
+
* name, and absent for a local declaration, which was not imported at all.
|
|
262
|
+
*/
|
|
263
|
+
exported?: string
|
|
264
|
+
/**
|
|
265
|
+
* The module it was imported from. Absent for a local declaration — that absence is what
|
|
266
|
+
* distinguishes an imported `Decimal` from a local class that happens to share the name.
|
|
267
|
+
*/
|
|
268
|
+
module?: string
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* What the oracle established about an expression, from {@link TypeApi.typeOf}.
|
|
273
|
+
*
|
|
274
|
+
* At most one of `primitive`, `symbol` or `union` is set, matching which kind of type this
|
|
275
|
+
* is — a `union`'s members are already flattened one level and in canonical order. `text` is
|
|
276
|
+
* set alongside whichever it is, but it is **display-only**: what TypeScript itself would
|
|
277
|
+
* call the type, for a message a rule builds. Branch on `primitive` and `symbol`, never on
|
|
278
|
+
* `text`'s wording.
|
|
279
|
+
*
|
|
280
|
+
* All three can be unset at once: a *nominal* type whose name the resolver could not
|
|
281
|
+
* attribute — an unresolvable, global or ambient type such as `Date` used with no local
|
|
282
|
+
* declaration or import — carries only `text`. That is not a gap to code around; it is
|
|
283
|
+
* another shape of the same "I could not be sure" answer this whole surface is built on,
|
|
284
|
+
* the same posture `typeOf` itself takes by returning `undefined` rather than guessing. Do
|
|
285
|
+
* not assume the final branch of `if (primitive) … else if (symbol) … else` is unreachable
|
|
286
|
+
* — for this shape, it is not.
|
|
287
|
+
*
|
|
288
|
+
* There is deliberately no `complete` field on a *type*. Whether the oracle had a full view
|
|
289
|
+
* is a property of the file rather than of any one answer — `TypeApi.complete()` is where it
|
|
290
|
+
* is asked — and putting it here would invite a rule to read it per type and conclude
|
|
291
|
+
* something different on each.
|
|
292
|
+
*/
|
|
293
|
+
export interface TypeInfo {
|
|
294
|
+
/** What TypeScript would call this type. Display-only — branch on the fields below instead. */
|
|
295
|
+
text: string
|
|
296
|
+
/** Set when this is a primitive — exactly the set TypeScript itself recognizes as one. */
|
|
297
|
+
primitive?: 'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'null' | 'undefined'
|
|
298
|
+
/**
|
|
299
|
+
* Set when this is a named type and the oracle could resolve where the name came from.
|
|
300
|
+
* Absent on an unresolvable, global or ambient nominal type — see the interface doc above.
|
|
301
|
+
*/
|
|
302
|
+
symbol?: SymbolInfo
|
|
303
|
+
/** Set when this is a union. */
|
|
304
|
+
union?: TypeInfo[]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The type oracle, reached through `ctx.types`.
|
|
309
|
+
*
|
|
310
|
+
* Every question can come back with no answer, and no answer is a first-class result rather
|
|
311
|
+
* than a failure to work around: the oracle is conservative on purpose, and it would rather
|
|
312
|
+
* say nothing than say something wrong, because a rule reporting on a wrong type accuses
|
|
313
|
+
* correct code. A rule is expected to check for `undefined` and quietly stay silent, the same
|
|
314
|
+
* posture the rest of the navigation surface already takes on a dead handle.
|
|
315
|
+
*
|
|
316
|
+
* It reads the file in front of it **and the declaration files that file imports**, through
|
|
317
|
+
* the same tracked, confined reads `ctx.readFile` uses. Nothing above the project root is read,
|
|
318
|
+
* ever — so point lanekeep at the workspace root, the directory `node_modules` lives in,
|
|
319
|
+
* rather than at a package inside it. `--config` does not move the root.
|
|
320
|
+
*/
|
|
321
|
+
export interface TypeApi {
|
|
322
|
+
/**
|
|
323
|
+
* The type of the expression at `n`. `undefined` is that first-class no-answer, not a
|
|
324
|
+
* failure.
|
|
325
|
+
*/
|
|
326
|
+
typeOf(n: Node): TypeInfo | undefined
|
|
327
|
+
/** Where the identifier at `n` was declared. `undefined` on the same terms as `typeOf`. */
|
|
328
|
+
symbolOf(n: Node): SymbolInfo | undefined
|
|
329
|
+
/**
|
|
330
|
+
* What calling the function at `n` yields.
|
|
331
|
+
*
|
|
332
|
+
* Separate from `typeOf` because a function declaration is not an expression. Accepts a
|
|
333
|
+
* call expression, a function-like declaration, or an identifier bound to one. An
|
|
334
|
+
* annotated signature answers its annotation; an unannotated one with a single `return`
|
|
335
|
+
* answers that expression's type; several returns answer their union when every member is
|
|
336
|
+
* known and `undefined` otherwise; a function with no `return` answers `undefined` rather
|
|
337
|
+
* than guessing `void`. Type arguments are dropped throughout — `useQuery<Balance[]>`
|
|
338
|
+
* answers by the result type's name, never by the argument.
|
|
339
|
+
*/
|
|
340
|
+
returnTypeOf(n: Node): TypeInfo | undefined
|
|
341
|
+
/**
|
|
342
|
+
* Whether the type at `n` is the type `module` exports as `name`, or inherits from it.
|
|
343
|
+
*
|
|
344
|
+
* Nominal, never structural: a class reaches it through `extends` or `implements`, an
|
|
345
|
+
* interface through `extends`, an alias by being transparent, and a union only when *every*
|
|
346
|
+
* member does. A primitive answers `false` — but only when `module` and `name` resolve to a
|
|
347
|
+
* declaration; a target that does not resolve answers `undefined` whatever `n` is.
|
|
348
|
+
*
|
|
349
|
+
* **`undefined` is not `false`.** `false` means the walk completed and reached nothing;
|
|
350
|
+
* `undefined` means a link could not be read — an unresolvable import, a package that is
|
|
351
|
+
* not installed, a type with no symbol at all. A rule treating the two alike reports on
|
|
352
|
+
* code the oracle never saw.
|
|
353
|
+
*/
|
|
354
|
+
isAssignableTo(n: Node, module: string, name: string): boolean | undefined
|
|
355
|
+
/**
|
|
356
|
+
* Whether every import in the file being checked resolved to something readable.
|
|
357
|
+
*
|
|
358
|
+
* `false` is the honest label on a partial view: some name in this file came from a module
|
|
359
|
+
* the oracle could not open, so an `undefined` anywhere in it may be ignorance rather than
|
|
360
|
+
* a considered answer. A rule that reports only on what it established does not need to
|
|
361
|
+
* ask; a rule that wants to say "I could not check this file" does.
|
|
362
|
+
*
|
|
363
|
+
* An import that resolves to a file which does not *parse* counts as unreadable too: the
|
|
364
|
+
* names outside the broken span still answer, the ones inside it come back `undefined`, and
|
|
365
|
+
* nothing on either answer says which. An import of something that is not code — a
|
|
366
|
+
* stylesheet, a JSON asset, an image — is not counted at all, since it is not a module the
|
|
367
|
+
* oracle reads.
|
|
368
|
+
*
|
|
369
|
+
* **The verdict is the whole file's, and a parse fault is the whole declaration file's.**
|
|
370
|
+
* One `ERROR` node anywhere in a fifty-thousand-line `@types` bundle makes every file that
|
|
371
|
+
* imports it `false`, however far that span is from the names the rule asked about. Silence
|
|
372
|
+
* is the safe direction; a narrower verdict is a refinement rather than a promise.
|
|
373
|
+
*/
|
|
374
|
+
complete(): boolean
|
|
375
|
+
}
|
|
376
|
+
|
|
194
377
|
/** A rule's RuleContext surface. */
|
|
195
378
|
export interface RuleContext {
|
|
196
379
|
readonly filePath: string
|
|
@@ -217,8 +400,19 @@ export interface RuleContext {
|
|
|
217
400
|
emitFact(fact: Fact): void
|
|
218
401
|
loc(n: Node): NodeLocation | undefined
|
|
219
402
|
report(at: Node, message?: string | ReportOptions): void
|
|
220
|
-
/**
|
|
221
|
-
|
|
403
|
+
/**
|
|
404
|
+
* The type oracle, present only for a rule that declared `requires: ['types']`.
|
|
405
|
+
* Bounded by a fixed depth rather than by file: it follows an import into the
|
|
406
|
+
* declaration file that answers it, and the file after that.
|
|
407
|
+
*
|
|
408
|
+
* Typed as always present because there is no way to spell "present when
|
|
409
|
+
* this rule's own `requires` says so" as a type — so a rule that forgets the
|
|
410
|
+
* declaration still compiles. It finds out at the first call instead:
|
|
411
|
+
* `ctx.types` is `undefined` at run time, and `ctx.types.typeOf(...)` throws a
|
|
412
|
+
* `TypeError` rather than returning a quietly wrong answer. That loudness is
|
|
413
|
+
* deliberate.
|
|
414
|
+
*/
|
|
415
|
+
types: TypeApi
|
|
222
416
|
}
|
|
223
417
|
|
|
224
418
|
/** A violation the reduce phase reports, which has no node to point at. */
|
|
@@ -235,7 +429,51 @@ export interface ReduceLocation {
|
|
|
235
429
|
export interface ReduceContext {
|
|
236
430
|
readonly files: string[]
|
|
237
431
|
facts(kind?: string): EmittedFact[]
|
|
238
|
-
|
|
432
|
+
/** Takes a message; a supplied `fix` throws — there is no node to attach one to. */
|
|
433
|
+
report(at: ReduceLocation, message?: string | ReduceReportOptions): void
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Queries whose named captures drive the taint analysis. */
|
|
437
|
+
export type FlowSpec = {
|
|
438
|
+
/** Queries whose `@source` capture marks a tainted origin. */
|
|
439
|
+
sources: string[]
|
|
440
|
+
/** Queries whose `@sink` capture marks a forbidden destination. */
|
|
441
|
+
sinks: string[]
|
|
442
|
+
/** Queries whose `@sanitizer` capture clears taint from the value it produces. */
|
|
443
|
+
sanitizers?: string[]
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** One tainted flow: a source whose value reaches a sink with no intervening sanitizer. */
|
|
447
|
+
export type FlowPath = {
|
|
448
|
+
readonly source: Node
|
|
449
|
+
readonly sink: Node
|
|
450
|
+
/** The assignments and calls between source and sink, in flow order. One canonical path. */
|
|
451
|
+
readonly steps: readonly Node[]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A typestate obligation: an acquired resource must reach a release on every
|
|
456
|
+
* path out of `scope`. Requires `requires: ['dataflow']`.
|
|
457
|
+
*/
|
|
458
|
+
export type ObligationSpec = {
|
|
459
|
+
/** Queries whose `@acquire` capture starts an obligation on the captured value. */
|
|
460
|
+
acquire: string[]
|
|
461
|
+
/** Queries whose `@release` capture discharges it. */
|
|
462
|
+
release: string[]
|
|
463
|
+
/**
|
|
464
|
+
* `'function'` — every path out of the enclosing function, `return`/`throw` included.
|
|
465
|
+
* `'block'` — every path out of the block the acquire is in.
|
|
466
|
+
*/
|
|
467
|
+
scope: 'function' | 'block'
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** An acquire some path leaves undischarged, passed to `checkObligation`. */
|
|
471
|
+
export type UnmetObligation = {
|
|
472
|
+
readonly acquire: Node
|
|
473
|
+
/** The exit the value escapes through — a `return`, a `throw`, or the implicit end. */
|
|
474
|
+
readonly exit: Node
|
|
475
|
+
/** Whether any path *did* discharge it: partial coverage reads differently to none. */
|
|
476
|
+
readonly partial: boolean
|
|
239
477
|
}
|
|
240
478
|
|
|
241
479
|
/** A rule, as `defineRule` takes it. */
|
|
@@ -256,6 +494,15 @@ export interface Rule {
|
|
|
256
494
|
* or Rust rule means it silently never fires.
|
|
257
495
|
*/
|
|
258
496
|
language?: LanguageId | LanguageId[]
|
|
497
|
+
/**
|
|
498
|
+
* Host analyses this rule needs before it can run.
|
|
499
|
+
*
|
|
500
|
+
* Absent means none, which is every rule today. A rule declaring one the engine cannot
|
|
501
|
+
* provide is refused at load rather than run without it: an analysis that silently goes
|
|
502
|
+
* missing makes the rule report nothing, and a rule reporting nothing is indistinguishable
|
|
503
|
+
* from a codebase with nothing to report.
|
|
504
|
+
*/
|
|
505
|
+
requires?: Array<'types' | 'dataflow'>
|
|
259
506
|
/** How serious a violation is, before any config override. */
|
|
260
507
|
severity: Severity
|
|
261
508
|
/** What the rule tells whoever has to act on it. */
|
|
@@ -269,6 +516,11 @@ export interface Rule {
|
|
|
269
516
|
* what keeps a JavaScript rule affordable. Write the narrowest query that captures what
|
|
270
517
|
* you need; `check` then only refines.
|
|
271
518
|
*
|
|
519
|
+
* Optional — required only when `check` is present. A flow-only rule (`checkFlow` with no
|
|
520
|
+
* `check`) declares no top-level query; its file gate comes from the union of its `flow`
|
|
521
|
+
* queries instead (see {@link FlowSpec}). The type cannot express that conditional, so a
|
|
522
|
+
* `check` rule missing `query` type-checks here and is refused by the config loader instead.
|
|
523
|
+
*
|
|
272
524
|
* A single string applies to every declared language. An object maps each declared
|
|
273
525
|
* language to its own query — required when the grammars do not share node vocabulary
|
|
274
526
|
* (Python spells a call `call`, the other supported grammars say `call_expression`).
|
|
@@ -282,13 +534,29 @@ export interface Rule {
|
|
|
282
534
|
* no backreferences or lookaround. `#is?`, `#is-not?`, `#set!`, or an operator the
|
|
283
535
|
* binding does not know is refused at compile time.
|
|
284
536
|
*/
|
|
285
|
-
query
|
|
537
|
+
query?: string | Partial<Record<LanguageId, string>>
|
|
286
538
|
/** A per-invocation budget overriding the default, in milliseconds. */
|
|
287
539
|
timeout?: number
|
|
288
540
|
/** Called once per query match. */
|
|
289
541
|
check?(ctx: RuleContext, match: Match): void
|
|
290
542
|
/** Called once per run, after every file, with facts only. */
|
|
291
543
|
reduce?(ctx: ReduceContext): void
|
|
544
|
+
/**
|
|
545
|
+
* A taint-flow specification: queries whose captures mark tainted origins, forbidden
|
|
546
|
+
* destinations, and the calls that neutralize a value. Requires `requires: ['dataflow']`
|
|
547
|
+
* and a `checkFlow` handler; a rule declaring one without the other is refused at load.
|
|
548
|
+
*/
|
|
549
|
+
flow?: FlowSpec
|
|
550
|
+
/** Called once per canonical tainted flow from a source to a sink. */
|
|
551
|
+
checkFlow?(ctx: RuleContext, path: FlowPath): void
|
|
552
|
+
/**
|
|
553
|
+
* A typestate obligation: queries whose captures acquire an obligation on a value and
|
|
554
|
+
* queries that discharge it. Requires `requires: ['dataflow']` and a `checkObligation`
|
|
555
|
+
* handler; a rule declaring one without the other is refused at load.
|
|
556
|
+
*/
|
|
557
|
+
obligation?: ObligationSpec
|
|
558
|
+
/** Called once per value left with an unmet obligation at the end of its scope. */
|
|
559
|
+
checkObligation?(ctx: RuleContext, unmet: UnmetObligation): void
|
|
292
560
|
}
|
|
293
561
|
|
|
294
562
|
/** A lanekeep configuration, as `defineConfig` takes it. */
|
|
@@ -307,6 +575,13 @@ export interface Config {
|
|
|
307
575
|
rule?: number
|
|
308
576
|
/** Wall-clock, for the whole run. */
|
|
309
577
|
global?: number
|
|
578
|
+
/**
|
|
579
|
+
* Host-side type-provider work, across the whole run.
|
|
580
|
+
*
|
|
581
|
+
* Analysis time rather than elapsed time: only what the provider spends building
|
|
582
|
+
* programs and answering requests is charged against it.
|
|
583
|
+
*/
|
|
584
|
+
analysis?: number
|
|
310
585
|
}
|
|
311
586
|
/** Policy for suppression directives. All off by default. */
|
|
312
587
|
suppressions?: {
|
|
@@ -317,6 +592,19 @@ export interface Config {
|
|
|
317
592
|
/** Any whole-file directive is reported. */
|
|
318
593
|
forbidFileScope?: boolean
|
|
319
594
|
}
|
|
595
|
+
/** Which type oracle answers `ctx.types`. */
|
|
596
|
+
types?: {
|
|
597
|
+
/**
|
|
598
|
+
* `builtin` is lanekeep's own bounded oracle and needs no toolchain. `tsc` drives the
|
|
599
|
+
* project's own `typescript` package through a sidecar process: wider answers, a slower
|
|
600
|
+
* run, and a pre-commit hook that builds the project's program before it checks anything.
|
|
601
|
+
*/
|
|
602
|
+
provider?: 'builtin' | 'tsc'
|
|
603
|
+
/** How to launch the sidecar. `tsc` only. */
|
|
604
|
+
command?: string[]
|
|
605
|
+
/** The `typescript` package the sidecar loads, resolved from the project root. `tsc` only. */
|
|
606
|
+
typescript?: string
|
|
607
|
+
}
|
|
320
608
|
/** The rules to run, in order. */
|
|
321
609
|
rules: Rule[]
|
|
322
610
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanekeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.10.0",
|
|
27
|
+
"@lanekeep/linux-arm64": "0.10.0",
|
|
28
|
+
"@lanekeep/linux-x64": "0.10.0",
|
|
29
|
+
"@lanekeep/win32-x64": "0.10.0"
|
|
30
30
|
},
|
|
31
31
|
"main": "index.js",
|
|
32
32
|
"types": "index.d.ts",
|