@prisma-next/psl-parser 0.14.0-dev.7 → 0.14.0-dev.71

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.
Files changed (58) hide show
  1. package/dist/{declarations-D9h_ihD3.mjs → declarations-DR6To8_k.mjs} +295 -55
  2. package/dist/declarations-DR6To8_k.mjs.map +1 -0
  3. package/dist/format.d.mts +1 -1
  4. package/dist/format.mjs +2 -2
  5. package/dist/index.d.mts +95 -112
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +374 -60
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/interpret.d.mts +29 -0
  10. package/dist/interpret.d.mts.map +1 -0
  11. package/dist/interpret.mjs +9 -0
  12. package/dist/interpret.mjs.map +1 -0
  13. package/dist/{parse-BjZ1LPe6.d.mts → parse-BazJr7Ye.d.mts} +128 -51
  14. package/dist/parse-BazJr7Ye.d.mts.map +1 -0
  15. package/dist/{parse-DhEV6av6.mjs → parse-CdeXr0T3.mjs} +28 -7
  16. package/dist/parse-CdeXr0T3.mjs.map +1 -0
  17. package/dist/symbol-table-C-AH04Ug.d.mts +127 -0
  18. package/dist/symbol-table-C-AH04Ug.d.mts.map +1 -0
  19. package/dist/syntax.d.mts +20 -2
  20. package/dist/syntax.d.mts.map +1 -1
  21. package/dist/syntax.mjs +43 -3
  22. package/dist/syntax.mjs.map +1 -0
  23. package/package.json +9 -7
  24. package/src/attribute-spec/combinators/bool.ts +19 -0
  25. package/src/attribute-spec/combinators/diagnostic.ts +15 -0
  26. package/src/attribute-spec/combinators/entity-ref.ts +24 -0
  27. package/src/attribute-spec/combinators/field-ref.ts +36 -0
  28. package/src/attribute-spec/combinators/identifier.ts +16 -0
  29. package/src/attribute-spec/combinators/int.ts +19 -0
  30. package/src/attribute-spec/combinators/list.ts +43 -0
  31. package/src/attribute-spec/combinators/one-of.ts +29 -0
  32. package/src/attribute-spec/combinators/record.ts +43 -0
  33. package/src/attribute-spec/combinators/str.ts +19 -0
  34. package/src/attribute-spec/field-attribute.ts +27 -0
  35. package/src/attribute-spec/interpret.ts +154 -0
  36. package/src/attribute-spec/model-attribute.ts +27 -0
  37. package/src/attribute-spec/optional.ts +8 -0
  38. package/src/attribute-spec/types.ts +72 -0
  39. package/src/block-reconstruction.ts +1 -0
  40. package/src/exports/index.ts +30 -0
  41. package/src/exports/interpret.ts +2 -0
  42. package/src/exports/syntax.ts +25 -5
  43. package/src/interpret.ts +40 -0
  44. package/src/parse.ts +23 -5
  45. package/src/resolve.ts +4 -1
  46. package/src/source-file.ts +25 -0
  47. package/src/syntax/ast/attributes.ts +5 -6
  48. package/src/syntax/ast/declarations.ts +51 -26
  49. package/src/syntax/ast/expressions.ts +12 -13
  50. package/src/syntax/ast/identifier.ts +2 -3
  51. package/src/syntax/ast/qualified-name.ts +28 -19
  52. package/src/syntax/ast/type-annotation.ts +4 -5
  53. package/src/syntax/ast-helpers.ts +27 -3
  54. package/src/syntax/navigation.ts +55 -0
  55. package/src/syntax/red.ts +317 -42
  56. package/dist/declarations-D9h_ihD3.mjs.map +0 -1
  57. package/dist/parse-BjZ1LPe6.d.mts.map +0 -1
  58. package/dist/parse-DhEV6av6.mjs.map +0 -1
@@ -0,0 +1,154 @@
1
+ import type { PslDiagnostic, PslSpan } from '@prisma-next/framework-components/psl-ast';
2
+ import { blindCast } from '@prisma-next/utils/casts';
3
+ import { notOk, ok, type Result } from '@prisma-next/utils/result';
4
+ import { nodePslSpan } from '../resolve';
5
+ import type { FieldAttributeAst, ModelAttributeAst } from '../syntax/ast/attributes';
6
+ import type { AttributeArgAst } from '../syntax/ast/expressions';
7
+ import { ATTRIBUTE_DIAGNOSTIC_CODE } from './combinators/diagnostic';
8
+ import type { ArgType, AttributeSpec, InterpretCtx, OptionalArgType, Param } from './types';
9
+
10
+ export function interpretAttribute<Out>(
11
+ attrNode: FieldAttributeAst | ModelAttributeAst,
12
+ spec: AttributeSpec<Out>,
13
+ ctx: InterpretCtx,
14
+ ): Result<Out, readonly PslDiagnostic[]> {
15
+ const diagnostics: PslDiagnostic[] = [];
16
+ const attributeSpan = nodePslSpan(attrNode.syntax, ctx.sourceFile);
17
+
18
+ const output: Record<string, unknown> = {};
19
+ const seen = new Set<string>();
20
+ let positionalSlot = 0;
21
+ let reportedExcess = false;
22
+
23
+ for (const arg of attrNode.argList()?.args() ?? []) {
24
+ const name = arg.name()?.name();
25
+
26
+ let key: string;
27
+ let param: Param<unknown>;
28
+ if (name === undefined) {
29
+ const posParam = spec.positional[positionalSlot];
30
+ if (posParam === undefined) {
31
+ if (!reportedExcess) {
32
+ diagnostics.push(
33
+ diagnostic(
34
+ `Attribute "${spec.name}" received too many positional arguments`,
35
+ ctx,
36
+ attributeSpan,
37
+ ),
38
+ );
39
+ reportedExcess = true;
40
+ }
41
+ continue;
42
+ }
43
+ positionalSlot += 1;
44
+ key = posParam.key;
45
+ param = posParam.type;
46
+ } else {
47
+ const namedParam = Object.hasOwn(spec.named, name) ? spec.named[name] : undefined;
48
+ if (namedParam === undefined) {
49
+ diagnostics.push(
50
+ diagnostic(
51
+ `Attribute "${spec.name}" received unknown argument "${name}"`,
52
+ ctx,
53
+ nodePslSpan(arg.syntax, ctx.sourceFile),
54
+ ),
55
+ );
56
+ continue;
57
+ }
58
+ key = name;
59
+ param = namedParam;
60
+ }
61
+
62
+ if (seen.has(key)) {
63
+ diagnostics.push(
64
+ diagnostic(
65
+ `Attribute "${spec.name}" received duplicate argument "${key}"`,
66
+ ctx,
67
+ nodePslSpan(arg.syntax, ctx.sourceFile),
68
+ ),
69
+ );
70
+ continue;
71
+ }
72
+ seen.add(key);
73
+ const result = parseArgValue(arg, param, ctx, diagnostics);
74
+ if (result.ok) output[key] = result.value;
75
+ }
76
+
77
+ const finalized = new Set<string>();
78
+ const finalizeAbsentKey = (
79
+ key: string,
80
+ positionalParam: Param<unknown> | undefined,
81
+ namedParam: Param<unknown> | undefined,
82
+ ): void => {
83
+ if (finalized.has(key) || seen.has(key)) return;
84
+ finalized.add(key);
85
+ const effective = namedParam ?? positionalParam;
86
+ if (effective === undefined) return;
87
+ if (isOptionalArgType(effective)) {
88
+ if (effective.hasDefault) output[key] = effective.defaultValue;
89
+ return;
90
+ }
91
+ diagnostics.push(
92
+ diagnostic(
93
+ `Attribute "${spec.name}" is missing required argument "${key}"`,
94
+ ctx,
95
+ attributeSpan,
96
+ ),
97
+ );
98
+ };
99
+
100
+ for (const param of spec.positional) {
101
+ const namedParam = Object.hasOwn(spec.named, param.key) ? spec.named[param.key] : undefined;
102
+ finalizeAbsentKey(param.key, param.type, namedParam);
103
+ }
104
+ for (const key of Object.keys(spec.named)) {
105
+ finalizeAbsentKey(key, undefined, spec.named[key]);
106
+ }
107
+
108
+ if (diagnostics.length > 0) {
109
+ return notOk<readonly PslDiagnostic[]>(diagnostics);
110
+ }
111
+
112
+ const value = blindCast<
113
+ Out,
114
+ 'The engine builds the output object structurally from the spec; TypeScript cannot relate the dynamically-keyed record to the spec-inferred output type.'
115
+ >(output);
116
+ if (spec.refine !== undefined) {
117
+ const refineDiagnostics = spec.refine(value, ctx);
118
+ if (refineDiagnostics.length > 0) {
119
+ return notOk<readonly PslDiagnostic[]>(refineDiagnostics);
120
+ }
121
+ }
122
+ return ok(value);
123
+ }
124
+
125
+ function parseArgValue(
126
+ arg: AttributeArgAst,
127
+ argType: ArgType<unknown>,
128
+ ctx: InterpretCtx,
129
+ diagnostics: PslDiagnostic[],
130
+ ): Result<unknown, readonly PslDiagnostic[]> {
131
+ const value = arg.value();
132
+ if (value === undefined) {
133
+ const missing = diagnostic(
134
+ 'Attribute argument is missing a value',
135
+ ctx,
136
+ nodePslSpan(arg.syntax, ctx.sourceFile),
137
+ );
138
+ diagnostics.push(missing);
139
+ return notOk<readonly PslDiagnostic[]>([missing]);
140
+ }
141
+ const result = argType.parse(value, ctx);
142
+ if (!result.ok) {
143
+ for (const failure of result.failure) diagnostics.push(failure);
144
+ }
145
+ return result;
146
+ }
147
+
148
+ function isOptionalArgType(param: Param<unknown>): param is OptionalArgType<unknown> {
149
+ return 'optional' in param && param.optional === true;
150
+ }
151
+
152
+ function diagnostic(message: string, ctx: InterpretCtx, span: PslSpan): PslDiagnostic {
153
+ return { code: ATTRIBUTE_DIAGNOSTIC_CODE, message, sourceId: ctx.sourceId, span };
154
+ }
@@ -0,0 +1,27 @@
1
+ import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
2
+ import type { AttributeOut, AttributeSpec, InterpretCtx, Param, PositionalParam } from './types';
3
+
4
+ interface ModelAttributeConfig<
5
+ Pos extends readonly PositionalParam[],
6
+ Named extends Record<string, Param<unknown>>,
7
+ > {
8
+ readonly positional?: Pos;
9
+ readonly named?: Named;
10
+ readonly refine?: (
11
+ parsed: AttributeOut<Pos, Named>,
12
+ ctx: InterpretCtx,
13
+ ) => readonly PslDiagnostic[];
14
+ }
15
+
16
+ export function modelAttribute<
17
+ const Pos extends readonly PositionalParam[] = readonly [],
18
+ const Named extends Record<string, Param<unknown>> = Record<never, never>,
19
+ >(name: string, config: ModelAttributeConfig<Pos, Named>): AttributeSpec<AttributeOut<Pos, Named>> {
20
+ return {
21
+ level: 'model',
22
+ name,
23
+ positional: config.positional ?? [],
24
+ named: config.named ?? {},
25
+ ...(config.refine !== undefined ? { refine: config.refine } : {}),
26
+ };
27
+ }
@@ -0,0 +1,8 @@
1
+ import type { ArgType, OptionalArgType } from './types';
2
+
3
+ export function optional<T>(type: ArgType<T>, ...rest: [defaultValue: T] | []): OptionalArgType<T> {
4
+ if (rest.length === 0) {
5
+ return { ...type, optional: true, hasDefault: false };
6
+ }
7
+ return { ...type, optional: true, hasDefault: true, defaultValue: rest[0] };
8
+ }
@@ -0,0 +1,72 @@
1
+ import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast';
2
+ import type { Result } from '@prisma-next/utils/result';
3
+ import type { Simplify, UnionToIntersection } from '@prisma-next/utils/types';
4
+ import type { SourceFile } from '../source-file';
5
+ import type { FieldSymbol, ModelSymbol } from '../symbol-table';
6
+ import type { ExpressionAst } from '../syntax/ast/expressions';
7
+
8
+ export type AttributeLevel = 'field' | 'model' | 'block';
9
+
10
+ export interface ArgType<T> {
11
+ readonly kind: string;
12
+ readonly label: string;
13
+ // phantom carrier for `T`; never read at runtime.
14
+ readonly _out?: T;
15
+ parse(arg: ExpressionAst, ctx: InterpretCtx): Result<T, readonly PslDiagnostic[]>;
16
+ }
17
+
18
+ export interface InterpretCtx {
19
+ readonly level: AttributeLevel;
20
+ readonly sourceId: string;
21
+ readonly sourceFile: SourceFile;
22
+ readonly selfModel: ModelSymbol;
23
+ resolveReferencedModel(): ModelSymbol | undefined;
24
+ readonly field?: FieldSymbol;
25
+ }
26
+
27
+ export interface OptionalArgType<T> extends ArgType<T> {
28
+ // the engine detects optionality by checking for this marker (`'optional' in param`).
29
+ readonly optional: true;
30
+ readonly hasDefault: boolean;
31
+ readonly defaultValue?: T;
32
+ }
33
+
34
+ export type Param<T> = ArgType<T>;
35
+
36
+ export interface PositionalParam<T = unknown> {
37
+ readonly key: string;
38
+ readonly type: Param<T>;
39
+ }
40
+
41
+ export interface AttributeSpec<Out> {
42
+ readonly level: AttributeLevel;
43
+ readonly name: string;
44
+ readonly positional: readonly PositionalParam[];
45
+ readonly named: Readonly<Record<string, Param<unknown>>>;
46
+ readonly refine?: (parsed: Out, ctx: InterpretCtx) => readonly PslDiagnostic[];
47
+ }
48
+
49
+ export type OutOf<P> = P extends ArgType<infer T> ? T : never;
50
+
51
+ export type NamedOut<N extends Record<string, Param<unknown>>> = Simplify<
52
+ { [K in keyof N as N[K] extends OptionalArgType<unknown> ? never : K]: OutOf<N[K]> } & {
53
+ [K in keyof N as N[K] extends OptionalArgType<unknown> ? K : never]?: OutOf<N[K]>;
54
+ }
55
+ >;
56
+
57
+ type PosEntryObject<E extends PositionalParam> =
58
+ E['type'] extends OptionalArgType<unknown>
59
+ ? { [K in E['key']]?: OutOf<E['type']> }
60
+ : { [K in E['key']]: OutOf<E['type']> };
61
+
62
+ export type PosOut<Pos extends readonly PositionalParam[]> = Simplify<
63
+ UnionToIntersection<{ [I in keyof Pos]: PosEntryObject<Pos[I]> }[number]>
64
+ >;
65
+
66
+ export type AttributeOut<
67
+ Pos extends readonly PositionalParam[],
68
+ Named extends Record<string, Param<unknown>>,
69
+ > = Simplify<PosOut<Pos> & NamedOut<Named>>;
70
+
71
+ // `S` is unconstrained on purpose: `refine` makes `Out` contravariant, so a bound like `S extends AttributeSpec<unknown>` would reject every spec that uses `refine`.
72
+ export type InferAttr<S> = S extends AttributeSpec<infer Out> ? Out : never;
@@ -71,6 +71,7 @@ export function reconstructExtensionBlock(
71
71
 
72
72
  return {
73
73
  kind: descriptor?.discriminator ?? keyword,
74
+ keyword,
74
75
  name: blockName,
75
76
  parameters,
76
77
  blockAttributes,
@@ -36,6 +36,36 @@ export {
36
36
  namespacePslExtensionBlocks,
37
37
  } from '@prisma-next/framework-components/psl-ast';
38
38
  export { getPositionalArgument, parseQuotedStringLiteral } from '../attribute-helpers';
39
+ export { bool } from '../attribute-spec/combinators/bool';
40
+ export { leafDiagnostic } from '../attribute-spec/combinators/diagnostic';
41
+ export { entityRef } from '../attribute-spec/combinators/entity-ref';
42
+ export type { FieldRefArgType, FieldRefScope } from '../attribute-spec/combinators/field-ref';
43
+ export { fieldRef } from '../attribute-spec/combinators/field-ref';
44
+ export { identifier } from '../attribute-spec/combinators/identifier';
45
+ export { int } from '../attribute-spec/combinators/int';
46
+ export type { ListOptions } from '../attribute-spec/combinators/list';
47
+ export { list } from '../attribute-spec/combinators/list';
48
+ export { oneOf } from '../attribute-spec/combinators/one-of';
49
+ export { record } from '../attribute-spec/combinators/record';
50
+ export { str } from '../attribute-spec/combinators/str';
51
+ export { fieldAttribute } from '../attribute-spec/field-attribute';
52
+ export { interpretAttribute } from '../attribute-spec/interpret';
53
+ export { modelAttribute } from '../attribute-spec/model-attribute';
54
+ export { optional } from '../attribute-spec/optional';
55
+ export type {
56
+ ArgType,
57
+ AttributeLevel,
58
+ AttributeOut,
59
+ AttributeSpec,
60
+ InferAttr,
61
+ InterpretCtx,
62
+ NamedOut,
63
+ OptionalArgType,
64
+ OutOf,
65
+ Param,
66
+ PositionalParam,
67
+ PosOut,
68
+ } from '../attribute-spec/types';
39
69
  export { findBlockDescriptor, validateExtensionBlockFromSymbol } from '../extension-block';
40
70
  export {
41
71
  keywordPslSpan,
@@ -0,0 +1,2 @@
1
+ export type { PslInterpretCapable, PslInterpretInput } from '../interpret';
2
+ export { hasPslInterpreter } from '../interpret';
@@ -7,7 +7,13 @@ export {
7
7
  FieldAttributeAst,
8
8
  ModelAttributeAst,
9
9
  } from '../syntax/ast/attributes';
10
- export type { NamespaceMemberAst } from '../syntax/ast/declarations';
10
+ export type {
11
+ AttributeAst,
12
+ BlockMemberAst,
13
+ DeclarationAst,
14
+ GenericBlockMemberAst,
15
+ NamespaceMemberAst,
16
+ } from '../syntax/ast/declarations';
11
17
  export {
12
18
  CompositeTypeDeclarationAst,
13
19
  DocumentAst,
@@ -35,12 +41,26 @@ export {
35
41
  export { IdentifierAst } from '../syntax/ast/identifier';
36
42
  export { QualifiedNameAst } from '../syntax/ast/qualified-name';
37
43
  export { TypeAnnotationAst } from '../syntax/ast/type-annotation';
38
- export type { AstNode } from '../syntax/ast-helpers';
39
- export { filterChildren, findChildToken, findFirstChild, printSyntax } from '../syntax/ast-helpers';
44
+ export type { AstNode, BracedBlock } from '../syntax/ast-helpers';
45
+ export {
46
+ any,
47
+ filterChildren,
48
+ findChildToken,
49
+ findFirstChild,
50
+ printSyntax,
51
+ } from '../syntax/ast-helpers';
40
52
  export type { GreenElement, GreenNode, GreenToken } from '../syntax/green';
41
53
  export { greenNode, greenToken } from '../syntax/green';
42
54
  export { GreenNodeBuilder } from '../syntax/green-builder';
55
+ // Navigation helpers
56
+ export type { Direction } from '../syntax/navigation';
57
+ export {
58
+ isTrivia,
59
+ isTriviaKind,
60
+ nonTriviaSibling,
61
+ skipTriviaToken,
62
+ } from '../syntax/navigation';
43
63
  // Red layer
44
- export type { SyntaxElement, SyntaxToken } from '../syntax/red';
45
- export { createSyntaxTree, SyntaxNode } from '../syntax/red';
64
+ export type { SyntaxElement } from '../syntax/red';
65
+ export { createSyntaxTree, SyntaxNode, SyntaxToken, TokenAtOffset } from '../syntax/red';
46
66
  export type { SyntaxKind } from '../syntax/syntax-kind';
@@ -0,0 +1,40 @@
1
+ import type {
2
+ ContractSourceContext,
3
+ ContractSourceDiagnostic,
4
+ ContractSourceProvider,
5
+ PslContractSourceProvider,
6
+ } from '@prisma-next/config/config-types';
7
+ import type { SourceFile } from './source-file';
8
+ import type { SymbolTable } from './symbol-table';
9
+ import type { DocumentAst } from './syntax/ast/declarations';
10
+
11
+ /**
12
+ * Lets editor tooling that already parses incrementally (e.g. the language
13
+ * server) hand cached artifacts to the interpreter instead of forcing a
14
+ * disk re-parse.
15
+ */
16
+ export interface PslInterpretInput {
17
+ readonly document: DocumentAst;
18
+ readonly sourceFile: SourceFile;
19
+ readonly symbolTable: SymbolTable;
20
+ readonly sourceId: string;
21
+ }
22
+
23
+ /**
24
+ * Declared here — the authoring layer that owns `DocumentAst` / `SourceFile` /
25
+ * `SymbolTable` — because `@prisma-next/config` (core) cannot name authoring
26
+ * types.
27
+ */
28
+ export interface PslInterpretCapable extends PslContractSourceProvider {
29
+ interpret(
30
+ input: PslInterpretInput,
31
+ context: ContractSourceContext,
32
+ ): readonly ContractSourceDiagnostic[];
33
+ }
34
+
35
+ /** The single seam that narrows a contract source to the interpret capability. */
36
+ export function hasPslInterpreter(source: ContractSourceProvider): source is PslInterpretCapable {
37
+ return (
38
+ source.sourceFormat === 'psl' && 'interpret' in source && typeof source.interpret === 'function'
39
+ );
40
+ }
package/src/parse.ts CHANGED
@@ -399,14 +399,24 @@ function parseParenArgs(cursor: Cursor): void {
399
399
  }
400
400
  }
401
401
 
402
- export function parseAttributeArg(cursor: Cursor): GreenNode {
402
+ export function parseAttributeArg(cursor: Cursor): void {
403
+ const kind = cursor.peekKind();
404
+ if (
405
+ kind !== 'Ident' &&
406
+ kind !== 'StringLiteral' &&
407
+ kind !== 'NumberLiteral' &&
408
+ kind !== 'LBracket' &&
409
+ kind !== 'LBrace'
410
+ ) {
411
+ return;
412
+ }
403
413
  cursor.startNode('AttributeArg');
404
414
  if (cursor.peekKind() === 'Ident' && cursor.peekKind(1) === 'Colon') {
405
415
  parseIdentifier(cursor);
406
416
  cursor.bump();
407
417
  }
408
418
  parseArgValue(cursor);
409
- return cursor.finishNode();
419
+ cursor.finishNode();
410
420
  }
411
421
 
412
422
  function parseArgValue(cursor: Cursor): void {
@@ -435,8 +445,16 @@ export function parseAttribute(cursor: Cursor): GreenNode {
435
445
  return cursor.finishNode();
436
446
  }
437
447
 
438
- /** A type annotation: `QualifiedName (argList)? ([])? (?)?`, e.g. `pgvector.Vector(1536)[]?`. */
439
- export function parseTypeAnnotation(cursor: Cursor): GreenNode {
448
+ /**
449
+ * A type annotation: `QualifiedName (argList)? ([])? (?)?`, e.g.
450
+ * `pgvector.Vector(1536)[]?`. When the field has no type, no node is emitted —
451
+ * a missing type is the absence of a `TypeAnnotation`, not a zero-width one.
452
+ */
453
+ export function parseTypeAnnotation(cursor: Cursor): void {
454
+ const kind = cursor.peekKind();
455
+ if (kind !== 'Ident' && kind !== 'LBracket' && kind !== 'Question') {
456
+ return;
457
+ }
440
458
  cursor.startNode('TypeAnnotation');
441
459
  if (cursor.peekKind() === 'Ident') {
442
460
  parseQualifiedName(cursor);
@@ -453,7 +471,7 @@ export function parseTypeAnnotation(cursor: Cursor): GreenNode {
453
471
  if (cursor.peekKind() === 'Question') {
454
472
  cursor.bump();
455
473
  }
456
- return cursor.finishNode();
474
+ cursor.finishNode();
457
475
  }
458
476
 
459
477
  type MemberParser = (cursor: Cursor) => void;
package/src/resolve.ts CHANGED
@@ -15,6 +15,7 @@ export interface ResolvedAttributeArg {
15
15
  readonly kind: 'positional' | 'named';
16
16
  readonly name?: string;
17
17
  readonly value: string;
18
+ readonly expression?: ExpressionAst;
18
19
  readonly span: PslSpan;
19
20
  }
20
21
 
@@ -69,10 +70,12 @@ function readResolvedArgList(
69
70
  const args: ResolvedAttributeArg[] = [];
70
71
  for (const arg of argList.args()) {
71
72
  const name = arg.name()?.name();
73
+ const expression = arg.value();
72
74
  args.push({
73
75
  kind: name !== undefined ? 'named' : 'positional',
74
76
  ...(name !== undefined ? { name } : {}),
75
- value: renderExpression(arg.value()),
77
+ value: renderExpression(expression),
78
+ ...(expression !== undefined ? { expression } : {}),
76
79
  span: nodePslSpan(arg.syntax, sourceFile),
77
80
  });
78
81
  }
@@ -1,3 +1,4 @@
1
+ const CARRIAGE_RETURN = 13;
1
2
  const LINE_FEED = 10;
2
3
 
3
4
  export interface Position {
@@ -41,6 +42,30 @@ export class SourceFile {
41
42
  return this.#lineStarts;
42
43
  }
43
44
 
45
+ lineStartOffset(line: number): number {
46
+ if (line <= 0) {
47
+ return 0;
48
+ }
49
+ return this.#lineStarts[line] ?? this.#text.length;
50
+ }
51
+
52
+ lineEndOffset(line: number): number {
53
+ if (line < 0) {
54
+ return 0;
55
+ }
56
+
57
+ const nextLineStart = this.#lineStarts[line + 1];
58
+ if (nextLineStart === undefined) {
59
+ return this.#text.length;
60
+ }
61
+
62
+ const lineFeedOffset = nextLineStart - 1;
63
+ const carriageReturnOffset = lineFeedOffset - 1;
64
+ return this.#text.charCodeAt(carriageReturnOffset) === CARRIAGE_RETURN
65
+ ? carriageReturnOffset
66
+ : lineFeedOffset;
67
+ }
68
+
44
69
  positionAt(offset: number): Position {
45
70
  const clamped = clamp(offset, 0, this.#text.length);
46
71
  const line = this.#lineIndexAt(clamped);
@@ -1,7 +1,6 @@
1
- import type { Token } from '../../tokenizer';
2
1
  import type { AstNode } from '../ast-helpers';
3
2
  import { filterChildren, findChildToken, findFirstChild } from '../ast-helpers';
4
- import type { SyntaxNode } from '../red';
3
+ import type { SyntaxNode, SyntaxToken } from '../red';
5
4
  import { AttributeArgAst } from './expressions';
6
5
  import { QualifiedNameAst } from './qualified-name';
7
6
 
@@ -12,11 +11,11 @@ export class AttributeArgListAst implements AstNode {
12
11
  this.syntax = syntax;
13
12
  }
14
13
 
15
- lparen(): Token | undefined {
14
+ lparen(): SyntaxToken | undefined {
16
15
  return findChildToken(this.syntax, 'LParen');
17
16
  }
18
17
 
19
- rparen(): Token | undefined {
18
+ rparen(): SyntaxToken | undefined {
20
19
  return findChildToken(this.syntax, 'RParen');
21
20
  }
22
21
 
@@ -36,7 +35,7 @@ export class FieldAttributeAst implements AstNode {
36
35
  this.syntax = syntax;
37
36
  }
38
37
 
39
- at(): Token | undefined {
38
+ at(): SyntaxToken | undefined {
40
39
  return findChildToken(this.syntax, 'At');
41
40
  }
42
41
 
@@ -60,7 +59,7 @@ export class ModelAttributeAst implements AstNode {
60
59
  this.syntax = syntax;
61
60
  }
62
61
 
63
- doubleAt(): Token | undefined {
62
+ doubleAt(): SyntaxToken | undefined {
64
63
  return findChildToken(this.syntax, 'DoubleAt');
65
64
  }
66
65