@sqldoc/core 0.0.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.
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Validates parsed tags against loaded namespace definitions.
3
+ * Uses externally-provided SQL statements for AST info (adapter-based).
4
+ */
5
+
6
+ import type { SqlStatement } from './ast/types.ts'
7
+ import type { AstInfo } from './blocks.ts'
8
+ import { buildBlocks, detectTarget, detectTargetFallback } from './blocks.ts'
9
+ import type { ParsedTag } from './parser.ts'
10
+ import { parseArgs } from './parser.ts'
11
+ import type { ArgType, TagDef, TagNamespace, ValidationContext } from './types.ts'
12
+
13
+ // ── Diagnostics ─────────────────────────────────────────────────────
14
+
15
+ export interface Diagnostic {
16
+ message: string
17
+ line: number
18
+ startCol: number
19
+ endCol: number
20
+ severity: 'error' | 'warning' | 'info'
21
+ }
22
+
23
+ // Re-export for consumers
24
+ export type { AstInfo }
25
+ export { detectTarget, detectTargetFallback }
26
+
27
+ // ── Main validate ───────────────────────────────────────────────────
28
+
29
+ export function validate(
30
+ tags: ParsedTag[],
31
+ namespaces: Map<string, TagNamespace>,
32
+ docText: string,
33
+ stmts?: SqlStatement[],
34
+ ): Diagnostic[] {
35
+ const diagnostics: Diagnostic[] = []
36
+ const docLines = docText.split('\n')
37
+ const blocks = buildBlocks(tags, docText, docLines, stmts ?? [])
38
+
39
+ for (const block of blocks) {
40
+ for (const tag of block.tags) {
41
+ const ns = namespaces.get(tag.namespace)
42
+ if (!ns) {
43
+ diagnostics.push({
44
+ message: `Unknown namespace '@${tag.namespace}'`,
45
+ line: tag.line,
46
+ startCol: tag.namespaceStart - 1,
47
+ endCol: tag.namespaceEnd,
48
+ severity: 'error',
49
+ })
50
+ continue
51
+ }
52
+
53
+ let tagDef: TagDef | undefined
54
+ if (tag.tag === null) {
55
+ tagDef = ns.tags.$self
56
+ if (!tagDef) {
57
+ diagnostics.push({
58
+ message: `Namespace '${tag.namespace}' cannot be used as a standalone tag (no $self defined)`,
59
+ line: tag.line,
60
+ startCol: tag.namespaceStart - 1,
61
+ endCol: tag.namespaceEnd,
62
+ severity: 'error',
63
+ })
64
+ continue
65
+ }
66
+ } else {
67
+ tagDef = ns.tags[tag.tag]
68
+ if (!tagDef) {
69
+ diagnostics.push({
70
+ message: `Unknown tag '${tag.tag}' in namespace '${tag.namespace}'`,
71
+ line: tag.line,
72
+ startCol: tag.tagStart,
73
+ endCol: tag.tagEnd,
74
+ severity: 'error',
75
+ })
76
+ continue
77
+ }
78
+ }
79
+
80
+ // Validate target
81
+ if (tagDef.targets && tagDef.targets.length > 0 && block.ast.target !== 'unknown') {
82
+ if (!tagDef.targets.includes(block.ast.target)) {
83
+ const label = tag.tag ? `@${tag.namespace}.${tag.tag}` : `@${tag.namespace}`
84
+ diagnostics.push({
85
+ message: `${label} cannot be used on a ${block.ast.target} (allowed: ${tagDef.targets.join(', ')})`,
86
+ line: tag.line,
87
+ startCol: tag.startCol,
88
+ endCol: tag.endCol,
89
+ severity: 'error',
90
+ })
91
+ continue
92
+ }
93
+ }
94
+
95
+ // Validate arguments
96
+ diagnostics.push(...validateArgs(tag, tagDef))
97
+
98
+ // Run custom validation function
99
+ if (tagDef.validate) {
100
+ try {
101
+ const parsedArgs = tag.rawArgs !== null ? parseArgs(tag.rawArgs) : null
102
+ const fileTags = blocks.map((b) => ({
103
+ objectName: b.ast.objectName ?? 'unknown',
104
+ target: b.ast.target,
105
+ tags: b.tags.map((t) => ({ namespace: t.namespace, tag: t.tag, rawArgs: t.rawArgs })),
106
+ }))
107
+ const ctx: ValidationContext = {
108
+ target: block.ast.target,
109
+ lines: block.sqlLines,
110
+ siblingTags: block.tags
111
+ .filter((t) => t !== tag)
112
+ .map((t) => ({ namespace: t.namespace, tag: t.tag, rawArgs: t.rawArgs })),
113
+ fileTags,
114
+ argValues: parsedArgs ? (parsedArgs.type === 'named' ? parsedArgs.values : parsedArgs.values) : {},
115
+ columnName: block.ast.columnName,
116
+ columnType: block.ast.columnType,
117
+ objectName: block.ast.objectName,
118
+ astNode: block.ast.astNode,
119
+ }
120
+ const result = tagDef.validate(ctx)
121
+ if (result) {
122
+ const message = typeof result === 'string' ? result : result.message
123
+ const severity = typeof result === 'string' ? 'error' : (result.severity ?? 'error')
124
+ diagnostics.push({
125
+ message,
126
+ line: tag.line,
127
+ startCol: tag.startCol,
128
+ endCol: tag.endCol,
129
+ severity,
130
+ })
131
+ }
132
+ } catch (err: any) {
133
+ diagnostics.push({
134
+ message: `Validation error: ${err?.message ?? err}`,
135
+ line: tag.line,
136
+ startCol: tag.startCol,
137
+ endCol: tag.endCol,
138
+ severity: 'error',
139
+ })
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ return diagnostics
146
+ }
147
+
148
+ // ── Arg validation ──────────────────────────────────────────────────
149
+
150
+ function validateArgs(tag: ParsedTag, def: TagDef): Diagnostic[] {
151
+ const diagnostics: Diagnostic[] = []
152
+ const hasArgsDef = def.args !== undefined
153
+
154
+ if (tag.rawArgs === null) {
155
+ if (hasArgsDef && !Array.isArray(def.args)) {
156
+ const namedDef = def.args as Record<string, ArgType & { required?: boolean }>
157
+ const required = Object.entries(namedDef).filter(([, v]) => (v as any).required)
158
+ if (required.length > 0) {
159
+ diagnostics.push({
160
+ message: `Missing required argument(s): ${required.map(([k]) => k).join(', ')}`,
161
+ line: tag.line,
162
+ startCol: tag.startCol,
163
+ endCol: tag.endCol,
164
+ severity: 'error',
165
+ })
166
+ }
167
+ }
168
+ return diagnostics
169
+ }
170
+
171
+ if (!hasArgsDef) {
172
+ diagnostics.push({
173
+ message: `Tag '${tag.tag ?? tag.namespace}' does not accept arguments`,
174
+ line: tag.line,
175
+ startCol: tag.argsStart - 1,
176
+ endCol: tag.argsEnd + 1,
177
+ severity: 'error',
178
+ })
179
+ return diagnostics
180
+ }
181
+
182
+ const parsed = parseArgs(tag.rawArgs)
183
+
184
+ if (Array.isArray(def.args)) {
185
+ validatePositionalArgs(tag, def as { args: ArgType[] }, parsed, diagnostics)
186
+ } else {
187
+ validateNamedArgs(tag, def as { args: Record<string, ArgType & { required?: boolean }> }, parsed, diagnostics)
188
+ }
189
+
190
+ return diagnostics
191
+ }
192
+
193
+ function validatePositionalArgs(
194
+ tag: ParsedTag,
195
+ def: { args: ArgType[] },
196
+ parsed: ReturnType<typeof parseArgs>,
197
+ diagnostics: Diagnostic[],
198
+ ) {
199
+ if (parsed.type === 'named') {
200
+ diagnostics.push({
201
+ message: `Tag '${tag.tag ?? tag.namespace}' expects positional arguments, not named`,
202
+ line: tag.line,
203
+ startCol: tag.argsStart,
204
+ endCol: tag.argsEnd,
205
+ severity: 'error',
206
+ })
207
+ return
208
+ }
209
+
210
+ if (parsed.values.length > def.args.length) {
211
+ diagnostics.push({
212
+ message: `Too many arguments: expected at most ${def.args.length}, got ${parsed.values.length}`,
213
+ line: tag.line,
214
+ startCol: tag.argsStart,
215
+ endCol: tag.argsEnd,
216
+ severity: 'error',
217
+ })
218
+ return
219
+ }
220
+
221
+ for (let i = 0; i < parsed.values.length; i++) {
222
+ const typeError = checkType(parsed.values[i], def.args[i])
223
+ if (typeError) {
224
+ diagnostics.push({
225
+ message: typeError,
226
+ line: tag.line,
227
+ startCol: tag.argsStart,
228
+ endCol: tag.argsEnd,
229
+ severity: 'error',
230
+ })
231
+ }
232
+ }
233
+ }
234
+
235
+ function validateNamedArgs(
236
+ tag: ParsedTag,
237
+ def: { args: Record<string, ArgType & { required?: boolean }> },
238
+ parsed: ReturnType<typeof parseArgs>,
239
+ diagnostics: Diagnostic[],
240
+ ) {
241
+ if (parsed.type === 'positional' && parsed.values.length > 0) {
242
+ diagnostics.push({
243
+ message: `Tag '${tag.tag ?? tag.namespace}' expects named arguments (key: value)`,
244
+ line: tag.line,
245
+ startCol: tag.argsStart,
246
+ endCol: tag.argsEnd,
247
+ severity: 'error',
248
+ })
249
+ return
250
+ }
251
+
252
+ const values = parsed.type === 'named' ? parsed.values : {}
253
+
254
+ // If args definition is empty, accept any named args (dynamic keys)
255
+ const acceptsAnyKeys = Object.keys(def.args).length === 0
256
+
257
+ if (!acceptsAnyKeys) {
258
+ for (const key of Object.keys(values)) {
259
+ if (!(key in def.args)) {
260
+ diagnostics.push({
261
+ message: `Unknown argument '${key}'`,
262
+ line: tag.line,
263
+ startCol: tag.argsStart,
264
+ endCol: tag.argsEnd,
265
+ severity: 'error',
266
+ })
267
+ }
268
+ }
269
+ }
270
+
271
+ for (const [key, argDef] of Object.entries(def.args)) {
272
+ const value = values[key]
273
+ if (value === undefined) {
274
+ if ((argDef as any).required) {
275
+ diagnostics.push({
276
+ message: `Missing required argument '${key}'`,
277
+ line: tag.line,
278
+ startCol: tag.argsStart,
279
+ endCol: tag.argsEnd,
280
+ severity: 'error',
281
+ })
282
+ }
283
+ continue
284
+ }
285
+ const typeError = checkType(value, argDef)
286
+ if (typeError) {
287
+ diagnostics.push({
288
+ message: `Argument '${key}': ${typeError}`,
289
+ line: tag.line,
290
+ startCol: tag.argsStart,
291
+ endCol: tag.argsEnd,
292
+ severity: 'error',
293
+ })
294
+ }
295
+ }
296
+ }
297
+
298
+ function checkType(value: unknown, argType: ArgType): string | undefined {
299
+ switch (argType.type) {
300
+ case 'string':
301
+ if (typeof value !== 'string') return `expected string, got ${typeof value}`
302
+ return undefined
303
+ case 'number':
304
+ if (typeof value !== 'number') return `expected number, got ${typeof value}`
305
+ return undefined
306
+ case 'boolean':
307
+ if (typeof value !== 'boolean') return `expected boolean, got ${typeof value}`
308
+ return undefined
309
+ case 'enum':
310
+ if (!argType.values.includes(String(value))) {
311
+ return `expected one of [${argType.values.join(', ')}], got '${value}'`
312
+ }
313
+ return undefined
314
+ case 'array':
315
+ if (!Array.isArray(value)) return `expected array, got ${typeof value}`
316
+ for (let i = 0; i < value.length; i++) {
317
+ const err = checkType(value[i], argType.items)
318
+ if (err) return `element ${i}: ${err}`
319
+ }
320
+ return undefined
321
+ default:
322
+ return undefined
323
+ }
324
+ }