@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.
- package/package.json +33 -0
- package/src/__tests__/ast/sqlparser-ts.test.ts +117 -0
- package/src/__tests__/blocks.test.ts +80 -0
- package/src/__tests__/compile.test.ts +407 -0
- package/src/__tests__/compiler/compile.test.ts +363 -0
- package/src/__tests__/lint-rules.test.ts +249 -0
- package/src/__tests__/lint.test.ts +270 -0
- package/src/__tests__/parser.test.ts +169 -0
- package/src/__tests__/tags.sql +15 -0
- package/src/__tests__/validator.test.ts +210 -0
- package/src/ast/adapter.ts +10 -0
- package/src/ast/index.ts +3 -0
- package/src/ast/sqlparser-ts.ts +218 -0
- package/src/ast/types.ts +28 -0
- package/src/blocks.ts +242 -0
- package/src/compiler/compile.ts +783 -0
- package/src/compiler/config.ts +102 -0
- package/src/compiler/index.ts +29 -0
- package/src/compiler/types.ts +320 -0
- package/src/index.ts +72 -0
- package/src/lint.ts +127 -0
- package/src/loader.ts +102 -0
- package/src/parser.ts +202 -0
- package/src/ts-import.ts +70 -0
- package/src/types.ts +111 -0
- package/src/utils.ts +31 -0
- package/src/validator.ts +324 -0
package/src/validator.ts
ADDED
|
@@ -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
|
+
}
|