@prisma-next/psl-parser 0.14.0 → 0.15.0-dev.1

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 (75) 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 +19 -0
  5. package/dist/format.d.mts.map +1 -0
  6. package/dist/format.mjs +470 -0
  7. package/dist/format.mjs.map +1 -0
  8. package/dist/index.d.mts +144 -3
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +849 -3
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/interpret.d.mts +39 -0
  13. package/dist/interpret.d.mts.map +1 -0
  14. package/dist/interpret.mjs +25 -0
  15. package/dist/interpret.mjs.map +1 -0
  16. package/dist/parse-BazJr7Ye.d.mts +426 -0
  17. package/dist/parse-BazJr7Ye.d.mts.map +1 -0
  18. package/dist/parse-CdeXr0T3.mjs +626 -0
  19. package/dist/parse-CdeXr0T3.mjs.map +1 -0
  20. package/dist/symbol-table-C-AH04Ug.d.mts +127 -0
  21. package/dist/symbol-table-C-AH04Ug.d.mts.map +1 -0
  22. package/dist/syntax.d.mts +21 -347
  23. package/dist/syntax.d.mts.map +1 -1
  24. package/dist/syntax.mjs +26 -1401
  25. package/dist/syntax.mjs.map +1 -1
  26. package/package.json +11 -8
  27. package/src/attribute-spec/combinators/bool.ts +19 -0
  28. package/src/attribute-spec/combinators/diagnostic.ts +15 -0
  29. package/src/attribute-spec/combinators/entity-ref.ts +24 -0
  30. package/src/attribute-spec/combinators/field-ref.ts +36 -0
  31. package/src/attribute-spec/combinators/func-call.ts +65 -0
  32. package/src/attribute-spec/combinators/identifier.ts +16 -0
  33. package/src/attribute-spec/combinators/int.ts +35 -0
  34. package/src/attribute-spec/combinators/list.ts +43 -0
  35. package/src/attribute-spec/combinators/num.ts +26 -0
  36. package/src/attribute-spec/combinators/one-of.ts +29 -0
  37. package/src/attribute-spec/combinators/record.ts +43 -0
  38. package/src/attribute-spec/combinators/str.ts +19 -0
  39. package/src/attribute-spec/field-attribute.ts +27 -0
  40. package/src/attribute-spec/interpret.ts +177 -0
  41. package/src/attribute-spec/model-attribute.ts +27 -0
  42. package/src/attribute-spec/optional.ts +8 -0
  43. package/src/attribute-spec/types.ts +72 -0
  44. package/src/block-reconstruction.ts +140 -0
  45. package/src/exports/format.ts +3 -0
  46. package/src/exports/index.ts +61 -3
  47. package/src/exports/interpret.ts +2 -0
  48. package/src/exports/syntax.ts +25 -5
  49. package/src/extension-block.ts +107 -0
  50. package/src/format/emit.ts +603 -0
  51. package/src/format/error.ts +13 -0
  52. package/src/format/format.ts +13 -0
  53. package/src/format/options.ts +28 -0
  54. package/src/interpret.ts +67 -0
  55. package/src/parse.ts +32 -5
  56. package/src/resolve.ts +123 -0
  57. package/src/source-file.ts +25 -0
  58. package/src/symbol-table.ts +446 -0
  59. package/src/syntax/ast/attributes.ts +5 -6
  60. package/src/syntax/ast/declarations.ts +51 -26
  61. package/src/syntax/ast/expressions.ts +12 -13
  62. package/src/syntax/ast/identifier.ts +2 -3
  63. package/src/syntax/ast/qualified-name.ts +28 -19
  64. package/src/syntax/ast/type-annotation.ts +4 -5
  65. package/src/syntax/ast-helpers.ts +27 -3
  66. package/src/syntax/navigation.ts +55 -0
  67. package/src/syntax/red.ts +317 -42
  68. package/dist/parser-CaplKvRs.mjs +0 -1145
  69. package/dist/parser-CaplKvRs.mjs.map +0 -1
  70. package/dist/parser-Dfi3Wfdq.d.mts +0 -7
  71. package/dist/parser-Dfi3Wfdq.d.mts.map +0 -1
  72. package/dist/parser.d.mts +0 -2
  73. package/dist/parser.mjs +0 -2
  74. package/src/exports/parser.ts +0 -1
  75. package/src/parser.ts +0 -1642
@@ -0,0 +1,13 @@
1
+ import type { ParseDiagnostic } from '../parse';
2
+
3
+ export class PslFormatError extends Error {
4
+ readonly diagnostics: readonly ParseDiagnostic[];
5
+
6
+ constructor(diagnostics: readonly ParseDiagnostic[]) {
7
+ const summary = diagnostics[0]?.message ?? 'unknown parse error';
8
+ const more = diagnostics.length > 1 ? ` (and ${diagnostics.length - 1} more)` : '';
9
+ super(`Cannot format PSL with parse errors: ${summary}${more}`);
10
+ this.name = 'PslFormatError';
11
+ this.diagnostics = diagnostics;
12
+ }
13
+ }
@@ -0,0 +1,13 @@
1
+ import { parse } from '../parse';
2
+ import { emitDocument } from './emit';
3
+ import { PslFormatError } from './error';
4
+ import { type FormatOptions, resolveFormatOptions } from './options';
5
+
6
+ export function format(source: string, options?: FormatOptions): string {
7
+ const resolved = resolveFormatOptions(options);
8
+ const { document, diagnostics } = parse(source);
9
+ if (diagnostics.length > 0) {
10
+ throw new PslFormatError(diagnostics);
11
+ }
12
+ return emitDocument(document, resolved.indentUnit, resolved.newline);
13
+ }
@@ -0,0 +1,28 @@
1
+ export interface FormatOptions {
2
+ readonly indent?: number | 'tab';
3
+ readonly newline?: 'LF' | 'CRLF';
4
+ }
5
+
6
+ export interface ResolvedFormatOptions {
7
+ readonly indentUnit: string;
8
+ readonly newline: string;
9
+ }
10
+
11
+ export function resolveFormatOptions(options: FormatOptions | undefined): ResolvedFormatOptions {
12
+ const indent = options?.indent ?? 2;
13
+ if (indent !== 'tab' && (typeof indent !== 'number' || !Number.isInteger(indent) || indent < 1)) {
14
+ throw new TypeError(
15
+ `Invalid format options: indent must be a positive integer or 'tab', got ${String(indent)}`,
16
+ );
17
+ }
18
+ const newline = options?.newline ?? 'LF';
19
+ if (newline !== 'LF' && newline !== 'CRLF') {
20
+ throw new TypeError(
21
+ `Invalid format options: newline must be 'LF' or 'CRLF', got ${String(newline)}`,
22
+ );
23
+ }
24
+ return {
25
+ indentUnit: indent === 'tab' ? '\t' : ' '.repeat(indent),
26
+ newline: newline === 'CRLF' ? '\r\n' : '\n',
27
+ };
28
+ }
@@ -0,0 +1,67 @@
1
+ import type {
2
+ ContractSourceContext,
3
+ ContractSourceDiagnostic,
4
+ ContractSourceDiagnostics,
5
+ ContractSourceProvider,
6
+ PslContractSourceProvider,
7
+ } from '@prisma-next/config/config-types';
8
+ import type { Contract } from '@prisma-next/contract/types';
9
+ import { notOk, type Result } from '@prisma-next/utils/result';
10
+ import type { SourceFile } from './source-file';
11
+ import type { SymbolTable } from './symbol-table';
12
+ import type { DocumentAst } from './syntax/ast/declarations';
13
+
14
+ /**
15
+ * Lets editor tooling that already parses incrementally (e.g. the language
16
+ * server) hand cached artifacts to the interpreter instead of forcing a
17
+ * disk re-parse.
18
+ */
19
+ export interface PslInterpretInput {
20
+ readonly document: DocumentAst;
21
+ readonly sourceFile: SourceFile;
22
+ readonly symbolTable: SymbolTable;
23
+ readonly sourceId: string;
24
+ }
25
+
26
+ /**
27
+ * Declared here — the authoring layer that owns `DocumentAst` / `SourceFile` /
28
+ * `SymbolTable` — because `@prisma-next/config` (core) cannot name authoring
29
+ * types. `interpret` must not read disk or `context.resolvedInputs` — those
30
+ * are load-path concerns.
31
+ */
32
+ export interface PslInterpretCapable extends PslContractSourceProvider {
33
+ interpret(
34
+ input: PslInterpretInput,
35
+ context: ContractSourceContext,
36
+ ): Result<Contract, ContractSourceDiagnostics>;
37
+ }
38
+
39
+ /**
40
+ * Merges caller-side diagnostics (e.g. parse / symbol-table findings) into an
41
+ * interpretation result. The authored headline is deliberately uniform — it
42
+ * reports how many errors the schema has, never which pipeline stage found
43
+ * them.
44
+ */
45
+ export function withSeedDiagnostics(
46
+ result: Result<Contract, ContractSourceDiagnostics>,
47
+ seedDiagnostics: readonly ContractSourceDiagnostic[],
48
+ ): Result<Contract, ContractSourceDiagnostics> {
49
+ if (seedDiagnostics.length === 0) {
50
+ return result;
51
+ }
52
+ const diagnostics = result.ok
53
+ ? seedDiagnostics
54
+ : [...seedDiagnostics, ...result.failure.diagnostics];
55
+ return notOk({
56
+ summary: `Schema has ${diagnostics.length} error${diagnostics.length === 1 ? '' : 's'}`,
57
+ diagnostics,
58
+ ...(result.ok || result.failure.meta === undefined ? {} : { meta: result.failure.meta }),
59
+ });
60
+ }
61
+
62
+ /** The single seam that narrows a contract source to the interpret capability. */
63
+ export function hasPslInterpreter(source: ContractSourceProvider): source is PslInterpretCapable {
64
+ return (
65
+ source.sourceFormat === 'psl' && 'interpret' in source && typeof source.interpret === 'function'
66
+ );
67
+ }
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;
@@ -693,7 +711,16 @@ function invalidMember(cursor: Cursor, code: PslDiagnosticCode, message: string)
693
711
  export function parseField(cursor: Cursor): GreenNode | undefined {
694
712
  if (cursor.peekKind() !== 'Ident') return undefined;
695
713
  cursor.startNode('FieldDeclaration');
714
+ const nameMark = cursor.mark();
715
+ const nameText = cursor.peekToken().text;
696
716
  parseIdentifier(cursor);
717
+ if (cursor.peekKind() !== 'Ident') {
718
+ cursor.diagnostic(
719
+ 'PSL_INVALID_MODEL_MEMBER',
720
+ `Expected a type after field "${nameText}"`,
721
+ nameMark,
722
+ );
723
+ }
697
724
  parseTypeAnnotation(cursor);
698
725
  while (cursor.peekKind() === 'At') {
699
726
  parseAttribute(cursor);
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);