@daml-tools/lint-plugin 0.3.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 -0
- package/dist/index.d.ts +290 -0
- package/package.json +29 -0
- package/templates/minimal-rule.ts +14 -0
- package/templates/project/README.md +20 -0
- package/templates/project/fixtures/missing-ensure.daml +9 -0
- package/templates/project/fixtures/with-ensure.daml +10 -0
- package/templates/project/package.json +17 -0
- package/templates/project/src/template-requires-ensure.ts +14 -0
- package/templates/project/tsconfig.json +11 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @daml-tools/lint-plugin
|
|
2
|
+
|
|
3
|
+
TypeScript types and starter templates for external `daml-lint --rules`
|
|
4
|
+
custom rule plugin authors.
|
|
5
|
+
|
|
6
|
+
Install it with TypeScript and esbuild in your rule project:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm pkg set type=module
|
|
10
|
+
npm install --save-dev @daml-tools/lint-plugin typescript esbuild
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Author rules in TypeScript, keep top-level `const NAME`, `const SEVERITY`, an
|
|
14
|
+
optional `const DESCRIPTION`, and top-level visitor `function` declarations,
|
|
15
|
+
then assign the same values to `globalThis.__daml_lint_rule` so TypeScript can
|
|
16
|
+
validate the rule object. Bundle the rule to one JavaScript file before passing
|
|
17
|
+
it to `daml-lint --rules`.
|
|
18
|
+
|
|
19
|
+
Runtime helper functions are intentionally not exported. The package is the
|
|
20
|
+
public rule-facing IR contract and starter templates for plugin projects.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// Type definitions for daml-lint custom rule authoring packages.
|
|
2
|
+
//
|
|
3
|
+
// Import these types from the published package when writing external custom
|
|
4
|
+
// rules, compile TypeScript to bundled JavaScript, and pass the .js file to
|
|
5
|
+
// daml-lint --rules. Node shapes mirror daml-lint src/ir.rs.
|
|
6
|
+
//
|
|
7
|
+
// v3: nodes carry structured expression and type ASTs. Compatibility-only raw
|
|
8
|
+
// fields and rendered party-name lists from v1/v2 have been removed.
|
|
9
|
+
|
|
10
|
+
/** Span of a declaration-level node (template, choice, field, ...). */
|
|
11
|
+
export interface Span {
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Source range for typed parser nodes.
|
|
18
|
+
*
|
|
19
|
+
* `start`/`end` are UTF-16 code-unit offsets into `module.source`, so
|
|
20
|
+
* `module.source.slice(span.start, span.end)` returns the exact source text.
|
|
21
|
+
* `byte_start`/`byte_end` preserve the parser's UTF-8 byte-span basis. */
|
|
22
|
+
export interface SourceSpan extends Span {
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
byte_start: number;
|
|
26
|
+
byte_end: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Position of an expression-level node. 1-based; the file is the
|
|
30
|
+
* enclosing module's. */
|
|
31
|
+
export interface SrcPos {
|
|
32
|
+
line: number;
|
|
33
|
+
column: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Structured DAML type AST. Source spans support diagnostics and exact
|
|
37
|
+
* `module.source` slicing. Unknown/unparseable types are represented as null
|
|
38
|
+
* at the field that carries the type. */
|
|
39
|
+
export type TypeNode =
|
|
40
|
+
| { Con: { qualifier: string | null; name: string; span: SourceSpan } }
|
|
41
|
+
| { App: { head: TypeNode; args: TypeNode[]; span: SourceSpan } }
|
|
42
|
+
| { List: { inner: TypeNode; span: SourceSpan } }
|
|
43
|
+
| { Tuple: { items: TypeNode[]; span: SourceSpan } }
|
|
44
|
+
| { Fun: { param: TypeNode; result: TypeNode; span: SourceSpan } }
|
|
45
|
+
| { Var: { name: string; span: SourceSpan } }
|
|
46
|
+
| { Unit: { span: SourceSpan } }
|
|
47
|
+
| { Constrained: { body: TypeNode; span: SourceSpan } };
|
|
48
|
+
|
|
49
|
+
/** Expression AST. Tagged unions: use the key as discriminant, e.g.
|
|
50
|
+
* `if ("BinOp" in e && e.BinOp.op === "/") { ... }`.
|
|
51
|
+
*
|
|
52
|
+
* - Var: variable reference; qualifier is the module alias for qualified
|
|
53
|
+
* names (`Map.lookup` → { name: "lookup", qualifier: "Map" }).
|
|
54
|
+
* - Con: constructor / type name in expression position (`Some`, `Iou`).
|
|
55
|
+
* - Lit: kind is "Int" | "Decimal" | "Text" | "Char"; value is source text.
|
|
56
|
+
* - App: application, flattened (`f a b` has two args).
|
|
57
|
+
* - BinOp: op is source-level text (`+`, `/`, `&&`, "`div`" for backtick
|
|
58
|
+
* application, ".." for ranges).
|
|
59
|
+
* - DoBlock: nested do, lowered to statements like a choice body.
|
|
60
|
+
* - Record: construction (`Iou with amount = 1.0`) when base is Con,
|
|
61
|
+
* update (`this with owner = p`) otherwise. Punned fields and `..`
|
|
62
|
+
* spreads have value: null.
|
|
63
|
+
* - Unknown: no structured encoding (operator sections, comprehension
|
|
64
|
+
* qualifiers, recovered parse errors); raw preserves source text. */
|
|
65
|
+
export type Expr =
|
|
66
|
+
| { Var: { name: string; qualifier: string | null; span: SrcPos } }
|
|
67
|
+
| { Con: { name: string; qualifier: string | null; span: SrcPos } }
|
|
68
|
+
| { Lit: { kind: "Int" | "Decimal" | "Text" | "Char"; value: string; span: SrcPos } }
|
|
69
|
+
| { App: { func: Expr; args: Expr[]; span: SrcPos } }
|
|
70
|
+
| { BinOp: { op: string; lhs: Expr; rhs: Expr; span: SrcPos } }
|
|
71
|
+
| { Neg: { expr: Expr; span: SrcPos } }
|
|
72
|
+
| { Lambda: { params: string[]; body: Expr; span: SrcPos } }
|
|
73
|
+
| { If: { cond: Expr; then_branch: Expr; else_branch: Expr; span: SrcPos } }
|
|
74
|
+
| { Case: { scrutinee: Expr; alts: CaseAlt[]; span: SrcPos } }
|
|
75
|
+
| { DoBlock: { statements: Statement[]; span: SrcPos } }
|
|
76
|
+
| { LetIn: { bindings: LetBinding[]; body: Expr; span: SrcPos } }
|
|
77
|
+
| { Record: { base: Expr; fields: RecordField[]; span: SrcPos } }
|
|
78
|
+
| { Tuple: { items: Expr[]; span: SrcPos } }
|
|
79
|
+
| { List: { items: Expr[]; span: SrcPos } }
|
|
80
|
+
| { Unknown: { raw: string; span: SrcPos } };
|
|
81
|
+
|
|
82
|
+
export interface CaseAlt {
|
|
83
|
+
/** Pattern rendered to source text: "Some x", "[]", "_". */
|
|
84
|
+
pattern: string;
|
|
85
|
+
body: Expr;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface LetBinding {
|
|
89
|
+
/** Bound name; for function bindings includes parameters ("go x"). */
|
|
90
|
+
name: string;
|
|
91
|
+
value: Expr;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RecordField {
|
|
95
|
+
name: string;
|
|
96
|
+
/** null for punned fields (`Iou with owner`) and `..` spreads. */
|
|
97
|
+
value: Expr | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface Field {
|
|
101
|
+
name: string;
|
|
102
|
+
type_: TypeNode | null;
|
|
103
|
+
span: Span;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface EnsureClause {
|
|
107
|
+
expr: Expr;
|
|
108
|
+
span: Span;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Statements are single-key objects tagged by kind. Use the tag as a
|
|
112
|
+
* discriminant: `if ("Create" in stmt) { stmt.Create.template_name ... }`.
|
|
113
|
+
*
|
|
114
|
+
* Structured payloads (`value`, `condition_expr`, `cid`, `argument`) carry
|
|
115
|
+
* the parse tree. `Other.raw` is the deliberate raw-source form for statements
|
|
116
|
+
* with no structured encoding.
|
|
117
|
+
* `binder` is the pattern text bound by `x <- ...`, when
|
|
118
|
+
* present. Ledger actions under `$`/lambdas are surfaced as their own
|
|
119
|
+
* statements, in source order. An `if`/`case` is surfaced as a `Branch`
|
|
120
|
+
* whose `arms` are each their own statement scope (exactly one runs), so a
|
|
121
|
+
* rule must descend into `arms` to see effects inside a branch. */
|
|
122
|
+
export type Statement =
|
|
123
|
+
| {
|
|
124
|
+
Let: {
|
|
125
|
+
name: string;
|
|
126
|
+
value: Expr;
|
|
127
|
+
span: SrcPos;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
Assert: {
|
|
132
|
+
condition_expr: Expr;
|
|
133
|
+
span: SrcPos;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
| {
|
|
137
|
+
Fetch: {
|
|
138
|
+
cid: Expr;
|
|
139
|
+
binder: string | null;
|
|
140
|
+
span: SrcPos;
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
| {
|
|
144
|
+
Archive: {
|
|
145
|
+
cid: Expr;
|
|
146
|
+
span: SrcPos;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
| {
|
|
150
|
+
Create: {
|
|
151
|
+
template_name: string;
|
|
152
|
+
argument: Expr;
|
|
153
|
+
binder: string | null;
|
|
154
|
+
span: SrcPos;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
| {
|
|
158
|
+
Exercise: {
|
|
159
|
+
choice_name: string;
|
|
160
|
+
cid: Expr;
|
|
161
|
+
argument: Expr | null;
|
|
162
|
+
binder: string | null;
|
|
163
|
+
span: SrcPos;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
| { TryCatch: { try_body: Statement[]; catch_body: Statement[]; span: SrcPos } }
|
|
167
|
+
/** `if`/`case`: each arm is an independent statement scope (exactly one
|
|
168
|
+
* arm runs at runtime). `scrutinee` is the `case <e> of` expression (null
|
|
169
|
+
* for `if`); each arm carries the source pattern it matched (null for the
|
|
170
|
+
* `if` then/else arms). */
|
|
171
|
+
| { Branch: { scrutinee: Expr | null; arms: BranchArm[]; span: SrcPos } }
|
|
172
|
+
| { Other: { raw: string; expr: Expr; binder: string | null; span: SrcPos } };
|
|
173
|
+
|
|
174
|
+
/** One arm of a `Branch`: the matched case pattern (null for `if` arms) and the
|
|
175
|
+
* arm's own statement scope. */
|
|
176
|
+
export interface BranchArm {
|
|
177
|
+
pattern: string | null;
|
|
178
|
+
body: Statement[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface Choice {
|
|
182
|
+
name: string;
|
|
183
|
+
consuming: boolean;
|
|
184
|
+
controller_exprs: Expr[];
|
|
185
|
+
/** Choice observers, if declared. */
|
|
186
|
+
observer_exprs: Expr[];
|
|
187
|
+
parameters: Field[];
|
|
188
|
+
return_type: TypeNode | null;
|
|
189
|
+
body: Statement[];
|
|
190
|
+
span: Span;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface InterfaceInstance {
|
|
194
|
+
interface_name: string;
|
|
195
|
+
/** Implemented method names, in declaration order. */
|
|
196
|
+
methods: string[];
|
|
197
|
+
span: Span;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface Template {
|
|
201
|
+
name: string;
|
|
202
|
+
fields: Field[];
|
|
203
|
+
signatory_exprs: Expr[];
|
|
204
|
+
observer_exprs: Expr[];
|
|
205
|
+
ensure_clause: EnsureClause | null;
|
|
206
|
+
/** `key <expr> : <Type>`, if declared. */
|
|
207
|
+
key_expr: Expr | null;
|
|
208
|
+
key_type: TypeNode | null;
|
|
209
|
+
maintainer_exprs: Expr[];
|
|
210
|
+
choices: Choice[];
|
|
211
|
+
/** Interfaces this template implements. */
|
|
212
|
+
interface_instances: InterfaceInstance[];
|
|
213
|
+
span: Span;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface InterfaceMethod {
|
|
217
|
+
name: string;
|
|
218
|
+
type_: TypeNode | null;
|
|
219
|
+
span: Span;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A DAML interface declaration. Visited via on_interface. */
|
|
223
|
+
export interface DamlInterface {
|
|
224
|
+
name: string;
|
|
225
|
+
/** Interfaces this interface requires (`requires Lockable.I`). */
|
|
226
|
+
requires: string[];
|
|
227
|
+
viewtype: string | null;
|
|
228
|
+
methods: InterfaceMethod[];
|
|
229
|
+
choices: Choice[];
|
|
230
|
+
span: Span;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface DamlFunction {
|
|
234
|
+
name: string;
|
|
235
|
+
/** Declared type signature, if present. */
|
|
236
|
+
type_signature: TypeNode | null;
|
|
237
|
+
body: Statement[];
|
|
238
|
+
span: Span;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface Import {
|
|
242
|
+
module_name: string;
|
|
243
|
+
qualified: boolean;
|
|
244
|
+
alias: string | null;
|
|
245
|
+
span: Span;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface DamlModule {
|
|
249
|
+
ir_version: 3;
|
|
250
|
+
name: string;
|
|
251
|
+
file: string;
|
|
252
|
+
imports: Import[];
|
|
253
|
+
templates: Template[];
|
|
254
|
+
interfaces: DamlInterface[];
|
|
255
|
+
functions: DamlFunction[];
|
|
256
|
+
source: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export type DamlLintRuleSeverity = "critical" | "high" | "medium" | "low" | "info";
|
|
260
|
+
|
|
261
|
+
export type DamlLintRuleVisitorModule = {
|
|
262
|
+
on_template: (template: Template) => void;
|
|
263
|
+
on_choice: (choice: Choice, template: Template) => void;
|
|
264
|
+
on_field: (field: Field, template: Template) => void;
|
|
265
|
+
on_function: (fn: DamlFunction) => void;
|
|
266
|
+
on_import: (imp: Import) => void;
|
|
267
|
+
on_interface: (iface: DamlInterface) => void;
|
|
268
|
+
check: (module: DamlModule) => void;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export type DamlLintRuleVisitor = {
|
|
272
|
+
[Name in keyof DamlLintRuleVisitorModule]: Pick<DamlLintRuleVisitorModule, Name> &
|
|
273
|
+
Partial<Omit<DamlLintRuleVisitorModule, Name>>;
|
|
274
|
+
}[keyof DamlLintRuleVisitorModule];
|
|
275
|
+
|
|
276
|
+
export type DamlLintRuleModule = {
|
|
277
|
+
NAME: string;
|
|
278
|
+
SEVERITY: DamlLintRuleSeverity;
|
|
279
|
+
DESCRIPTION?: string;
|
|
280
|
+
} & DamlLintRuleVisitor;
|
|
281
|
+
|
|
282
|
+
export type DamlLintReportTarget = { span: Span } | { span: SrcPos } | number;
|
|
283
|
+
|
|
284
|
+
declare global {
|
|
285
|
+
var __daml_lint_rule: DamlLintRuleModule | undefined;
|
|
286
|
+
|
|
287
|
+
/** Report a finding at a node's span, or at an explicit 1-based line number.
|
|
288
|
+
* `evidence`, when supplied, is used in reports instead of the source line. */
|
|
289
|
+
function report(node: DamlLintReportTarget, message: string, evidence?: string): void;
|
|
290
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@daml-tools/lint-plugin",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "TypeScript contract and starter templates for daml-lint custom rule plugins",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/stevennevins/daml-tools.git",
|
|
9
|
+
"directory": "crates/daml-lint/lint-plugin"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"./templates/minimal-rule": "./templates/minimal-rule.ts",
|
|
18
|
+
"./templates/project/package": "./templates/project/package.json",
|
|
19
|
+
"./templates/project/tsconfig": "./templates/project/tsconfig.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/index.d.ts",
|
|
23
|
+
"templates",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DamlLintRuleModule, Template } from "@daml-tools/lint-plugin";
|
|
2
|
+
|
|
3
|
+
const NAME = "template-requires-ensure";
|
|
4
|
+
const SEVERITY = "medium";
|
|
5
|
+
const DESCRIPTION = "Every template must declare an ensure clause";
|
|
6
|
+
|
|
7
|
+
function on_template(template: Template): void {
|
|
8
|
+
if (template.ensure_clause === null) {
|
|
9
|
+
report(template, `Template '${template.name}' has no ensure clause`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rule: DamlLintRuleModule = { NAME, SEVERITY, DESCRIPTION, on_template };
|
|
14
|
+
globalThis.__daml_lint_rule = rule;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# daml-lint plugin starter
|
|
2
|
+
|
|
3
|
+
This project shows the minimal TypeScript flow for a `daml-lint --rules`
|
|
4
|
+
custom rule plugin.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm install
|
|
8
|
+
npm run check
|
|
9
|
+
npm run build
|
|
10
|
+
daml-lint fixtures/missing-ensure.daml --rules dist/template-requires-ensure.js --fail-on info
|
|
11
|
+
daml-lint fixtures/with-ensure.daml --rules dist/template-requires-ensure.js --fail-on info
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The first scan reports one `template-requires-ensure` finding. The second scan
|
|
15
|
+
has no finding from the custom rule.
|
|
16
|
+
|
|
17
|
+
The runtime still discovers top-level metadata constants and visitor
|
|
18
|
+
`function` declarations. The `globalThis.__daml_lint_rule` assignment gives
|
|
19
|
+
TypeScript a single rule object to validate, but it is not the only runtime
|
|
20
|
+
discovery mechanism.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "daml-lint-plugin-template-requires-ensure",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"check": "tsc --noEmit",
|
|
8
|
+
"build": "esbuild src/template-requires-ensure.ts --bundle --format=esm --target=es2020 --outfile=dist/template-requires-ensure.js",
|
|
9
|
+
"lint:missing": "daml-lint fixtures/missing-ensure.daml --rules dist/template-requires-ensure.js --fail-on info",
|
|
10
|
+
"lint:clean": "daml-lint fixtures/with-ensure.daml --rules dist/template-requires-ensure.js --fail-on info"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@daml-tools/lint-plugin": "^0.3.0",
|
|
14
|
+
"esbuild": "^0.28.1",
|
|
15
|
+
"typescript": "^6.0.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DamlLintRuleModule, Template } from "@daml-tools/lint-plugin";
|
|
2
|
+
|
|
3
|
+
const NAME = "template-requires-ensure";
|
|
4
|
+
const SEVERITY = "medium";
|
|
5
|
+
const DESCRIPTION = "Every template must declare an ensure clause";
|
|
6
|
+
|
|
7
|
+
function on_template(template: Template): void {
|
|
8
|
+
if (template.ensure_clause === null) {
|
|
9
|
+
report(template, `Template '${template.name}' has no ensure clause`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rule: DamlLintRuleModule = { NAME, SEVERITY, DESCRIPTION, on_template };
|
|
14
|
+
globalThis.__daml_lint_rule = rule;
|