lanekeep 0.5.0 → 0.6.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 +7 -4
- package/builtin.d.ts +24 -0
- package/index.d.ts +308 -0
- package/index.js +15 -0
- package/package.json +17 -5
package/README.md
CHANGED
|
@@ -138,10 +138,13 @@ that gates it.
|
|
|
138
138
|
**The card is not documentation.** `message`, `remediation` and `examples` are mandatory, because
|
|
139
139
|
they are what gets fed back to whoever has to act on the violation — increasingly an agent.
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
**Editor types ship with the npm package.** `npm install --save-dev lanekeep` gives you the
|
|
142
|
+
binary *and* TypeScript definitions for the whole host API, so `ctx` autocompletes and a typo'd
|
|
143
|
+
method is a compile error rather than a rule that throws in the sandbox. They are checked
|
|
144
|
+
against the engine's own registration, so they cannot drift from what actually exists.
|
|
145
|
+
|
|
146
|
+
A Go, Python or Rust project that wants them can add the npm package as a dev dependency
|
|
147
|
+
purely for authoring — nothing about the checker needs Node.
|
|
145
148
|
|
|
146
149
|
## Supported languages
|
|
147
150
|
|
package/builtin.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types reached through the `typesVersions` mapping in `package.json`.
|
|
3
|
+
*
|
|
4
|
+
* That mapping points *every* specifier here, the bare `lanekeep` included — TypeScript's
|
|
5
|
+
* `"*"` pattern does not exclude the package root, and a narrower pattern would have to
|
|
6
|
+
* predict what future built-ins are called. So this file is a superset: it re-exports
|
|
7
|
+
* everything `index.d.ts` has, and adds the default export a built-in subpath needs.
|
|
8
|
+
*
|
|
9
|
+
* A `declare module 'lanekeep/*'` block inside `index.d.ts` would have been the obvious way
|
|
10
|
+
* to do this and does nothing at all: a `declare module` inside a file that has its own
|
|
11
|
+
* imports or exports is module augmentation, not an ambient declaration, so TypeScript
|
|
12
|
+
* ignores it and the import stays unresolved. That failed silently until a compile test
|
|
13
|
+
* caught it.
|
|
14
|
+
*
|
|
15
|
+
* The default covers both shapes a built-in can take, because which one it is cannot be known
|
|
16
|
+
* from the specifier: a rule taking options is a factory — `noRestrictedImports({ ... })` —
|
|
17
|
+
* and one taking none is the rule itself.
|
|
18
|
+
*/
|
|
19
|
+
export * from './index'
|
|
20
|
+
|
|
21
|
+
import type { Rule } from './index'
|
|
22
|
+
|
|
23
|
+
declare const rule: Rule & ((options?: Record<string, unknown>) => Rule)
|
|
24
|
+
export default rule
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for authoring lanekeep rules.
|
|
3
|
+
*
|
|
4
|
+
* These describe the host API a rule reaches inside lanekeep's sandbox. Nothing here runs in
|
|
5
|
+
* Node: `defineRule` and `defineConfig` are identity functions whose only job is to give the
|
|
6
|
+
* compiler something to check against, and `RuleContext` is provided by lanekeep at run time.
|
|
7
|
+
*
|
|
8
|
+
* Every member below is asserted against the host's own registration in
|
|
9
|
+
* `crates/lanekeep-js/tests/host_types.rs`. A method that exists here and not there — or the
|
|
10
|
+
* reverse — fails that test, because a definition that drifts from the engine is worse than
|
|
11
|
+
* none: it produces confident autocomplete for something that does not exist.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A node in the parse tree.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately opaque. Nodes cross into the sandbox as integer handles rather than objects,
|
|
18
|
+
* which is one of the one-way doors in the architecture — and the reason this is a branded
|
|
19
|
+
* type rather than `number` is that **the root node's handle is `0`**. Written as a plain
|
|
20
|
+
* number, `if (!node)` looks like a null check and silently discards the root, which is how
|
|
21
|
+
* a rule loses its whole top-level case without any error.
|
|
22
|
+
*
|
|
23
|
+
* Compare against `undefined` explicitly.
|
|
24
|
+
*/
|
|
25
|
+
export type Node = number & { readonly __lanekeepNode: unique symbol }
|
|
26
|
+
|
|
27
|
+
/** How a name was introduced, as `ctx.bindingKind` reports it. */
|
|
28
|
+
export type BindingKind =
|
|
29
|
+
| 'import'
|
|
30
|
+
| 'const'
|
|
31
|
+
| 'let'
|
|
32
|
+
| 'var'
|
|
33
|
+
| 'param'
|
|
34
|
+
| 'function'
|
|
35
|
+
| 'class'
|
|
36
|
+
| 'catch-param'
|
|
37
|
+
| 'assignment'
|
|
38
|
+
| 'loop'
|
|
39
|
+
| 'context-manager'
|
|
40
|
+
| 'comprehension'
|
|
41
|
+
| 'type'
|
|
42
|
+
| 'receiver'
|
|
43
|
+
| 'type-param'
|
|
44
|
+
| 'module'
|
|
45
|
+
| 'trait'
|
|
46
|
+
|
|
47
|
+
/** A language lanekeep can parse. */
|
|
48
|
+
export type LanguageId = 'typescript' | 'tsx' | 'javascript' | 'python' | 'go' | 'rust'
|
|
49
|
+
|
|
50
|
+
/** How serious a violation is. */
|
|
51
|
+
export type Severity = 'error' | 'warn' | 'off'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The captures of one query match, keyed by capture name without the `@`.
|
|
55
|
+
*
|
|
56
|
+
* A capture that did not participate in the match is absent, which is why the values are
|
|
57
|
+
* optional — an alternation like `[(a) (b)] @x` binds `@x` either way, but two separate
|
|
58
|
+
* patterns capturing different names do not.
|
|
59
|
+
*/
|
|
60
|
+
export type Match = Record<string, Node | undefined>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* What a rule tells whoever has to act on the violation — increasingly an agent.
|
|
64
|
+
*
|
|
65
|
+
* Not documentation, and not optional. `remediation` is the field worth the effort: it should
|
|
66
|
+
* say what to do, not restate the problem.
|
|
67
|
+
*/
|
|
68
|
+
export interface RuleCard {
|
|
69
|
+
/** What is wrong, in a few words. */
|
|
70
|
+
message: string
|
|
71
|
+
/** What to do about it. */
|
|
72
|
+
remediation: string
|
|
73
|
+
/** One example each way. */
|
|
74
|
+
examples: {
|
|
75
|
+
bad: string
|
|
76
|
+
good: string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Cheap rejections applied before a file is parsed. */
|
|
81
|
+
export interface Gates {
|
|
82
|
+
/**
|
|
83
|
+
* Literal substrings a file's raw bytes must contain. A file missing any one of them is
|
|
84
|
+
* never parsed.
|
|
85
|
+
*
|
|
86
|
+
* **This is an *and*, not an *or*.** A rule matching either of two tokens cannot express
|
|
87
|
+
* its gate as `['a', 'b']` — that rejects any file containing only one, which is usually
|
|
88
|
+
* most of them, and the rule then reports nothing while looking healthy. There is no `or`
|
|
89
|
+
* form; omit the gate when no single substring covers every case.
|
|
90
|
+
*/
|
|
91
|
+
fileContains?: string[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A replacement a rule offers for a violation. */
|
|
95
|
+
export interface Fix {
|
|
96
|
+
/** The node whose text is replaced. */
|
|
97
|
+
node: Node
|
|
98
|
+
/** What to replace it with. */
|
|
99
|
+
text: string
|
|
100
|
+
/**
|
|
101
|
+
* Whether the fix preserves behavior.
|
|
102
|
+
*
|
|
103
|
+
* Only a fix marked `true` is applied by `--fix`. Anything else is a suggestion — shown,
|
|
104
|
+
* never written — because the cautious mistake costs a manual edit and the other one
|
|
105
|
+
* rewrites someone's code silently.
|
|
106
|
+
*/
|
|
107
|
+
safe?: boolean
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Options for a single report. */
|
|
111
|
+
export interface ReportOptions {
|
|
112
|
+
/** Overrides the card's `message` for this one violation. */
|
|
113
|
+
message?: string
|
|
114
|
+
/** A replacement to offer. */
|
|
115
|
+
fix?: Fix
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A fact a rule emits for the reduce phase.
|
|
120
|
+
*
|
|
121
|
+
* `kind` is required and must be non-empty, because it is what `ctx.facts('...')` filters on.
|
|
122
|
+
* A fact without one could never be retrieved, so emitting it is always a mistake — and a
|
|
123
|
+
* silent one, since the rule would look like it was working right up until `reduce` found
|
|
124
|
+
* nothing. lanekeep throws rather than accept it.
|
|
125
|
+
*/
|
|
126
|
+
export interface Fact {
|
|
127
|
+
kind: string
|
|
128
|
+
[key: string]: unknown
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A fact as `reduce` receives it, with the file that emitted it. */
|
|
132
|
+
export interface EmittedFact extends Fact {
|
|
133
|
+
/** Path of the file this came from, relative to the project root. */
|
|
134
|
+
file: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** What a rule's `check` handler reaches. */
|
|
138
|
+
export interface RuleContext {
|
|
139
|
+
/** Path of the file being checked, relative to the project root. */
|
|
140
|
+
readonly filePath: string
|
|
141
|
+
/** The whole file, as text. */
|
|
142
|
+
readonly fileText: string
|
|
143
|
+
/** The tree's root node. Its handle is `0` — see {@link Node}. */
|
|
144
|
+
readonly root: Node
|
|
145
|
+
|
|
146
|
+
/** The node's kind, as tree-sitter names it. */
|
|
147
|
+
kind(node: Node): string
|
|
148
|
+
/** The source text the node spans. */
|
|
149
|
+
text(node: Node): string
|
|
150
|
+
/** Whether this is a named node rather than an anonymous token. */
|
|
151
|
+
isNamed(node: Node): boolean
|
|
152
|
+
/** One-based line of the node's start. */
|
|
153
|
+
line(node: Node): number
|
|
154
|
+
/** One-based column of the node's start. */
|
|
155
|
+
column(node: Node): number
|
|
156
|
+
|
|
157
|
+
/** The node's parent, or `undefined` at the root. */
|
|
158
|
+
parent(node: Node): Node | undefined
|
|
159
|
+
/** Every child, including anonymous tokens. */
|
|
160
|
+
children(node: Node): Node[]
|
|
161
|
+
/** Named children only. */
|
|
162
|
+
namedChildren(node: Node): Node[]
|
|
163
|
+
/** Every ancestor, innermost first. */
|
|
164
|
+
ancestors(node: Node): Node[]
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether an identifier resolves to a given import.
|
|
168
|
+
*
|
|
169
|
+
* Handles aliasing, so `import { makeStyles as ms }` resolves correctly. This is the call
|
|
170
|
+
* that separates a rule from a grep: a text match both misses the alias and fires on a
|
|
171
|
+
* local of the same name.
|
|
172
|
+
*
|
|
173
|
+
* @param name Which export. Omit to match the module regardless of which name was taken.
|
|
174
|
+
*/
|
|
175
|
+
resolvesToImport(node: Node, module: string, name?: string): boolean
|
|
176
|
+
/** Whether an identifier came from a module matching this glob. */
|
|
177
|
+
isImportedFrom(node: Node, pattern: string): boolean
|
|
178
|
+
/** How the name was introduced, or `undefined` when it does not resolve. */
|
|
179
|
+
bindingKind(node: Node): BindingKind | undefined
|
|
180
|
+
/** Whether an outer binding of the same name is hidden by this one. */
|
|
181
|
+
isShadowed(node: Node): boolean
|
|
182
|
+
|
|
183
|
+
/** Run a query inside a subtree. */
|
|
184
|
+
querySubtree(node: Node, query: string): Match[]
|
|
185
|
+
/** The nearest ancestor matching a query, with its captures. */
|
|
186
|
+
closestAncestor(node: Node, query: string): Match | undefined
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Read another file, relative to the project root.
|
|
190
|
+
*
|
|
191
|
+
* Tracked: the read becomes part of the cache key, so a change to that file invalidates
|
|
192
|
+
* this one's result. Confined to the project root; `undefined` when absent or outside.
|
|
193
|
+
*/
|
|
194
|
+
readFile(path: string): string | undefined
|
|
195
|
+
/** Whether a file exists, tracked the same way. */
|
|
196
|
+
fileExists(path: string): boolean
|
|
197
|
+
|
|
198
|
+
/** Emit a fact for the reduce phase. */
|
|
199
|
+
emitFact(fact: Fact): void
|
|
200
|
+
/** Facts emitted so far, optionally filtered by `kind`. */
|
|
201
|
+
facts(kind?: string): EmittedFact[]
|
|
202
|
+
|
|
203
|
+
/** Report a violation at a node. */
|
|
204
|
+
report(at: Node, message?: string | ReportOptions): void
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A violation the reduce phase reports, which has no node to point at. */
|
|
208
|
+
export interface ReduceLocation {
|
|
209
|
+
/** Path relative to the project root. */
|
|
210
|
+
file: string
|
|
211
|
+
/** One-based. */
|
|
212
|
+
line?: number
|
|
213
|
+
/** One-based. */
|
|
214
|
+
column?: number
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* What a rule's `reduce` handler reaches.
|
|
219
|
+
*
|
|
220
|
+
* Deliberately smaller than {@link RuleContext}: **the reduce phase never touches parse
|
|
221
|
+
* trees.** Facts are small and serializable, which is what keeps cross-file rules parallel
|
|
222
|
+
* and cacheable — handing a tree to `reduce` would make the whole corpus resident.
|
|
223
|
+
*/
|
|
224
|
+
export interface ReduceContext {
|
|
225
|
+
/** Every file the run checked, relative to the project root. */
|
|
226
|
+
readonly files: string[]
|
|
227
|
+
/** Facts from every file, optionally filtered by `kind`. */
|
|
228
|
+
facts(kind?: string): EmittedFact[]
|
|
229
|
+
/** Report a violation against a file. */
|
|
230
|
+
report(at: ReduceLocation, message?: string | ReportOptions): void
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A rule, as `defineRule` takes it. */
|
|
234
|
+
export interface Rule {
|
|
235
|
+
/**
|
|
236
|
+
* Namespaced identifier, as `namespace/name`.
|
|
237
|
+
*
|
|
238
|
+
* `local/` needs no declaration and `lanekeep/` is reserved for built-ins; any other
|
|
239
|
+
* namespace must be listed in the config's `namespaces`.
|
|
240
|
+
*/
|
|
241
|
+
id: string
|
|
242
|
+
/**
|
|
243
|
+
* Which languages this rule applies to.
|
|
244
|
+
*
|
|
245
|
+
* **Defaults to `['typescript', 'tsx']`**, and this is the field most worth getting right
|
|
246
|
+
* on a rule for anything else. The grammar is chosen by the file, not by the rule, and a
|
|
247
|
+
* rule does not run on a file whose language it does not name — so omitting this on a Go
|
|
248
|
+
* or Rust rule means it silently never fires.
|
|
249
|
+
*/
|
|
250
|
+
language?: LanguageId | LanguageId[]
|
|
251
|
+
/** How serious a violation is, before any config override. */
|
|
252
|
+
severity: Severity
|
|
253
|
+
/** What the rule tells whoever has to act on it. */
|
|
254
|
+
card: RuleCard
|
|
255
|
+
/** Cheap rejections before parsing. */
|
|
256
|
+
gates?: Gates
|
|
257
|
+
/**
|
|
258
|
+
* The tree-sitter query gating the handler.
|
|
259
|
+
*
|
|
260
|
+
* Rust matches it across a single shared parse and only matches reach `check`, which is
|
|
261
|
+
* what keeps a JavaScript rule affordable. Write the narrowest query that captures what
|
|
262
|
+
* you need; `check` then only refines.
|
|
263
|
+
*/
|
|
264
|
+
query: string
|
|
265
|
+
/** A per-invocation budget overriding the default, in milliseconds. */
|
|
266
|
+
timeout?: number
|
|
267
|
+
/** Called once per query match. */
|
|
268
|
+
check?(ctx: RuleContext, match: Match): void
|
|
269
|
+
/** Called once per run, after every file, with facts only. */
|
|
270
|
+
reduce?(ctx: ReduceContext): void
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** A lanekeep configuration, as `defineConfig` takes it. */
|
|
274
|
+
export interface Config {
|
|
275
|
+
/** Globs selecting files to check, relative to the project root. */
|
|
276
|
+
include?: string[]
|
|
277
|
+
/** Globs removing files from that selection. */
|
|
278
|
+
exclude?: string[]
|
|
279
|
+
/** Rule-id namespaces this project uses beyond `local`. */
|
|
280
|
+
namespaces?: string[]
|
|
281
|
+
/** Override a rule's own severity, by id. */
|
|
282
|
+
severity?: Record<string, Severity>
|
|
283
|
+
/** Execution budgets, in milliseconds. */
|
|
284
|
+
timeouts?: {
|
|
285
|
+
/** Per rule invocation. */
|
|
286
|
+
rule?: number
|
|
287
|
+
/** Wall-clock, for the whole run. */
|
|
288
|
+
global?: number
|
|
289
|
+
}
|
|
290
|
+
/** The rules to run, in order. */
|
|
291
|
+
rules: Rule[]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Define a rule.
|
|
296
|
+
*
|
|
297
|
+
* An identity function. It exists so the compiler checks the object against {@link Rule}
|
|
298
|
+
* where it is written, rather than reporting a mismatch from wherever it is imported.
|
|
299
|
+
*/
|
|
300
|
+
export declare function defineRule(rule: Rule): Rule
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Define a configuration.
|
|
304
|
+
*
|
|
305
|
+
* An identity function, for the same reason as {@link defineRule}. Most projects will write
|
|
306
|
+
* `lanekeep.json` instead — configuration is data, and only rules need to be programs.
|
|
307
|
+
*/
|
|
308
|
+
export declare function defineConfig(config: Config): Config
|
package/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The runtime half of the authoring package.
|
|
2
|
+
//
|
|
3
|
+
// Rules never execute in Node — lanekeep evaluates them in its own sandbox, where `lanekeep`
|
|
4
|
+
// resolves to a host module rather than to this file. These exports exist so that a tool
|
|
5
|
+
// which *does* load a rule under Node (a bundler, a test runner, an editor's type server
|
|
6
|
+
// following the import) finds something coherent instead of a missing module.
|
|
7
|
+
//
|
|
8
|
+
// Identity functions, which is also what they are inside the sandbox: their entire job is to
|
|
9
|
+
// give the compiler a place to check the object against a type.
|
|
10
|
+
'use strict'
|
|
11
|
+
|
|
12
|
+
const identity = (value) => value
|
|
13
|
+
|
|
14
|
+
module.exports = { defineRule: identity, defineConfig: identity }
|
|
15
|
+
module.exports.default = module.exports
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanekeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Deterministic, AST-based architectural conformance checking",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -14,15 +14,27 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"bin/lanekeep",
|
|
16
16
|
"resolve.js",
|
|
17
|
+
"index.js",
|
|
18
|
+
"index.d.ts",
|
|
19
|
+
"builtin.d.ts",
|
|
17
20
|
"README.md"
|
|
18
21
|
],
|
|
19
22
|
"engines": {
|
|
20
23
|
"node": ">=18"
|
|
21
24
|
},
|
|
22
25
|
"optionalDependencies": {
|
|
23
|
-
"@lanekeep/darwin-arm64": "0.
|
|
24
|
-
"@lanekeep/linux-arm64": "0.
|
|
25
|
-
"@lanekeep/linux-x64": "0.
|
|
26
|
-
"@lanekeep/win32-x64": "0.
|
|
26
|
+
"@lanekeep/darwin-arm64": "0.6.0",
|
|
27
|
+
"@lanekeep/linux-arm64": "0.6.0",
|
|
28
|
+
"@lanekeep/linux-x64": "0.6.0",
|
|
29
|
+
"@lanekeep/win32-x64": "0.6.0"
|
|
30
|
+
},
|
|
31
|
+
"main": "index.js",
|
|
32
|
+
"types": "index.d.ts",
|
|
33
|
+
"typesVersions": {
|
|
34
|
+
"*": {
|
|
35
|
+
"*": [
|
|
36
|
+
"builtin.d.ts"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
27
39
|
}
|
|
28
40
|
}
|