@xdbml/parse 0.6.3 → 0.6.4

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.
@@ -107,6 +107,12 @@ export declare function isUrlKey(s: string): boolean;
107
107
  * union; throws ModuleSourceError for disallowed forms. Pure and sync.
108
108
  */
109
109
  export declare function classifyModuleSource(from: string): ModuleSource;
110
+ /**
111
+ * A local file path with Windows backslashes turned into forward slashes;
112
+ * a URL key is returned unchanged. The parser keys modules, and detects
113
+ * import cycles, on paths in this form.
114
+ */
115
+ export declare function toForwardSlashes(p: string): string;
110
116
  /**
111
117
  * Default recursion depth limit for module resolution. Enough for any
112
118
  * realistic module graph; small enough to bound stack usage on
@@ -731,8 +731,11 @@ export function classifyModuleSource(from) {
731
731
  * exactly as in v0.2.
732
732
  *
733
733
  * Uses pure string / WHATWG-URL manipulation (no node:path) so the same
734
- * code runs in Node and the browser. Forward slashes only; Windows paths
735
- * with backslashes should be normalized before reaching the parser.
734
+ * code runs in Node and the browser. A local path may use backslashes, as
735
+ * a native Windows path does (`C:\\repo\\main.xdbml`): they read as forward
736
+ * slashes, and the key handed to `readFile` uses forward slashes, which
737
+ * Node's fs accepts on Windows. Until v0.6.4 a backslash path lost its
738
+ * directory, so './lib' was read from the working directory instead.
736
739
  */
737
740
  function resolveModulePath(fromClause, importerPath) {
738
741
  const source = classifyModuleSource(fromClause);
@@ -749,21 +752,30 @@ function resolveModulePath(fromClause, importerPath) {
749
752
  // also what we want for the cycle-detection / de-duplication key.
750
753
  return new URL(rel, importerPath).href;
751
754
  }
752
- // Local relative source (unchanged v0.2 behavior).
753
- let withExt = source.from;
755
+ // Local relative source (v0.2 behavior, with Windows separators read as
756
+ // forward slashes from v0.6.4).
757
+ let withExt = toForwardSlashes(source.from);
754
758
  if (!withExt.endsWith('.xdbml')) {
755
759
  withExt = `${withExt}.xdbml`;
756
760
  }
757
- // Absolute path: return as-is.
758
- if (withExt.startsWith('/'))
761
+ // Absolute path, POSIX or with a Windows drive letter: return as-is.
762
+ if (withExt.startsWith('/') || /^[A-Za-z]:\//.test(withExt))
759
763
  return withExt;
760
764
  // No importer context: return as-is (resolver handles it).
761
765
  if (!importerPath)
762
766
  return withExt;
763
767
  // Resolve relative to importer's directory.
764
- const importerDir = posixDirname(importerPath);
768
+ const importerDir = posixDirname(toForwardSlashes(importerPath));
765
769
  return posixJoin(importerDir, withExt);
766
770
  }
771
+ /**
772
+ * A local file path with Windows backslashes turned into forward slashes;
773
+ * a URL key is returned unchanged. The parser keys modules, and detects
774
+ * import cycles, on paths in this form.
775
+ */
776
+ export function toForwardSlashes(p) {
777
+ return isUrlKey(p) ? p : p.replace(/\\/g, '/');
778
+ }
767
779
  function posixDirname(p) {
768
780
  const idx = p.lastIndexOf('/');
769
781
  if (idx < 0)
@@ -67,7 +67,7 @@ export interface SymbolEntry {
67
67
  * Stable diagnostic code. Tooling can match on these to filter or style
68
68
  * messages without parsing the human-readable text.
69
69
  */
70
- export type DiagnosticCode = 'duplicate-declaration' | 'name-collision' | 'container-target-missing' | 'container-target-not-in-project' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'duplicate-unique-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group' | 'source-query-in-settings' | 'duplicate-source-query' | 'duplicate-diagram-view' | 'duplicate-diagram-view-category' | 'unresolved-diagram-view-name' | 'ambiguous-diagram-view-name' | 'diagram-view-wrong-category' | 'unknown-diagram-view-setting';
70
+ export type DiagnosticCode = 'duplicate-declaration' | 'name-collision' | 'container-target-missing' | 'container-target-not-in-project' | 'unresolved-type' | 'possible-type-typo' | 'unresolved-key-field' | 'unresolved-index-field' | 'key-path-crosses-collection' | 'invalid-key-flags' | 'duplicate-primary-key' | 'duplicate-unique-key' | 'null-in-primary-key' | 'duplicate-constraints-block' | 'ref-target-not-key' | 'unresolved-entity' | 'unresolved-field' | 'unresolved-partial' | 'unresolved-tablegroup-member' | 'unresolved-records-entity' | 'unresolved-records-column' | 'empty-import' | 'invalid-nested-path' | 'foreign-master-composite' | 'foreign-master-duplicate-child' | 'foreign-master-without-ref' | 'construct-requires-version' | 'ambiguous-ref-endpoint' | 'invalid-constraint-type' | 'constraint-type-on-foreign-master' | 'invalid-undirected' | 'entity-level-many-to-many' | 'missing-supertype' | 'unresolved-supertype-group-member' | 'invalid-supertype-group-value' | 'duplicate-subtype' | 'supertype-is-subtype' | 'subtype-in-multiple-groups' | 'supertype-cycle' | 'supertype-attribute-redeclared' | 'discriminator-on-overlapping-group' | 'merge-without-roll-up' | 'empty-supertype-group' | 'source-query-in-settings' | 'duplicate-source-query' | 'duplicate-diagram-view' | 'duplicate-diagram-view-category' | 'unresolved-diagram-view-name' | 'ambiguous-diagram-view-name' | 'diagram-view-wrong-category' | 'unknown-diagram-view-setting' | 'invalid-tuple-positions' | 'named-type-shadows-builtin';
71
71
  /**
72
72
  * A single resolution diagnostic. Severity is currently always `error`,
73
73
  * but the field is included to leave room for future warnings (e.g.,
@@ -239,6 +239,28 @@ export function resolveNames(doc) {
239
239
  diagnostics.push(...checkViews(flat));
240
240
  // Pass 7: diagram views (spec §18.6).
241
241
  diagnostics.push(...checkDiagramViews(flat));
242
+ // Pass 8: Types under the name of a built-in type (spec §15.2).
243
+ for (const t of flat.statements) {
244
+ if (t.kind !== 'TypeDeclaration' || !BUILTIN_TYPES.has(t.name.toLowerCase()))
245
+ continue;
246
+ {
247
+ diagnostics.push({
248
+ severity: 'error',
249
+ code: 'named-type-shadows-builtin',
250
+ message: `Type '${t.name}' takes the name of a built-in type. Built-in types take precedence, so a field ` +
251
+ `typed ${t.name.toLowerCase()} keeps the built-in type: rename the Type (spec §15.2).`,
252
+ span: t.span,
253
+ });
254
+ }
255
+ }
256
+ // The parser checked neither rule before v0.6.4, so a document declaring
257
+ // a version earlier than 0.6 draws a warning and stays valid (§8.6, §15.2).
258
+ if (!versionAtLeast(flat, '0.6')) {
259
+ for (const d of diagnostics) {
260
+ if (d.code === 'invalid-tuple-positions' || d.code === 'named-type-shadows-builtin')
261
+ d.severity = 'warning';
262
+ }
263
+ }
242
264
  return { diagnostics, symbols };
243
265
  }
244
266
  /* -------------------------------------------------------------------------
@@ -569,11 +591,24 @@ function resolveTypeExpression(expr, symbols, diagnostics) {
569
591
  case 'SetType':
570
592
  resolveTypeExpression(expr.elementType, symbols, diagnostics);
571
593
  return;
572
- case 'TupleType':
594
+ case 'TupleType': {
595
+ // Spec §8.6: positions form a contiguous range from 0, each once.
596
+ const positions = expr.elements.map((e) => e.position);
597
+ const sorted = [...positions].sort((a, b) => a - b);
598
+ if (sorted.some((p, i) => p !== i)) {
599
+ diagnostics.push({
600
+ severity: 'error',
601
+ code: 'invalid-tuple-positions',
602
+ message: `Tuple positions ${positions.map((p) => `[${p}]`).join(', ')} do not run from [0] to ` +
603
+ `[${positions.length - 1}], each once: a tuple numbers its elements from 0 without a gap (spec §8.6).`,
604
+ span: expr.span,
605
+ });
606
+ }
573
607
  for (const elem of expr.elements) {
574
608
  resolveTypeExpression(elem.type, symbols, diagnostics);
575
609
  }
576
610
  return;
611
+ }
577
612
  case 'JsonType':
578
613
  // No nested type expressions.
579
614
  return;
package/dist/parser.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare class ParseError extends Error {
26
26
  * declaring a later version is refused (spec 4.1) rather than parsed with
27
27
  * semantics it does not have.
28
28
  */
29
- export declare const SUPPORTED_XDBML_VERSION = "0.6.3";
29
+ export declare const SUPPORTED_XDBML_VERSION = "0.6.4";
30
30
  /** Compare dotted version strings numerically: -1, 0 or 1. */
31
31
  export declare function compareVersions(a: string, b: string): number;
32
32
  export declare class Parser {
@@ -40,6 +40,8 @@ export declare class Parser {
40
40
  * 1-arg form).
41
41
  */
42
42
  private options;
43
+ /** The version the document declares, once read; undefined without a declaration. */
44
+ private declaredVersion;
43
45
  /**
44
46
  * The set of file paths currently being parsed in the resolution chain.
45
47
  * Used for cycle detection: when resolving a directive whose `from` path
package/dist/parser.js CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import { markPrimaryKeyNotNull } from "./constraints.js";
13
13
  import { TokenKind, tokenize, } from "./lexer.js";
14
- import { resolveImport, classifyModuleSource, ModuleSourceError } from "./module-resolver.js";
14
+ import { resolveImport, classifyModuleSource, ModuleSourceError, toForwardSlashes } from "./module-resolver.js";
15
15
  export class ParseError extends Error {
16
16
  position;
17
17
  /**
@@ -32,7 +32,7 @@ export class ParseError extends Error {
32
32
  * declaring a later version is refused (spec 4.1) rather than parsed with
33
33
  * semantics it does not have.
34
34
  */
35
- export const SUPPORTED_XDBML_VERSION = '0.6.3';
35
+ export const SUPPORTED_XDBML_VERSION = '0.6.4';
36
36
  /** Compare dotted version strings numerically: -1, 0 or 1. */
37
37
  export function compareVersions(a, b) {
38
38
  const pa = a.split('.').map((n) => Number(n) || 0);
@@ -134,6 +134,8 @@ export class Parser {
134
134
  * 1-arg form).
135
135
  */
136
136
  options;
137
+ /** The version the document declares, once read; undefined without a declaration. */
138
+ declaredVersion;
137
139
  /**
138
140
  * The set of file paths currently being parsed in the resolution chain.
139
141
  * Used for cycle detection: when resolving a directive whose `from` path
@@ -274,6 +276,7 @@ export class Parser {
274
276
  this.advance(); // .
275
277
  version += `.${this.advance().text}`;
276
278
  }
279
+ this.declaredVersion = version;
277
280
  if (compareVersions(version, SUPPORTED_XDBML_VERSION) > 0) {
278
281
  throw new ParseError(`This document declares 'xdbml: ${version}', which is newer than the ` +
279
282
  `latest version this parser supports (${SUPPORTED_XDBML_VERSION}). ` +
@@ -733,6 +736,12 @@ export class Parser {
733
736
  const start = this.peek().start;
734
737
  const modeTok = this.advance(); // 'use' or 'reuse'
735
738
  const mode = modeTok.text.toLowerCase();
739
+ // Appendix D, item 3: the module system belongs to the v0.2 feature set,
740
+ // so a document declaring xdbml: 0.1 cannot use it (checked from v0.6.4).
741
+ if (this.declaredVersion && compareVersions(this.declaredVersion, '0.2') < 0) {
742
+ throw new ParseError(`The module system (use and reuse) needs xdbml: 0.2 or later; this document declares ` +
743
+ `xdbml: ${this.declaredVersion} (spec §27, Appendix D).`, modeTok.start);
744
+ }
736
745
  // Import spec: '*' or '{ ... }'
737
746
  let spec;
738
747
  if (this.check(TokenKind.Star)) {
@@ -742,16 +751,15 @@ export class Parser {
742
751
  else if (this.check(TokenKind.LBrace)) {
743
752
  this.advance();
744
753
  const items = [];
745
- // Skip leading whitespace/newlines (already handled by lexer).
746
- while (!this.check(TokenKind.RBrace) && !this.check(TokenKind.EOF)) {
747
- items.push(this.parseImportItem(context));
748
- if (this.match(TokenKind.Comma)) {
749
- // Tolerate trailing comma before the closing brace.
750
- continue;
751
- }
752
- else {
754
+ // A list body (spec §3.9, §27.2, v0.6.4): items one per line, or
755
+ // separated by commas, with commas and semicolons optional anywhere
756
+ // between items. Until v0.6.4 the parser required a comma between
757
+ // two items, against the multi-line form §27.2 shows.
758
+ for (;;) {
759
+ this.skipListSeparators();
760
+ if (this.check(TokenKind.RBrace) || this.check(TokenKind.EOF))
753
761
  break;
754
- }
762
+ items.push(this.parseImportItem(context));
755
763
  }
756
764
  this.expect(TokenKind.RBrace, "Expected '}' closing import item list");
757
765
  if (items.length === 0) {
@@ -2524,7 +2532,7 @@ export function parse(source, options = {}) {
2524
2532
  // the outer level too). If no filePath is provided, the stack is empty.
2525
2533
  const initialStack = new Set();
2526
2534
  if (options.filePath)
2527
- initialStack.add(options.filePath);
2535
+ initialStack.add(toForwardSlashes(options.filePath));
2528
2536
  const doc = new Parser(tokens, options, initialStack, 0).parseDocument();
2529
2537
  applyEntityAliases(doc);
2530
2538
  return doc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdbml/parse",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Parser for xDBML (eXtended Database Markup Language), a strict superset of DBML 3.13.6: tokenizer, parser, module resolver, and name resolver.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hackolade",