@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 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.
@@ -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,9 @@
1
+ module MissingEnsure where
2
+
3
+ template Iou
4
+ with
5
+ issuer : Party
6
+ owner : Party
7
+ where
8
+ signatory issuer
9
+ observer owner
@@ -0,0 +1,10 @@
1
+ module WithEnsure where
2
+
3
+ template Iou
4
+ with
5
+ issuer : Party
6
+ owner : Party
7
+ where
8
+ signatory issuer
9
+ observer owner
10
+ ensure True
@@ -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;
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "lib": ["ES2020"]
9
+ },
10
+ "include": ["src/**/*.ts"]
11
+ }