@prisma-next/psl-parser 0.14.0-dev.5 → 0.14.0-dev.50

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 (62) hide show
  1. package/README.md +28 -10
  2. package/dist/declarations-DR6To8_k.mjs +1060 -0
  3. package/dist/declarations-DR6To8_k.mjs.map +1 -0
  4. package/dist/format.d.mts +1 -1
  5. package/dist/format.mjs +2 -1
  6. package/dist/format.mjs.map +1 -1
  7. package/dist/index.d.mts +239 -3
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.mjs +785 -3
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/parse-3-vr14ej.mjs +626 -0
  12. package/dist/parse-3-vr14ej.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/syntax.d.mts +20 -2
  16. package/dist/syntax.d.mts.map +1 -1
  17. package/dist/syntax.mjs +43 -2
  18. package/dist/syntax.mjs.map +1 -0
  19. package/package.json +9 -25
  20. package/src/attribute-spec/combinators/bool.ts +19 -0
  21. package/src/attribute-spec/combinators/diagnostic.ts +15 -0
  22. package/src/attribute-spec/combinators/entity-ref.ts +24 -0
  23. package/src/attribute-spec/combinators/field-ref.ts +36 -0
  24. package/src/attribute-spec/combinators/identifier.ts +16 -0
  25. package/src/attribute-spec/combinators/int.ts +19 -0
  26. package/src/attribute-spec/combinators/list.ts +43 -0
  27. package/src/attribute-spec/combinators/one-of.ts +29 -0
  28. package/src/attribute-spec/combinators/record.ts +43 -0
  29. package/src/attribute-spec/combinators/str.ts +19 -0
  30. package/src/attribute-spec/field-attribute.ts +27 -0
  31. package/src/attribute-spec/interpret.ts +154 -0
  32. package/src/attribute-spec/model-attribute.ts +27 -0
  33. package/src/attribute-spec/optional.ts +8 -0
  34. package/src/attribute-spec/types.ts +72 -0
  35. package/src/block-reconstruction.ts +139 -0
  36. package/src/exports/index.ts +57 -3
  37. package/src/exports/syntax.ts +25 -5
  38. package/src/extension-block.ts +107 -0
  39. package/src/parse.ts +23 -5
  40. package/src/resolve.ts +123 -0
  41. package/src/source-file.ts +25 -0
  42. package/src/symbol-table.ts +446 -0
  43. package/src/syntax/ast/attributes.ts +5 -6
  44. package/src/syntax/ast/declarations.ts +51 -26
  45. package/src/syntax/ast/expressions.ts +12 -13
  46. package/src/syntax/ast/identifier.ts +2 -3
  47. package/src/syntax/ast/qualified-name.ts +28 -19
  48. package/src/syntax/ast/type-annotation.ts +4 -5
  49. package/src/syntax/ast-helpers.ts +27 -3
  50. package/src/syntax/navigation.ts +55 -0
  51. package/src/syntax/red.ts +317 -42
  52. package/dist/parse-B_3gIEFd.mjs +0 -1421
  53. package/dist/parse-B_3gIEFd.mjs.map +0 -1
  54. package/dist/parse-BjZ1LPe6.d.mts.map +0 -1
  55. package/dist/parser-CaplKvRs.mjs +0 -1145
  56. package/dist/parser-CaplKvRs.mjs.map +0 -1
  57. package/dist/parser-Dfi3Wfdq.d.mts +0 -7
  58. package/dist/parser-Dfi3Wfdq.d.mts.map +0 -1
  59. package/dist/parser.d.mts +0 -3
  60. package/dist/parser.mjs +0 -3
  61. package/src/exports/parser.ts +0 -4
  62. package/src/parser.ts +0 -1642
@@ -0,0 +1,139 @@
1
+ import type { AuthoringPslBlockDescriptor } from '@prisma-next/framework-components/authoring';
2
+ import type {
3
+ PslBlockParam,
4
+ PslExtensionBlock,
5
+ PslExtensionBlockAttribute,
6
+ PslExtensionBlockParamValue,
7
+ PslSpan,
8
+ } from '@prisma-next/framework-components/psl-ast';
9
+ import type { ParseDiagnostic } from './parse';
10
+ import { nodePslSpan } from './resolve';
11
+ import type { SourceFile } from './source-file';
12
+ import type { GenericBlockDeclarationAst, KeyValuePairAst } from './syntax/ast/declarations';
13
+ import { ArrayLiteralAst, type ExpressionAst } from './syntax/ast/expressions';
14
+ import { printSyntax } from './syntax/ast-helpers';
15
+
16
+ /**
17
+ * Descriptor-free and unknown parameters become `value` stubs so validation can
18
+ * report them via key-set comparison. Duplicate member names are first-wins.
19
+ */
20
+ export function reconstructExtensionBlock(
21
+ node: GenericBlockDeclarationAst,
22
+ descriptor: AuthoringPslBlockDescriptor | undefined,
23
+ sourceFile: SourceFile,
24
+ diagnostics: ParseDiagnostic[],
25
+ ): PslExtensionBlock {
26
+ const keyword = node.keyword()?.text ?? '';
27
+ const blockName = node.name()?.name() ?? '';
28
+
29
+ const blockAttributes: PslExtensionBlockAttribute[] = [];
30
+ for (const attribute of node.attributes()) {
31
+ const name = attribute.name()?.path().join('.') ?? '';
32
+ const args = Array.from(attribute.argList()?.args() ?? [], (arg) => {
33
+ const value = arg.value();
34
+ return {
35
+ kind: 'positional' as const,
36
+ value: value === undefined ? '' : printSyntax(value.syntax).trim(),
37
+ span: nodePslSpan(arg.syntax, sourceFile),
38
+ };
39
+ });
40
+ blockAttributes.push({
41
+ name,
42
+ args,
43
+ span: nodePslSpan(attribute.syntax, sourceFile),
44
+ });
45
+ }
46
+
47
+ const parameters: Record<string, PslExtensionBlockParamValue> = {};
48
+ for (const entry of node.entries()) {
49
+ const key = entry.key()?.name();
50
+ if (key === undefined) continue;
51
+ const span = nodePslSpan(entry.syntax, sourceFile);
52
+ if (Object.hasOwn(parameters, key)) {
53
+ diagnostics.push({
54
+ code: 'PSL_EXTENSION_DUPLICATE_PARAMETER',
55
+ message: `Duplicate parameter "${key}" in "${keyword}" block "${blockName}"; first occurrence wins`,
56
+ range: {
57
+ start: sourceFile.positionAt(entry.syntax.offset),
58
+ end: sourceFile.positionAt(entry.syntax.offset + entry.syntax.green.textLength),
59
+ },
60
+ });
61
+ continue;
62
+ }
63
+ parameters[key] = reconstructParamValue(
64
+ entry,
65
+ descriptor?.parameters[key],
66
+ span,
67
+ sourceFile,
68
+ diagnostics,
69
+ );
70
+ }
71
+
72
+ return {
73
+ kind: descriptor?.discriminator ?? keyword,
74
+ name: blockName,
75
+ parameters,
76
+ blockAttributes,
77
+ span: nodePslSpan(node.syntax, sourceFile),
78
+ };
79
+ }
80
+
81
+ function reconstructParamValue(
82
+ entry: KeyValuePairAst,
83
+ param: PslBlockParam | undefined,
84
+ span: PslSpan,
85
+ sourceFile: SourceFile,
86
+ diagnostics: ParseDiagnostic[],
87
+ ): PslExtensionBlockParamValue {
88
+ const value = entry.value();
89
+ if (value === undefined) {
90
+ return { kind: 'bare', span };
91
+ }
92
+ return reconstructFromExpression(value, param, span, sourceFile, diagnostics);
93
+ }
94
+
95
+ function reconstructFromExpression(
96
+ value: ExpressionAst,
97
+ param: PslBlockParam | undefined,
98
+ span: PslSpan,
99
+ sourceFile: SourceFile,
100
+ diagnostics?: ParseDiagnostic[],
101
+ ): PslExtensionBlockParamValue {
102
+ const raw = printSyntax(value.syntax).trim();
103
+ if (param?.kind === 'list') {
104
+ const array = ArrayLiteralAst.cast(value.syntax);
105
+ if (!array) {
106
+ diagnostics?.push({
107
+ code: 'PSL_EXTENSION_INVALID_VALUE',
108
+ message: `List parameter expects an array literal, got ${raw}`,
109
+ range: {
110
+ start: sourceFile.positionAt(value.syntax.offset),
111
+ end: sourceFile.positionAt(value.syntax.offset + value.syntax.green.textLength),
112
+ },
113
+ });
114
+ return { kind: 'value', raw, span };
115
+ }
116
+
117
+ const items: PslExtensionBlockParamValue[] = [];
118
+ for (const element of array.elements()) {
119
+ items.push(
120
+ reconstructFromExpression(
121
+ element,
122
+ param.of,
123
+ nodePslSpan(element.syntax, sourceFile),
124
+ sourceFile,
125
+ diagnostics,
126
+ ),
127
+ );
128
+ }
129
+ return { kind: 'list', items, span };
130
+ }
131
+ switch (param?.kind) {
132
+ case 'ref':
133
+ return { kind: 'ref', identifier: raw, span };
134
+ case 'option':
135
+ return { kind: 'option', token: raw, span };
136
+ default:
137
+ return { kind: 'value', raw, span };
138
+ }
139
+ }
@@ -1,6 +1,4 @@
1
1
  export type {
2
- ParsePslDocumentInput,
3
- ParsePslDocumentResult,
4
2
  PslAttribute,
5
3
  PslAttributeArgument,
6
4
  PslAttributeNamedArgument,
@@ -38,4 +36,60 @@ export {
38
36
  namespacePslExtensionBlocks,
39
37
  } from '@prisma-next/framework-components/psl-ast';
40
38
  export { getPositionalArgument, parseQuotedStringLiteral } from '../attribute-helpers';
41
- export { parsePslDocument } from '../parser';
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';
69
+ export { findBlockDescriptor, validateExtensionBlockFromSymbol } from '../extension-block';
70
+ export {
71
+ keywordPslSpan,
72
+ nodePslSpan,
73
+ rangeToPslSpan,
74
+ readResolvedAttribute,
75
+ readResolvedAttributes,
76
+ readResolvedConstructorCall,
77
+ } from '../resolve';
78
+ export type {
79
+ BlockSymbol,
80
+ BuildSymbolTableOptions,
81
+ CompositeTypeSymbol,
82
+ FieldSymbol,
83
+ ModelSymbol,
84
+ NamespaceSymbol,
85
+ ResolvedAttribute,
86
+ ResolvedAttributeArg,
87
+ ResolvedNamedTypeBinding,
88
+ ResolvedTypeConstructorCall,
89
+ ScalarSymbol,
90
+ SymbolTable,
91
+ SymbolTableResult,
92
+ TopLevelScope,
93
+ TypeAliasSymbol,
94
+ } from '../symbol-table';
95
+ export { buildSymbolTable } from '../symbol-table';
@@ -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,107 @@
1
+ import {
2
+ type AuthoringPslBlockDescriptor,
3
+ type AuthoringPslBlockDescriptorNamespace,
4
+ isAuthoringPslBlockDescriptor,
5
+ } from '@prisma-next/framework-components/authoring';
6
+ import type { CodecLookup } from '@prisma-next/framework-components/codec';
7
+ import {
8
+ makePslNamespace,
9
+ makePslNamespaceEntries,
10
+ type PslDiagnostic,
11
+ type PslModel,
12
+ type PslSpan,
13
+ UNSPECIFIED_PSL_NAMESPACE_ID,
14
+ validateExtensionBlock,
15
+ } from '@prisma-next/framework-components/psl-ast';
16
+ import type { SourceFile } from './source-file';
17
+ import type { BlockSymbol, ModelSymbol, SymbolTable } from './symbol-table';
18
+
19
+ export function findBlockDescriptor(
20
+ descriptors: AuthoringPslBlockDescriptorNamespace | undefined,
21
+ keyword: string,
22
+ ): AuthoringPslBlockDescriptor | undefined {
23
+ if (descriptors === undefined) return undefined;
24
+ for (const value of Object.values(descriptors)) {
25
+ if (value === undefined) continue;
26
+ if (isAuthoringPslBlockDescriptor(value)) {
27
+ if (value.keyword === keyword) return value;
28
+ continue;
29
+ }
30
+ const nested = findBlockDescriptor(value, keyword);
31
+ if (nested !== undefined) return nested;
32
+ }
33
+ return undefined;
34
+ }
35
+
36
+ export function validateExtensionBlockFromSymbol(input: {
37
+ readonly block: BlockSymbol;
38
+ readonly descriptor: AuthoringPslBlockDescriptor;
39
+ readonly symbolTable: SymbolTable;
40
+ readonly sourceFile: SourceFile;
41
+ readonly sourceId: string;
42
+ readonly codecLookup: CodecLookup;
43
+ }): readonly PslDiagnostic[] {
44
+ const refCtx = buildRefResolutionContext(input.symbolTable, input.block);
45
+ return validateExtensionBlock(
46
+ input.block.block,
47
+ input.descriptor,
48
+ input.sourceId,
49
+ input.codecLookup,
50
+ refCtx,
51
+ );
52
+ }
53
+
54
+ const ZERO_SPAN: PslSpan = {
55
+ start: { offset: 0, line: 1, column: 1 },
56
+ end: { offset: 0, line: 1, column: 1 },
57
+ };
58
+
59
+ function buildRefResolutionContext(
60
+ symbolTable: SymbolTable,
61
+ block: BlockSymbol,
62
+ ): {
63
+ ownerNamespace: ReturnType<typeof makePslNamespace>;
64
+ allNamespaces: readonly ReturnType<typeof makePslNamespace>[];
65
+ } {
66
+ const unspecifiedNamespace = makeNamespace(
67
+ UNSPECIFIED_PSL_NAMESPACE_ID,
68
+ Object.values(symbolTable.topLevel.models),
69
+ );
70
+ const namedNamespaces = Object.values(symbolTable.topLevel.namespaces).map((namespace) =>
71
+ makeNamespace(namespace.name, Object.values(namespace.models)),
72
+ );
73
+ const allNamespaces = [unspecifiedNamespace, ...namedNamespaces];
74
+ const ownerNamespaceName = findOwnerNamespaceName(symbolTable, block);
75
+ const ownerNamespace =
76
+ allNamespaces.find((namespace) => namespace.name === ownerNamespaceName) ??
77
+ unspecifiedNamespace;
78
+ return { ownerNamespace, allNamespaces };
79
+ }
80
+
81
+ function makeNamespace(
82
+ name: string,
83
+ models: readonly ModelSymbol[],
84
+ ): ReturnType<typeof makePslNamespace> {
85
+ const modelStubs: PslModel[] = models.map((model) => ({
86
+ kind: 'model',
87
+ name: model.name,
88
+ fields: [],
89
+ attributes: [],
90
+ span: ZERO_SPAN,
91
+ }));
92
+ return makePslNamespace({
93
+ kind: 'namespace',
94
+ name,
95
+ entries: makePslNamespaceEntries(modelStubs, [], []),
96
+ span: ZERO_SPAN,
97
+ });
98
+ }
99
+
100
+ function findOwnerNamespaceName(symbolTable: SymbolTable, block: BlockSymbol): string {
101
+ for (const namespace of Object.values(symbolTable.topLevel.namespaces)) {
102
+ if (Object.values(namespace.blocks).some((candidate) => candidate === block)) {
103
+ return namespace.name;
104
+ }
105
+ }
106
+ return UNSPECIFIED_PSL_NAMESPACE_ID;
107
+ }
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 ADDED
@@ -0,0 +1,123 @@
1
+ import type { PslSpan } from '@prisma-next/framework-components/psl-ast';
2
+ import type { Position, Range, SourceFile } from './source-file';
3
+ import type {
4
+ AttributeArgListAst,
5
+ FieldAttributeAst,
6
+ ModelAttributeAst,
7
+ } from './syntax/ast/attributes';
8
+ import type { ExpressionAst } from './syntax/ast/expressions';
9
+ import type { QualifiedNameAst } from './syntax/ast/qualified-name';
10
+ import type { TypeAnnotationAst } from './syntax/ast/type-annotation';
11
+ import { printSyntax } from './syntax/ast-helpers';
12
+ import type { SyntaxNode } from './syntax/red';
13
+
14
+ export interface ResolvedAttributeArg {
15
+ readonly kind: 'positional' | 'named';
16
+ readonly name?: string;
17
+ readonly value: string;
18
+ readonly expression?: ExpressionAst;
19
+ readonly span: PslSpan;
20
+ }
21
+
22
+ export interface ResolvedAttribute {
23
+ readonly name: string;
24
+ readonly args: readonly ResolvedAttributeArg[];
25
+ readonly span: PslSpan;
26
+ }
27
+
28
+ export interface ResolvedTypeConstructorCall {
29
+ readonly path: readonly string[];
30
+ readonly args: readonly ResolvedAttributeArg[];
31
+ readonly span: PslSpan;
32
+ }
33
+
34
+ export function readResolvedAttribute(
35
+ attribute: FieldAttributeAst | ModelAttributeAst,
36
+ sourceFile: SourceFile,
37
+ ): ResolvedAttribute {
38
+ return {
39
+ name: attributeName(attribute.name()),
40
+ args: readResolvedArgList(attribute.argList(), sourceFile),
41
+ span: nodePslSpan(attribute.syntax, sourceFile),
42
+ };
43
+ }
44
+
45
+ export function readResolvedAttributes(
46
+ attributes: Iterable<FieldAttributeAst | ModelAttributeAst>,
47
+ sourceFile: SourceFile,
48
+ ): readonly ResolvedAttribute[] {
49
+ return Array.from(attributes, (attribute) => readResolvedAttribute(attribute, sourceFile));
50
+ }
51
+
52
+ export function readResolvedConstructorCall(
53
+ annotation: TypeAnnotationAst | undefined,
54
+ sourceFile: SourceFile,
55
+ ): ResolvedTypeConstructorCall | undefined {
56
+ const argList = annotation?.argList();
57
+ if (annotation === undefined || argList === undefined) return undefined;
58
+ return {
59
+ path: annotation.name()?.path() ?? [],
60
+ args: readResolvedArgList(argList, sourceFile),
61
+ span: nodePslSpan(annotation.syntax, sourceFile),
62
+ };
63
+ }
64
+
65
+ function readResolvedArgList(
66
+ argList: AttributeArgListAst | undefined,
67
+ sourceFile: SourceFile,
68
+ ): readonly ResolvedAttributeArg[] {
69
+ if (argList === undefined) return [];
70
+ const args: ResolvedAttributeArg[] = [];
71
+ for (const arg of argList.args()) {
72
+ const name = arg.name()?.name();
73
+ const expression = arg.value();
74
+ args.push({
75
+ kind: name !== undefined ? 'named' : 'positional',
76
+ ...(name !== undefined ? { name } : {}),
77
+ value: renderExpression(expression),
78
+ ...(expression !== undefined ? { expression } : {}),
79
+ span: nodePslSpan(arg.syntax, sourceFile),
80
+ });
81
+ }
82
+ return args;
83
+ }
84
+
85
+ function attributeName(name: QualifiedNameAst | undefined): string {
86
+ return name?.path().join('.') ?? '';
87
+ }
88
+
89
+ function renderExpression(expression: ExpressionAst | undefined): string {
90
+ if (expression === undefined) return '';
91
+ return printSyntax(expression.syntax).trim();
92
+ }
93
+
94
+ export function nodePslSpan(node: SyntaxNode, sourceFile: SourceFile): PslSpan {
95
+ const start = node.offset;
96
+ const end = start + node.green.textLength;
97
+ return {
98
+ start: offsetToPslPosition(start, sourceFile),
99
+ end: offsetToPslPosition(end, sourceFile),
100
+ };
101
+ }
102
+
103
+ /** Unsupported-top-level-block diagnostics are anchored to the keyword token. */
104
+ export function keywordPslSpan(node: SyntaxNode, keyword: string, sourceFile: SourceFile): PslSpan {
105
+ const start = node.offset;
106
+ const end = start + keyword.length;
107
+ return {
108
+ start: offsetToPslPosition(start, sourceFile),
109
+ end: offsetToPslPosition(end, sourceFile),
110
+ };
111
+ }
112
+
113
+ export function rangeToPslSpan(range: Range, sourceFile: SourceFile): PslSpan {
114
+ return {
115
+ start: offsetToPslPosition(sourceFile.offsetAt(range.start), sourceFile),
116
+ end: offsetToPslPosition(sourceFile.offsetAt(range.end), sourceFile),
117
+ };
118
+ }
119
+
120
+ function offsetToPslPosition(offset: number, sourceFile: SourceFile): PslSpan['start'] {
121
+ const position: Position = sourceFile.positionAt(offset);
122
+ return { offset, line: position.line + 1, column: position.character + 1 };
123
+ }
@@ -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);