@iyulab/m3l 0.1.0 → 0.1.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lexer.d.ts +1 -0
- package/dist/lexer.js +20 -6
- package/dist/parser.js +76 -14
- package/dist/resolver.d.ts +4 -0
- package/dist/resolver.js +23 -5
- package/dist/types.d.ts +16 -2
- package/dist/validator.js +17 -17
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { lex } from './lexer.js';
|
|
2
2
|
export { parseTokens, parseString as parseFileString } from './parser.js';
|
|
3
|
-
export { resolve } from './resolver.js';
|
|
3
|
+
export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
|
|
4
4
|
export { validate } from './validator.js';
|
|
5
5
|
export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
|
|
6
6
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { lex } from './lexer.js';
|
|
2
2
|
export { parseTokens, parseString as parseFileString } from './parser.js';
|
|
3
|
-
export { resolve } from './resolver.js';
|
|
3
|
+
export { resolve, AST_VERSION, PARSER_VERSION } from './resolver.js';
|
|
4
4
|
export { validate } from './validator.js';
|
|
5
5
|
export { readM3LFiles, readM3LString, readProjectConfig } from './reader.js';
|
|
6
6
|
import { readM3LFiles, readProjectConfig } from './reader.js';
|
package/dist/lexer.d.ts
CHANGED
package/dist/lexer.js
CHANGED
|
@@ -11,7 +11,7 @@ const RE_TYPE_INDICATOR = /^([\w][\w.]*(?:\([^)]*\))?)\s*::(\w+)(.*)$/;
|
|
|
11
11
|
const RE_MODEL_DEF = /^([\w][\w.]*(?:\([^)]*\))?)\s*(?::\s*(.+?))?(\s+@.+)?$/;
|
|
12
12
|
// Field line patterns
|
|
13
13
|
const RE_FIELD_NAME = /^([\w]+)(?:\(([^)]*)\))?\s*(?::\s*(.+))?$/;
|
|
14
|
-
const RE_TYPE_PART = /^([\w]+)(?:\(([^)]*)\))?(\?)?(\[\])?/;
|
|
14
|
+
const RE_TYPE_PART = /^([\w]+)(?:<([^>]+)>)?(?:\(([^)]*)\))?(\?)?(\[\])?(\?)?/;
|
|
15
15
|
const RE_FRAMEWORK_ATTR = /`\[([^\]]+)\]`/g;
|
|
16
16
|
const RE_INLINE_COMMENT = /\s+#\s+(.+)$/;
|
|
17
17
|
// Known kind-context H1 headers
|
|
@@ -264,7 +264,7 @@ function parseFieldLine(content) {
|
|
|
264
264
|
parseTypeAndAttrs(rest, data);
|
|
265
265
|
return data;
|
|
266
266
|
}
|
|
267
|
-
function parseTypeAndAttrs(rest, data) {
|
|
267
|
+
export function parseTypeAndAttrs(rest, data) {
|
|
268
268
|
let pos = 0;
|
|
269
269
|
const len = rest.length;
|
|
270
270
|
const skipWS = () => { while (pos < len && rest[pos] === ' ')
|
|
@@ -277,15 +277,29 @@ function parseTypeAndAttrs(rest, data) {
|
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
|
-
// Parse type: word(params)?[]
|
|
280
|
+
// Parse type: word<generics>?(params)?[]??
|
|
281
281
|
const typeMatch = rest.match(RE_TYPE_PART);
|
|
282
282
|
if (typeMatch) {
|
|
283
283
|
data.type_name = typeMatch[1];
|
|
284
|
+
// Group 2: generic params from <K,V>
|
|
284
285
|
if (typeMatch[2]) {
|
|
285
|
-
data.
|
|
286
|
+
data.type_generic_params = typeMatch[2].split(',').map(s => s.trim());
|
|
287
|
+
}
|
|
288
|
+
// Group 3: size/type params from (params)
|
|
289
|
+
if (typeMatch[3]) {
|
|
290
|
+
data.type_params = typeMatch[3].split(',').map(s => s.trim());
|
|
291
|
+
}
|
|
292
|
+
// Group 5: array
|
|
293
|
+
data.array = typeMatch[5] === '[]';
|
|
294
|
+
// Group 4: ? before [] = element nullable; Group 6: ? after [] = container nullable
|
|
295
|
+
if (data.array) {
|
|
296
|
+
data.nullable = typeMatch[6] === '?';
|
|
297
|
+
data.arrayItemNullable = typeMatch[4] === '?';
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
data.nullable = typeMatch[4] === '?' || typeMatch[6] === '?';
|
|
301
|
+
data.arrayItemNullable = false;
|
|
286
302
|
}
|
|
287
|
-
data.nullable = typeMatch[3] === '?';
|
|
288
|
-
data.array = typeMatch[4] === '[]';
|
|
289
303
|
pos = typeMatch[0].length;
|
|
290
304
|
skipWS();
|
|
291
305
|
}
|
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lex } from './lexer.js';
|
|
1
|
+
import { lex, parseTypeAndAttrs } from './lexer.js';
|
|
2
2
|
/**
|
|
3
3
|
* Parse M3L content string into a ParsedFile AST.
|
|
4
4
|
*/
|
|
@@ -87,6 +87,7 @@ function handleNamespace(token, state) {
|
|
|
87
87
|
function handleModelStart(token, state) {
|
|
88
88
|
finalizeElement(state);
|
|
89
89
|
const data = token.data;
|
|
90
|
+
const modelAttrs = parseAttributes(data.attributes);
|
|
90
91
|
const model = {
|
|
91
92
|
name: data.name,
|
|
92
93
|
label: data.label,
|
|
@@ -94,6 +95,7 @@ function handleModelStart(token, state) {
|
|
|
94
95
|
source: state.file,
|
|
95
96
|
line: token.line,
|
|
96
97
|
inherits: data.inherits || [],
|
|
98
|
+
attributes: modelAttrs,
|
|
97
99
|
fields: [],
|
|
98
100
|
sections: {
|
|
99
101
|
indexes: [],
|
|
@@ -137,6 +139,7 @@ function handleViewStart(token, state) {
|
|
|
137
139
|
source: state.file,
|
|
138
140
|
line: token.line,
|
|
139
141
|
inherits: [],
|
|
142
|
+
attributes: [],
|
|
140
143
|
materialized: data.materialized || false,
|
|
141
144
|
fields: [],
|
|
142
145
|
sections: {
|
|
@@ -248,15 +251,25 @@ function handleDirective(data, model, token, state) {
|
|
|
248
251
|
});
|
|
249
252
|
}
|
|
250
253
|
else {
|
|
251
|
-
// Generic directive
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
254
|
+
// Generic directive — normalize singular form
|
|
255
|
+
let sectionName = attr.name;
|
|
256
|
+
if (sectionName === 'behavior')
|
|
257
|
+
sectionName = 'behaviors';
|
|
258
|
+
if (sectionName === 'behaviors') {
|
|
259
|
+
model.sections.behaviors.push({
|
|
260
|
+
raw: data.raw_content,
|
|
261
|
+
args: attr.args,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (!model.sections[sectionName]) {
|
|
266
|
+
model.sections[sectionName] = [];
|
|
267
|
+
}
|
|
268
|
+
model.sections[sectionName].push({
|
|
269
|
+
raw: data.raw_content,
|
|
270
|
+
args: attr.args,
|
|
271
|
+
});
|
|
255
272
|
}
|
|
256
|
-
model.sections[sectionName].push({
|
|
257
|
-
raw: data.raw_content,
|
|
258
|
-
args: attr.args,
|
|
259
|
-
});
|
|
260
273
|
}
|
|
261
274
|
}
|
|
262
275
|
function handleSectionItem(data, model, token, state) {
|
|
@@ -332,10 +345,17 @@ function handleSectionItem(data, model, token, state) {
|
|
|
332
345
|
});
|
|
333
346
|
return;
|
|
334
347
|
}
|
|
335
|
-
// Generic section —
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
348
|
+
// Generic section — store as section items, NOT as fields
|
|
349
|
+
if (!model.sections[section]) {
|
|
350
|
+
model.sections[section] = [];
|
|
351
|
+
}
|
|
352
|
+
model.sections[section].push({
|
|
353
|
+
name: data.name,
|
|
354
|
+
raw: token.raw.trim(),
|
|
355
|
+
value: data.type_name || data.description || data.raw_value,
|
|
356
|
+
loc,
|
|
357
|
+
});
|
|
358
|
+
state.lastField = null;
|
|
339
359
|
}
|
|
340
360
|
function handleNestedItem(token, state) {
|
|
341
361
|
if (!state.currentElement)
|
|
@@ -402,6 +422,29 @@ function handleNestedItem(token, state) {
|
|
|
402
422
|
});
|
|
403
423
|
return;
|
|
404
424
|
}
|
|
425
|
+
// Sub-field for object/nested type
|
|
426
|
+
if (key && value) {
|
|
427
|
+
// Walk up to find the nearest object-type ancestor for this indent level
|
|
428
|
+
const parentField = field;
|
|
429
|
+
if (parentField.type === 'object') {
|
|
430
|
+
if (!parentField.fields)
|
|
431
|
+
parentField.fields = [];
|
|
432
|
+
// Re-parse value as type and attributes
|
|
433
|
+
const subData = { name: key };
|
|
434
|
+
parseTypeAndAttrs(value, subData);
|
|
435
|
+
// Only treat as sub-field if a type was extracted
|
|
436
|
+
if (subData.type_name) {
|
|
437
|
+
const subField = buildFieldNode(subData, token, state);
|
|
438
|
+
parentField.fields.push(subField);
|
|
439
|
+
// If the sub-field is also an object, set it as lastField for deeper nesting
|
|
440
|
+
if (subField.type === 'object') {
|
|
441
|
+
state.lastField = subField;
|
|
442
|
+
}
|
|
443
|
+
// Otherwise keep lastField pointing to the parent object for siblings
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
405
448
|
// Extended format field attributes
|
|
406
449
|
if (key) {
|
|
407
450
|
applyExtendedAttribute(field, key, value || '');
|
|
@@ -472,6 +515,7 @@ function buildFieldNode(data, token, state) {
|
|
|
472
515
|
const lookupAttr = attrs.find(a => a.name === 'lookup');
|
|
473
516
|
const rollupAttr = attrs.find(a => a.name === 'rollup');
|
|
474
517
|
const computedAttr = attrs.find(a => a.name === 'computed');
|
|
518
|
+
const computedRawAttr = attrs.find(a => a.name === 'computed_raw');
|
|
475
519
|
const fromAttr = attrs.find(a => a.name === 'from');
|
|
476
520
|
if (lookupAttr)
|
|
477
521
|
kind = 'lookup';
|
|
@@ -479,18 +523,22 @@ function buildFieldNode(data, token, state) {
|
|
|
479
523
|
kind = 'rollup';
|
|
480
524
|
else if (computedAttr)
|
|
481
525
|
kind = 'computed';
|
|
526
|
+
else if (computedRawAttr)
|
|
527
|
+
kind = 'computed';
|
|
482
528
|
const field = {
|
|
483
529
|
name: data.name,
|
|
484
530
|
label: data.label,
|
|
485
531
|
type: data.type_name,
|
|
486
532
|
params: parseTypeParams(data.type_params),
|
|
533
|
+
generic_params: data.type_generic_params,
|
|
487
534
|
nullable: data.nullable || false,
|
|
488
535
|
array: data.array || false,
|
|
536
|
+
arrayItemNullable: data.arrayItemNullable || false,
|
|
489
537
|
kind,
|
|
490
538
|
default_value: data.default_value,
|
|
491
539
|
description: data.description,
|
|
492
540
|
attributes: attrs,
|
|
493
|
-
framework_attrs: data.framework_attrs,
|
|
541
|
+
framework_attrs: parseCustomAttributes(data.framework_attrs),
|
|
494
542
|
loc: { file: state.file, line: token.line, col: 1 },
|
|
495
543
|
};
|
|
496
544
|
// Parse lookup
|
|
@@ -506,6 +554,11 @@ function buildFieldNode(data, token, state) {
|
|
|
506
554
|
const expr = computedAttr.args[0].replace(/^["']|["']$/g, '');
|
|
507
555
|
field.computed = { expression: expr };
|
|
508
556
|
}
|
|
557
|
+
// Parse computed_raw: @computed_raw("expression", ...)
|
|
558
|
+
if (computedRawAttr && computedRawAttr.args?.[0]) {
|
|
559
|
+
const expr = computedRawAttr.args[0].replace(/^["']|["']$/g, '');
|
|
560
|
+
field.computed = { expression: expr };
|
|
561
|
+
}
|
|
509
562
|
return field;
|
|
510
563
|
}
|
|
511
564
|
function parseAttributes(rawAttrs) {
|
|
@@ -699,6 +752,15 @@ function applyExtendedAttribute(field, key, value) {
|
|
|
699
752
|
break;
|
|
700
753
|
}
|
|
701
754
|
}
|
|
755
|
+
function parseCustomAttributes(rawAttrs) {
|
|
756
|
+
if (!rawAttrs || rawAttrs.length === 0)
|
|
757
|
+
return undefined;
|
|
758
|
+
return rawAttrs.map(raw => {
|
|
759
|
+
// raw is "[MaxLength(100)]" — strip brackets to get content
|
|
760
|
+
const content = raw.replace(/^\[|\]$/g, '');
|
|
761
|
+
return { content, raw };
|
|
762
|
+
});
|
|
763
|
+
}
|
|
702
764
|
function isEnumNode(el) {
|
|
703
765
|
return el.type === 'enum' && 'values' in el;
|
|
704
766
|
}
|
package/dist/resolver.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ParsedFile, M3LAST, ProjectInfo } from './types.js';
|
|
2
|
+
/** AST schema version — bump major on breaking structure changes */
|
|
3
|
+
export declare const AST_VERSION = "1.0";
|
|
4
|
+
/** Parser package version — kept in sync with package.json */
|
|
5
|
+
export declare const PARSER_VERSION = "0.1.1";
|
|
2
6
|
/**
|
|
3
7
|
* Resolve and merge multiple parsed file ASTs into a single M3LAST.
|
|
4
8
|
* Handles: inheritance resolution, duplicate detection, reference validation.
|
package/dist/resolver.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** AST schema version — bump major on breaking structure changes */
|
|
2
|
+
export const AST_VERSION = '1.0';
|
|
3
|
+
/** Parser package version — kept in sync with package.json */
|
|
4
|
+
export const PARSER_VERSION = '0.1.1';
|
|
1
5
|
/**
|
|
2
6
|
* Resolve and merge multiple parsed file ASTs into a single M3LAST.
|
|
3
7
|
* Handles: inheritance resolution, duplicate detection, reference validation.
|
|
@@ -58,6 +62,8 @@ export function resolve(files, project) {
|
|
|
58
62
|
projectInfo.name = ns;
|
|
59
63
|
}
|
|
60
64
|
return {
|
|
65
|
+
parserVersion: PARSER_VERSION,
|
|
66
|
+
astVersion: AST_VERSION,
|
|
61
67
|
project: projectInfo,
|
|
62
68
|
sources,
|
|
63
69
|
models: allModels,
|
|
@@ -72,7 +78,7 @@ function checkDuplicate(name, kind, item, map, allMap, errors) {
|
|
|
72
78
|
const existing = allMap.get(name);
|
|
73
79
|
if (existing) {
|
|
74
80
|
errors.push({
|
|
75
|
-
code: 'E005',
|
|
81
|
+
code: 'M3L-E005',
|
|
76
82
|
severity: 'error',
|
|
77
83
|
file: item.source,
|
|
78
84
|
line: item.line,
|
|
@@ -95,7 +101,7 @@ function resolveInheritance(model, modelMap, interfaceMap, allNamedMap, errors)
|
|
|
95
101
|
if (!parent) {
|
|
96
102
|
if (!allNamedMap.has(name)) {
|
|
97
103
|
errors.push({
|
|
98
|
-
code: 'E007',
|
|
104
|
+
code: 'M3L-E007',
|
|
99
105
|
severity: 'error',
|
|
100
106
|
file: fromModel.source,
|
|
101
107
|
line: fromModel.line,
|
|
@@ -122,9 +128,21 @@ function resolveInheritance(model, modelMap, interfaceMap, allNamedMap, errors)
|
|
|
122
128
|
for (const parentName of model.inherits) {
|
|
123
129
|
collectFields(parentName, model);
|
|
124
130
|
}
|
|
131
|
+
// Handle @override: child fields with @override replace inherited fields
|
|
132
|
+
const overrideNames = new Set();
|
|
133
|
+
for (const ownField of model.fields) {
|
|
134
|
+
const overrideIdx = ownField.attributes.findIndex(a => a.name === 'override');
|
|
135
|
+
if (overrideIdx >= 0) {
|
|
136
|
+
overrideNames.add(ownField.name);
|
|
137
|
+
// Remove @override attribute from the child field
|
|
138
|
+
ownField.attributes.splice(overrideIdx, 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Remove inherited fields that are overridden
|
|
142
|
+
const filteredInherited = inheritedFields.filter(f => !overrideNames.has(f.name));
|
|
125
143
|
// Prepend inherited fields before model's own fields
|
|
126
|
-
if (
|
|
127
|
-
model.fields = [...
|
|
144
|
+
if (filteredInherited.length > 0) {
|
|
145
|
+
model.fields = [...filteredInherited, ...model.fields];
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
function checkDuplicateFields(model, errors) {
|
|
@@ -133,7 +151,7 @@ function checkDuplicateFields(model, errors) {
|
|
|
133
151
|
const existing = seen.get(field.name);
|
|
134
152
|
if (existing) {
|
|
135
153
|
errors.push({
|
|
136
|
-
code: 'E005',
|
|
154
|
+
code: 'M3L-E005',
|
|
137
155
|
severity: 'error',
|
|
138
156
|
file: field.loc.file,
|
|
139
157
|
line: field.loc.line,
|
package/dist/types.d.ts
CHANGED
|
@@ -15,7 +15,14 @@ export interface Token {
|
|
|
15
15
|
export type FieldKind = 'stored' | 'computed' | 'lookup' | 'rollup';
|
|
16
16
|
export interface FieldAttribute {
|
|
17
17
|
name: string;
|
|
18
|
-
args?:
|
|
18
|
+
args?: (string | number | boolean)[];
|
|
19
|
+
}
|
|
20
|
+
/** Structured representation of a backtick-wrapped framework attribute like `[MaxLength(100)]` */
|
|
21
|
+
export interface CustomAttribute {
|
|
22
|
+
/** Content inside brackets, e.g., "MaxLength(100)" for `[MaxLength(100)]` */
|
|
23
|
+
content: string;
|
|
24
|
+
/** Original text including brackets, e.g., "[MaxLength(100)]" */
|
|
25
|
+
raw: string;
|
|
19
26
|
}
|
|
20
27
|
export interface EnumValue {
|
|
21
28
|
name: string;
|
|
@@ -28,13 +35,15 @@ export interface FieldNode {
|
|
|
28
35
|
label?: string;
|
|
29
36
|
type?: string;
|
|
30
37
|
params?: (string | number)[];
|
|
38
|
+
generic_params?: string[];
|
|
31
39
|
nullable: boolean;
|
|
32
40
|
array: boolean;
|
|
41
|
+
arrayItemNullable: boolean;
|
|
33
42
|
kind: FieldKind;
|
|
34
43
|
default_value?: string;
|
|
35
44
|
description?: string;
|
|
36
45
|
attributes: FieldAttribute[];
|
|
37
|
-
framework_attrs?:
|
|
46
|
+
framework_attrs?: CustomAttribute[];
|
|
38
47
|
lookup?: {
|
|
39
48
|
path: string;
|
|
40
49
|
};
|
|
@@ -60,6 +69,7 @@ export interface ModelNode {
|
|
|
60
69
|
line: number;
|
|
61
70
|
inherits: string[];
|
|
62
71
|
description?: string;
|
|
72
|
+
attributes: FieldAttribute[];
|
|
63
73
|
fields: FieldNode[];
|
|
64
74
|
sections: {
|
|
65
75
|
indexes: unknown[];
|
|
@@ -117,6 +127,10 @@ export interface ParsedFile {
|
|
|
117
127
|
views: ModelNode[];
|
|
118
128
|
}
|
|
119
129
|
export interface M3LAST {
|
|
130
|
+
/** Parser package version (semver) */
|
|
131
|
+
parserVersion: string;
|
|
132
|
+
/** AST schema version — incremented on breaking AST structure changes */
|
|
133
|
+
astVersion: string;
|
|
120
134
|
project: ProjectInfo;
|
|
121
135
|
sources: string[];
|
|
122
136
|
models: ModelNode[];
|
package/dist/validator.js
CHANGED
|
@@ -9,7 +9,7 @@ export function validate(ast, options = {}) {
|
|
|
9
9
|
for (const m of allModels) {
|
|
10
10
|
modelMap.set(m.name, m);
|
|
11
11
|
}
|
|
12
|
-
// E001: @rollup FK missing @reference
|
|
12
|
+
// M3L-E001: @rollup FK missing @reference
|
|
13
13
|
for (const model of allModels) {
|
|
14
14
|
for (const field of model.fields) {
|
|
15
15
|
if (field.kind === 'rollup' && field.rollup) {
|
|
@@ -17,7 +17,7 @@ export function validate(ast, options = {}) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
// E002: @lookup path FK missing @reference
|
|
20
|
+
// M3L-E002: @lookup path FK missing @reference
|
|
21
21
|
for (const model of allModels) {
|
|
22
22
|
for (const field of model.fields) {
|
|
23
23
|
if (field.kind === 'lookup' && field.lookup) {
|
|
@@ -25,12 +25,12 @@ export function validate(ast, options = {}) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
// E004: View @from references model not found
|
|
28
|
+
// M3L-E004: View @from references model not found
|
|
29
29
|
for (const view of ast.views) {
|
|
30
30
|
if (view.source_def?.from) {
|
|
31
31
|
if (!modelMap.has(view.source_def.from)) {
|
|
32
32
|
errors.push({
|
|
33
|
-
code: 'E004',
|
|
33
|
+
code: 'M3L-E004',
|
|
34
34
|
severity: 'error',
|
|
35
35
|
file: view.source,
|
|
36
36
|
line: view.line,
|
|
@@ -40,13 +40,13 @@ export function validate(ast, options = {}) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
//
|
|
43
|
+
// M3L-E006: Duplicate field names (already checked in resolver, but re-check for safety)
|
|
44
44
|
for (const model of allModels) {
|
|
45
45
|
const seen = new Set();
|
|
46
46
|
for (const field of model.fields) {
|
|
47
47
|
if (seen.has(field.name)) {
|
|
48
48
|
errors.push({
|
|
49
|
-
code: '
|
|
49
|
+
code: 'M3L-E006',
|
|
50
50
|
severity: 'error',
|
|
51
51
|
file: field.loc.file,
|
|
52
52
|
line: field.loc.line,
|
|
@@ -61,16 +61,16 @@ export function validate(ast, options = {}) {
|
|
|
61
61
|
if (options.strict) {
|
|
62
62
|
for (const model of allModels) {
|
|
63
63
|
for (const field of model.fields) {
|
|
64
|
-
// W001: Field line length > 80 chars
|
|
64
|
+
// M3L-W001: Field line length > 80 chars
|
|
65
65
|
// We check the source loc raw length — approximate using field attributes count
|
|
66
66
|
checkFieldLineLength(field, model, warnings);
|
|
67
|
-
// W003: Framework attrs without backtick (already processed in lexer, skip)
|
|
68
|
-
// W004: Lookup chain > 3 hops
|
|
67
|
+
// M3L-W003: Framework attrs without backtick (already processed in lexer, skip)
|
|
68
|
+
// M3L-W004: Lookup chain > 3 hops
|
|
69
69
|
if (field.kind === 'lookup' && field.lookup) {
|
|
70
70
|
const hops = field.lookup.path.split('.').length;
|
|
71
71
|
if (hops > 3) {
|
|
72
72
|
warnings.push({
|
|
73
|
-
code: 'W004',
|
|
73
|
+
code: 'M3L-W004',
|
|
74
74
|
severity: 'warning',
|
|
75
75
|
file: field.loc.file,
|
|
76
76
|
line: field.loc.line,
|
|
@@ -80,10 +80,10 @@ export function validate(ast, options = {}) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
// W002: Object nesting > 3 levels
|
|
83
|
+
// M3L-W002: Object nesting > 3 levels
|
|
84
84
|
checkNestingDepth(model.fields, 1, model, warnings);
|
|
85
85
|
}
|
|
86
|
-
// W006: Inline enum missing values: key
|
|
86
|
+
// M3L-W006: Inline enum missing values: key
|
|
87
87
|
for (const model of allModels) {
|
|
88
88
|
for (const field of model.fields) {
|
|
89
89
|
if (field.type === 'enum' && field.enum_values && field.enum_values.length > 0) {
|
|
@@ -102,7 +102,7 @@ function validateRollupReference(field, model, modelMap, errors) {
|
|
|
102
102
|
const rollup = field.rollup;
|
|
103
103
|
const targetModel = modelMap.get(rollup.target);
|
|
104
104
|
if (!targetModel) {
|
|
105
|
-
// Target model doesn't exist — this is E007 (already caught in resolver)
|
|
105
|
+
// Target model doesn't exist — this is M3L-E007 (already caught in resolver)
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
// Check that the FK field in the target model has @reference or @fk
|
|
@@ -114,7 +114,7 @@ function validateRollupReference(field, model, modelMap, errors) {
|
|
|
114
114
|
const hasReference = fkField.attributes.some(a => a.name === 'reference' || a.name === 'fk');
|
|
115
115
|
if (!hasReference) {
|
|
116
116
|
errors.push({
|
|
117
|
-
code: 'E001',
|
|
117
|
+
code: 'M3L-E001',
|
|
118
118
|
severity: 'error',
|
|
119
119
|
file: field.loc.file,
|
|
120
120
|
line: field.loc.line,
|
|
@@ -136,7 +136,7 @@ function validateLookupReference(field, model, modelMap, errors) {
|
|
|
136
136
|
const hasReference = fkField.attributes.some(a => a.name === 'reference' || a.name === 'fk');
|
|
137
137
|
if (!hasReference) {
|
|
138
138
|
errors.push({
|
|
139
|
-
code: 'E002',
|
|
139
|
+
code: 'M3L-E002',
|
|
140
140
|
severity: 'error',
|
|
141
141
|
file: field.loc.file,
|
|
142
142
|
line: field.loc.line,
|
|
@@ -168,7 +168,7 @@ function checkFieldLineLength(field, model, warnings) {
|
|
|
168
168
|
len += 3 + field.description.length;
|
|
169
169
|
if (len > 80) {
|
|
170
170
|
warnings.push({
|
|
171
|
-
code: 'W001',
|
|
171
|
+
code: 'M3L-W001',
|
|
172
172
|
severity: 'warning',
|
|
173
173
|
file: field.loc.file,
|
|
174
174
|
line: field.loc.line,
|
|
@@ -182,7 +182,7 @@ function checkNestingDepth(fields, depth, model, warnings) {
|
|
|
182
182
|
if (field.fields && field.fields.length > 0) {
|
|
183
183
|
if (depth >= 3) {
|
|
184
184
|
warnings.push({
|
|
185
|
-
code: 'W002',
|
|
185
|
+
code: 'M3L-W002',
|
|
186
186
|
severity: 'warning',
|
|
187
187
|
file: field.loc.file,
|
|
188
188
|
line: field.loc.line,
|