@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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as fs from 'node:fs'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import { tsImport } from '../ts-import.ts'
|
|
4
|
+
import { unwrapDefault } from '../utils.ts'
|
|
5
|
+
import type { ProjectConfig, ResolvedConfig, SqldocConfig } from './types.ts'
|
|
6
|
+
|
|
7
|
+
const CONFIG_FILENAMES = ['sqldoc.config.ts', 'sqldoc.config.js', 'sqldoc.config.mjs']
|
|
8
|
+
|
|
9
|
+
export interface ConfigResult {
|
|
10
|
+
config: SqldocConfig
|
|
11
|
+
configPath: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helper for creating typed sqldoc config.
|
|
16
|
+
* Used in sqldoc.config.ts: export default defineConfig({ ... })
|
|
17
|
+
* Accepts a single ProjectConfig or an array for multi-project.
|
|
18
|
+
*/
|
|
19
|
+
export function defineConfig(config: SqldocConfig): SqldocConfig {
|
|
20
|
+
return config
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a single project from the config.
|
|
25
|
+
*
|
|
26
|
+
* - If config is a single ProjectConfig, returns it directly
|
|
27
|
+
* (projectName must be undefined or match config.name)
|
|
28
|
+
* - If config is an array, requires projectName to select one
|
|
29
|
+
* - Throws with available project names on mismatch
|
|
30
|
+
*/
|
|
31
|
+
export function resolveProject(config: SqldocConfig, projectName?: string): ResolvedConfig {
|
|
32
|
+
// Single project config
|
|
33
|
+
if (!Array.isArray(config)) {
|
|
34
|
+
if (projectName && config.name && config.name !== projectName) {
|
|
35
|
+
throw new Error(`Project "${projectName}" not found. Available: ${config.name}`)
|
|
36
|
+
}
|
|
37
|
+
return config
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Multi-project config
|
|
41
|
+
const names = config.map((p) => p.name).filter(Boolean)
|
|
42
|
+
|
|
43
|
+
if (projectName) {
|
|
44
|
+
const found = config.find((p) => p.name === projectName)
|
|
45
|
+
if (!found) {
|
|
46
|
+
throw new Error(`Project "${projectName}" not found. Available: ${names.join(', ')}`)
|
|
47
|
+
}
|
|
48
|
+
return found
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// No --project specified
|
|
52
|
+
if (config.length === 1) {
|
|
53
|
+
return config[0]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(`Multiple projects configured. Use --project <name> or --all.\nAvailable: ${names.join(', ')}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve all projects from the config.
|
|
61
|
+
* Returns an array regardless of single/multi config.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveAllProjects(config: SqldocConfig): ProjectConfig[] {
|
|
64
|
+
return Array.isArray(config) ? config : [config]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load a config file by exact path using tsx.
|
|
69
|
+
*/
|
|
70
|
+
async function loadConfigFile(configPath: string): Promise<ConfigResult> {
|
|
71
|
+
const abs = path.resolve(configPath)
|
|
72
|
+
let mod = (await tsImport(abs)) as any
|
|
73
|
+
// Unwrap ESM default exports (CJS compat can double-wrap)
|
|
74
|
+
// Detect both single config (has namespaces/dialect/schema) and arrays
|
|
75
|
+
mod = unwrapDefault(mod, (m: any) => !!m.namespaces || !!m.dialect || !!m.schema || Array.isArray(m))
|
|
76
|
+
const config: SqldocConfig = mod ?? { dialect: 'postgres' }
|
|
77
|
+
return { config, configPath: abs }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load sqldoc config.
|
|
82
|
+
* If configFile is provided, loads that exact file.
|
|
83
|
+
* Otherwise searches projectRoot for sqldoc.config.{ts,js,mjs}.
|
|
84
|
+
* Returns default config if no config file found.
|
|
85
|
+
*/
|
|
86
|
+
export async function loadConfig(projectRoot: string, configFile?: string): Promise<ConfigResult> {
|
|
87
|
+
if (configFile) {
|
|
88
|
+
const resolved = path.resolve(configFile)
|
|
89
|
+
if (!fs.existsSync(resolved)) {
|
|
90
|
+
throw new Error(`Config file not found: ${resolved}`)
|
|
91
|
+
}
|
|
92
|
+
return loadConfigFile(resolved)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
96
|
+
const configPath = path.resolve(projectRoot, filename)
|
|
97
|
+
if (!fs.existsSync(configPath)) continue
|
|
98
|
+
return loadConfigFile(configPath)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { config: { dialect: 'postgres' } as ProjectConfig, configPath: null }
|
|
102
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type { CompileOptions } from './compile.ts'
|
|
2
|
+
export { compile } from './compile.ts'
|
|
3
|
+
export type { ConfigResult } from './config.ts'
|
|
4
|
+
export { defineConfig, loadConfig, resolveAllProjects, resolveProject } from './config.ts'
|
|
5
|
+
export type {
|
|
6
|
+
CodeOutput,
|
|
7
|
+
CompilerContext,
|
|
8
|
+
CompilerOutput,
|
|
9
|
+
DocsMeta,
|
|
10
|
+
LintConfig,
|
|
11
|
+
LintContext,
|
|
12
|
+
LintDiagnostic,
|
|
13
|
+
LintResult,
|
|
14
|
+
LintRule,
|
|
15
|
+
LintSeverity,
|
|
16
|
+
MigrationFormat,
|
|
17
|
+
MigrationNaming,
|
|
18
|
+
NamespaceConfig,
|
|
19
|
+
NamespacePlugin,
|
|
20
|
+
ProjectCompilerContext,
|
|
21
|
+
ProjectConfig,
|
|
22
|
+
ProjectContext,
|
|
23
|
+
ProjectOutput,
|
|
24
|
+
ResolvedConfig,
|
|
25
|
+
SqldocConfig,
|
|
26
|
+
SqlOutput,
|
|
27
|
+
TagContext,
|
|
28
|
+
TagOutput,
|
|
29
|
+
} from './types.ts'
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { SqlStatement } from '../ast/types.ts'
|
|
2
|
+
import type { SqlTarget, TagNamespace } from '../types.ts'
|
|
3
|
+
|
|
4
|
+
// ── Project config ──────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/** Migration file format — determines how up/down scripts are read and written */
|
|
7
|
+
export type MigrationFormat = 'atlas' | 'golang-migrate' | 'goose' | 'flyway' | 'dbmate' | 'plain'
|
|
8
|
+
|
|
9
|
+
/** File naming strategy for generated migration files */
|
|
10
|
+
export type MigrationNaming = 'timestamp' | 'sequential' | { provider: 'claude-code' }
|
|
11
|
+
|
|
12
|
+
/** A single project configuration */
|
|
13
|
+
export interface ProjectConfig {
|
|
14
|
+
/** Project name (required for multi-project, optional for single) */
|
|
15
|
+
name?: string
|
|
16
|
+
/** Schema source: file or directory of SQL files with sqldoc tags */
|
|
17
|
+
schema?: string
|
|
18
|
+
/** SQL dialect — mandatory */
|
|
19
|
+
dialect: 'postgres' | 'mysql' | 'sqlite'
|
|
20
|
+
/** Dev database URL. Default: pglite */
|
|
21
|
+
devUrl?: string
|
|
22
|
+
/** Migration settings */
|
|
23
|
+
migrations?: {
|
|
24
|
+
/** Directory containing migration files */
|
|
25
|
+
dir: string
|
|
26
|
+
/** Migration file format. Default: 'plain' */
|
|
27
|
+
format?: MigrationFormat
|
|
28
|
+
/** File naming strategy. Default: 'timestamp' */
|
|
29
|
+
naming?: MigrationNaming
|
|
30
|
+
}
|
|
31
|
+
/** Namespace plugin configuration */
|
|
32
|
+
namespaces?: Record<string, unknown>
|
|
33
|
+
/** Lint configuration */
|
|
34
|
+
lint?: LintConfig
|
|
35
|
+
|
|
36
|
+
// ── Legacy fields (backward compat) ──
|
|
37
|
+
/** @deprecated Use `schema` instead. Glob patterns for SQL source files */
|
|
38
|
+
include?: string[]
|
|
39
|
+
/** Output directory for generated SQL */
|
|
40
|
+
sqlOutDir?: string
|
|
41
|
+
/** Output directory for generated code artifacts */
|
|
42
|
+
codeOutDir?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Config can be a single project or array of projects */
|
|
46
|
+
export type SqldocConfig = ProjectConfig | ProjectConfig[]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolved single-project config — always a single ProjectConfig.
|
|
50
|
+
* Used internally after resolving --project/--all flags.
|
|
51
|
+
*/
|
|
52
|
+
export type ResolvedConfig = ProjectConfig
|
|
53
|
+
|
|
54
|
+
/** Generic constraint for per-namespace config */
|
|
55
|
+
export type NamespaceConfig = Record<string, unknown>
|
|
56
|
+
|
|
57
|
+
// ── Tag context ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/** Rich context passed to onTag for each tag occurrence */
|
|
60
|
+
export interface TagContext {
|
|
61
|
+
/** The SQL object this tag is attached to */
|
|
62
|
+
target: SqlTarget
|
|
63
|
+
/** Name of the SQL object (table name, function name, etc.) */
|
|
64
|
+
objectName: string
|
|
65
|
+
/** Column name when target is 'column' */
|
|
66
|
+
columnName?: string
|
|
67
|
+
/** Column data type when target is 'column' */
|
|
68
|
+
columnType?: string
|
|
69
|
+
/** The parsed tag that triggered this handler */
|
|
70
|
+
tag: {
|
|
71
|
+
name: string | null
|
|
72
|
+
args: Record<string, unknown> | unknown[]
|
|
73
|
+
}
|
|
74
|
+
/** All tags from the same namespace on the same SQL object */
|
|
75
|
+
namespaceTags: Array<{
|
|
76
|
+
tag: string | null
|
|
77
|
+
args: Record<string, unknown> | unknown[]
|
|
78
|
+
}>
|
|
79
|
+
/** All tags from ALL namespaces on the same SQL object (sibling introspection) */
|
|
80
|
+
siblingTags: Array<{
|
|
81
|
+
namespace: string
|
|
82
|
+
tag: string | null
|
|
83
|
+
args: Record<string, unknown> | unknown[]
|
|
84
|
+
}>
|
|
85
|
+
/** All tags across the entire file, grouped by SQL object */
|
|
86
|
+
fileTags: Array<{
|
|
87
|
+
objectName: string
|
|
88
|
+
target: SqlTarget
|
|
89
|
+
tags: Array<{
|
|
90
|
+
namespace: string
|
|
91
|
+
tag: string | null
|
|
92
|
+
args: Record<string, unknown> | unknown[]
|
|
93
|
+
}>
|
|
94
|
+
}>
|
|
95
|
+
/** The full AST statement for the SQL object */
|
|
96
|
+
astNode: unknown
|
|
97
|
+
/** All parsed SQL statements in the file */
|
|
98
|
+
fileStatements: SqlStatement[]
|
|
99
|
+
/** This namespace's config from sqldoc.config.ts */
|
|
100
|
+
config: NamespaceConfig
|
|
101
|
+
/** The source SQL file path */
|
|
102
|
+
filePath: string
|
|
103
|
+
/** Atlas-parsed table schema for this object (Tier 2 only, undefined in Tier 1) */
|
|
104
|
+
atlasTable?: unknown
|
|
105
|
+
/** Atlas-parsed column info (Tier 2 only, when target is 'column') */
|
|
106
|
+
atlasColumn?: unknown
|
|
107
|
+
/** Full Atlas realm with all schemas, tables, views (Tier 2 only) */
|
|
108
|
+
atlasRealm?: unknown
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @deprecated Use TagContext instead */
|
|
112
|
+
export type CompilerContext = TagContext
|
|
113
|
+
|
|
114
|
+
/** Context for afterCompile hook — receives ALL compiled file data */
|
|
115
|
+
export interface ProjectContext {
|
|
116
|
+
/** All compiled file outputs */
|
|
117
|
+
outputs: CompilerOutput[]
|
|
118
|
+
/** The merged SQL from all files combined */
|
|
119
|
+
mergedSql: string
|
|
120
|
+
/** All tags across all files, grouped by file then by object */
|
|
121
|
+
allFileTags: Array<{
|
|
122
|
+
sourceFile: string
|
|
123
|
+
objects: Array<{
|
|
124
|
+
objectName: string
|
|
125
|
+
target: SqlTarget
|
|
126
|
+
tags: Array<{
|
|
127
|
+
namespace: string
|
|
128
|
+
tag: string | null
|
|
129
|
+
args: Record<string, unknown> | unknown[]
|
|
130
|
+
}>
|
|
131
|
+
}>
|
|
132
|
+
}>
|
|
133
|
+
/** Aggregated docs metadata from all plugins across all files */
|
|
134
|
+
docsMeta: DocsMeta[]
|
|
135
|
+
/** This namespace's config from sqldoc.config.ts */
|
|
136
|
+
config: NamespaceConfig
|
|
137
|
+
/** Project root directory */
|
|
138
|
+
projectRoot: string
|
|
139
|
+
/** Atlas-parsed schema realm (Tier 2, when available) */
|
|
140
|
+
atlasRealm?: unknown
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @deprecated Use ProjectContext instead */
|
|
144
|
+
export type ProjectCompilerContext = ProjectContext
|
|
145
|
+
|
|
146
|
+
/** Output from an afterCompile hook */
|
|
147
|
+
export interface ProjectOutput {
|
|
148
|
+
/** Files to write (path relative to project root + content) */
|
|
149
|
+
files: Array<{
|
|
150
|
+
filePath: string
|
|
151
|
+
content: string
|
|
152
|
+
}>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Docs metadata ──────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/** Documentation metadata that plugins can return alongside SQL from onTag */
|
|
158
|
+
export interface DocsMeta {
|
|
159
|
+
/** Extra relationships to show in ER diagrams (e.g. audit trail arrows) */
|
|
160
|
+
relationships?: Array<{
|
|
161
|
+
from: string
|
|
162
|
+
to: string
|
|
163
|
+
label: string
|
|
164
|
+
style?: 'dashed'
|
|
165
|
+
}>
|
|
166
|
+
/** Table-level annotations (e.g. "RLS enabled", "Audited") */
|
|
167
|
+
annotations?: Array<{
|
|
168
|
+
object: string
|
|
169
|
+
text: string
|
|
170
|
+
}>
|
|
171
|
+
/**
|
|
172
|
+
* Extra doc columns contributed by plugins.
|
|
173
|
+
* If ANY plugin adds a column with a given header, that column appears on ALL tables.
|
|
174
|
+
* Each entry targets a specific table+column with a cell value.
|
|
175
|
+
*/
|
|
176
|
+
columns?: Array<{
|
|
177
|
+
/** Column header name (e.g. "Validation", "RLS", "Anonymization") */
|
|
178
|
+
header: string
|
|
179
|
+
/** Table this cell applies to */
|
|
180
|
+
object: string
|
|
181
|
+
/** DB column name this cell applies to (omit for table-level row) */
|
|
182
|
+
column?: string
|
|
183
|
+
/** Cell value text */
|
|
184
|
+
value: string
|
|
185
|
+
}>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Return type from onTag — SQL statements + optional docs metadata */
|
|
189
|
+
export interface TagOutput {
|
|
190
|
+
sql?: SqlOutput[]
|
|
191
|
+
docs?: DocsMeta
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Compiler outputs ────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/** A single SQL statement produced by a namespace */
|
|
197
|
+
export interface SqlOutput {
|
|
198
|
+
/** The SQL statement text (e.g., "ALTER TABLE users ENABLE ROW LEVEL SECURITY;") */
|
|
199
|
+
sql: string
|
|
200
|
+
/** Optional comment for clarity in output */
|
|
201
|
+
comment?: string
|
|
202
|
+
/** Source tag that produced this output (set by compiler, not by plugins) */
|
|
203
|
+
sourceTag?: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A non-SQL file produced by a namespace */
|
|
207
|
+
export interface CodeOutput {
|
|
208
|
+
/** Relative file path for the output (e.g., "types/users.ts") */
|
|
209
|
+
filePath: string
|
|
210
|
+
/** File content */
|
|
211
|
+
content: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Combined output from compiling one file */
|
|
215
|
+
export interface CompilerOutput {
|
|
216
|
+
/** Source SQL file path */
|
|
217
|
+
sourceFile: string
|
|
218
|
+
/** Complete SQL: original source with generated statements appended */
|
|
219
|
+
mergedSql: string
|
|
220
|
+
/** Individual SQL statements produced by namespaces (for inspection) */
|
|
221
|
+
sqlOutputs: SqlOutput[]
|
|
222
|
+
/** Code files to write */
|
|
223
|
+
codeOutputs: CodeOutput[]
|
|
224
|
+
/** Errors encountered during compilation */
|
|
225
|
+
errors: Array<{ namespace: string; message: string }>
|
|
226
|
+
/** Documentation metadata collected from all plugins */
|
|
227
|
+
docsMeta: DocsMeta[]
|
|
228
|
+
/** Per-file tag summary for project-level aggregation */
|
|
229
|
+
fileTags: Array<{
|
|
230
|
+
objectName: string
|
|
231
|
+
target: SqlTarget
|
|
232
|
+
tags: Array<{
|
|
233
|
+
namespace: string
|
|
234
|
+
tag: string | null
|
|
235
|
+
args: Record<string, unknown> | unknown[]
|
|
236
|
+
}>
|
|
237
|
+
}>
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Lint types ──────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
/** Severity levels for lint rules */
|
|
243
|
+
export type LintSeverity = 'error' | 'warn' | 'off'
|
|
244
|
+
|
|
245
|
+
/** A lint rule defined by a namespace plugin */
|
|
246
|
+
export interface LintRule {
|
|
247
|
+
/** Full rule name: namespace.ruleName (e.g. "audit.require-audit") */
|
|
248
|
+
name: string
|
|
249
|
+
/** Human-readable description of what this rule checks */
|
|
250
|
+
description: string
|
|
251
|
+
/** Default severity when not overridden in config */
|
|
252
|
+
default: LintSeverity
|
|
253
|
+
/** Check function — receives the full lint context and returns diagnostics */
|
|
254
|
+
check: (ctx: LintContext) => LintDiagnostic[]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Context passed to lint rule check functions */
|
|
258
|
+
export interface LintContext {
|
|
259
|
+
/** All compiled file outputs */
|
|
260
|
+
outputs: CompilerOutput[]
|
|
261
|
+
/** All loaded plugins, keyed by namespace name */
|
|
262
|
+
plugins: Map<string, NamespacePlugin>
|
|
263
|
+
/** Project config (resolved single project) */
|
|
264
|
+
config: ResolvedConfig
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A single lint diagnostic produced by a rule */
|
|
268
|
+
export interface LintDiagnostic {
|
|
269
|
+
/** The SQL object this diagnostic relates to (table name, etc.) */
|
|
270
|
+
objectName: string
|
|
271
|
+
/** Source file path */
|
|
272
|
+
sourceFile: string
|
|
273
|
+
/** Human-readable message */
|
|
274
|
+
message: string
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A lint result after applying severity and ignore processing */
|
|
278
|
+
export interface LintResult {
|
|
279
|
+
/** Rule that produced this result */
|
|
280
|
+
ruleName: string
|
|
281
|
+
/** Effective severity (after config override, 'skip' if @lint.ignore suppressed) */
|
|
282
|
+
severity: LintSeverity | 'skip'
|
|
283
|
+
/** The SQL object name */
|
|
284
|
+
objectName: string
|
|
285
|
+
/** Source file path */
|
|
286
|
+
sourceFile: string
|
|
287
|
+
/** Human-readable message */
|
|
288
|
+
message: string
|
|
289
|
+
/** Ignore reason (when severity is 'skip') */
|
|
290
|
+
ignoreReason?: string
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Lint config section in sqldoc.config.ts */
|
|
294
|
+
export interface LintConfig {
|
|
295
|
+
rules?: Record<string, LintSeverity>
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Namespace plugin contract ───────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
/** Stable type contract for namespace packages. Extends TagNamespace with compiler hooks. */
|
|
301
|
+
export interface NamespacePlugin extends TagNamespace {
|
|
302
|
+
/** API version for forward compatibility. Must be 1 for v1. */
|
|
303
|
+
apiVersion: 1
|
|
304
|
+
/** JSON Schema or Zod-like descriptor for namespace config (optional). Reserved for future validation. */
|
|
305
|
+
configSchema?: unknown
|
|
306
|
+
/** Called for each tag occurrence — returns SQL statements and/or docs metadata */
|
|
307
|
+
onTag?: (ctx: TagContext) => SqlOutput[] | TagOutput | undefined
|
|
308
|
+
/** Generate non-SQL code artifacts for a tag occurrence */
|
|
309
|
+
generateCode?: (ctx: TagContext) => CodeOutput[] | undefined
|
|
310
|
+
/** Runs once after all per-file compilation completes — project-level aggregation */
|
|
311
|
+
afterCompile?: (ctx: ProjectContext) => Promise<ProjectOutput> | ProjectOutput
|
|
312
|
+
/** Lint rules defined by this plugin */
|
|
313
|
+
lintRules?: LintRule[]
|
|
314
|
+
|
|
315
|
+
// Deprecated aliases — will be removed in v2
|
|
316
|
+
/** @deprecated Use onTag instead */
|
|
317
|
+
generateSQL?: (ctx: TagContext) => SqlOutput[] | TagOutput | undefined
|
|
318
|
+
/** @deprecated Use afterCompile instead */
|
|
319
|
+
generateProject?: (ctx: ProjectContext) => Promise<ProjectOutput> | ProjectOutput
|
|
320
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
|
|
3
|
+
export type { SqlAstAdapter, SqlColumn, SqlCommentOn, SqlStatement } from './ast/index.ts'
|
|
4
|
+
// AST Adapters
|
|
5
|
+
export { SqlparserTsAdapter } from './ast/index.ts'
|
|
6
|
+
// Blocks
|
|
7
|
+
export { buildBlocks } from './blocks.ts'
|
|
8
|
+
export type {
|
|
9
|
+
CodeOutput,
|
|
10
|
+
CompileOptions,
|
|
11
|
+
CompilerContext,
|
|
12
|
+
// CompilerContext is deprecated alias for TagContext
|
|
13
|
+
CompilerOutput,
|
|
14
|
+
ConfigResult,
|
|
15
|
+
DocsMeta,
|
|
16
|
+
LintConfig,
|
|
17
|
+
LintContext,
|
|
18
|
+
LintDiagnostic,
|
|
19
|
+
LintResult,
|
|
20
|
+
LintRule,
|
|
21
|
+
LintSeverity,
|
|
22
|
+
MigrationFormat,
|
|
23
|
+
MigrationNaming,
|
|
24
|
+
NamespaceConfig,
|
|
25
|
+
NamespacePlugin,
|
|
26
|
+
ProjectCompilerContext, // ProjectCompilerContext is deprecated alias
|
|
27
|
+
ProjectConfig,
|
|
28
|
+
ProjectContext,
|
|
29
|
+
ProjectOutput,
|
|
30
|
+
ResolvedConfig,
|
|
31
|
+
SqldocConfig,
|
|
32
|
+
SqlOutput,
|
|
33
|
+
TagContext,
|
|
34
|
+
TagOutput,
|
|
35
|
+
} from './compiler/index.ts'
|
|
36
|
+
// Compiler
|
|
37
|
+
export { compile, defineConfig, loadConfig, resolveAllProjects, resolveProject } from './compiler/index.ts'
|
|
38
|
+
// Lint engine
|
|
39
|
+
export { lint } from './lint.ts'
|
|
40
|
+
export type { ImportError, LoadResult } from './loader.ts'
|
|
41
|
+
// Loader
|
|
42
|
+
export { loadImports, setImportLogger } from './loader.ts'
|
|
43
|
+
export type { ArgValue, ImportStatement, ParsedArgs, ParsedTag, ParseResult } from './parser.ts'
|
|
44
|
+
// Parser
|
|
45
|
+
export { parse, parseArgs } from './parser.ts'
|
|
46
|
+
|
|
47
|
+
// TS import helper
|
|
48
|
+
export { tsImport } from './ts-import.ts'
|
|
49
|
+
export type {
|
|
50
|
+
ArgType,
|
|
51
|
+
ArrayType,
|
|
52
|
+
BooleanType,
|
|
53
|
+
EnumType,
|
|
54
|
+
InferArgType,
|
|
55
|
+
InferSchema,
|
|
56
|
+
NamedArgs,
|
|
57
|
+
NamespaceDef,
|
|
58
|
+
NoArgs,
|
|
59
|
+
NumberType,
|
|
60
|
+
PositionalArgs,
|
|
61
|
+
SqlTarget,
|
|
62
|
+
StringType,
|
|
63
|
+
TagArgs,
|
|
64
|
+
TagDef,
|
|
65
|
+
TagNamespace,
|
|
66
|
+
ValidationContext,
|
|
67
|
+
} from './types.ts'
|
|
68
|
+
// Shared utilities
|
|
69
|
+
export { findSqldocDir, unwrapDefault } from './utils.ts'
|
|
70
|
+
export type { AstInfo, Diagnostic } from './validator.ts'
|
|
71
|
+
// Validator
|
|
72
|
+
export { detectTarget, detectTargetFallback, validate } from './validator.ts'
|
package/src/lint.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint engine — collects lint rules from all loaded plugins, runs them
|
|
3
|
+
* against compiled output, respects @lint.ignore tags, and applies
|
|
4
|
+
* severity overrides from config.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CompilerOutput, LintContext, LintResult, NamespacePlugin, ResolvedConfig } from './compiler/types.ts'
|
|
8
|
+
|
|
9
|
+
/** Parsed @lint.ignore tag */
|
|
10
|
+
interface LintIgnore {
|
|
11
|
+
/** Rule name to ignore (e.g. "audit.require-audit") */
|
|
12
|
+
ruleName: string
|
|
13
|
+
/** Mandatory reason for suppression */
|
|
14
|
+
reason: string
|
|
15
|
+
/** SQL object name this ignore applies to */
|
|
16
|
+
objectName: string
|
|
17
|
+
/** Source file */
|
|
18
|
+
sourceFile: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Run all lint rules from loaded plugins against compiled outputs.
|
|
23
|
+
*
|
|
24
|
+
* Collects lintRules from every plugin, runs each rule's check function,
|
|
25
|
+
* applies severity overrides from config.lint.rules, and respects
|
|
26
|
+
* @lint.ignore tags found in file tags.
|
|
27
|
+
*/
|
|
28
|
+
export function lint(
|
|
29
|
+
outputs: CompilerOutput[],
|
|
30
|
+
plugins: Map<string, NamespacePlugin>,
|
|
31
|
+
config: ResolvedConfig,
|
|
32
|
+
): LintResult[] {
|
|
33
|
+
const results: LintResult[] = []
|
|
34
|
+
const lintConfig = config.lint ?? {}
|
|
35
|
+
const ruleOverrides = lintConfig.rules ?? {}
|
|
36
|
+
|
|
37
|
+
// 1. Collect all lint rules from plugins
|
|
38
|
+
const allRules = collectRules(plugins)
|
|
39
|
+
|
|
40
|
+
// 2. Collect all @lint.ignore tags from outputs
|
|
41
|
+
const ignores = collectIgnores(outputs)
|
|
42
|
+
|
|
43
|
+
// 3. Build lint context
|
|
44
|
+
const ctx: LintContext = { outputs, plugins, config }
|
|
45
|
+
|
|
46
|
+
// 4. Run each rule
|
|
47
|
+
for (const rule of allRules) {
|
|
48
|
+
// Determine effective severity
|
|
49
|
+
const effectiveSeverity = ruleOverrides[rule.name] ?? rule.default
|
|
50
|
+
if (effectiveSeverity === 'off') continue
|
|
51
|
+
|
|
52
|
+
// Run the check
|
|
53
|
+
const diagnostics = rule.check(ctx)
|
|
54
|
+
|
|
55
|
+
for (const diag of diagnostics) {
|
|
56
|
+
// Check if this diagnostic is suppressed by @lint.ignore
|
|
57
|
+
const ignore = ignores.find(
|
|
58
|
+
(ig) => ig.ruleName === rule.name && ig.objectName === diag.objectName && ig.sourceFile === diag.sourceFile,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (ignore) {
|
|
62
|
+
results.push({
|
|
63
|
+
ruleName: rule.name,
|
|
64
|
+
severity: 'skip',
|
|
65
|
+
objectName: diag.objectName,
|
|
66
|
+
sourceFile: diag.sourceFile,
|
|
67
|
+
message: diag.message,
|
|
68
|
+
ignoreReason: ignore.reason,
|
|
69
|
+
})
|
|
70
|
+
} else {
|
|
71
|
+
results.push({
|
|
72
|
+
ruleName: rule.name,
|
|
73
|
+
severity: effectiveSeverity,
|
|
74
|
+
objectName: diag.objectName,
|
|
75
|
+
sourceFile: diag.sourceFile,
|
|
76
|
+
message: diag.message,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return results
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Collect all lint rules from all plugins */
|
|
86
|
+
function collectRules(plugins: Map<string, NamespacePlugin>) {
|
|
87
|
+
const rules = []
|
|
88
|
+
for (const plugin of plugins.values()) {
|
|
89
|
+
if (plugin.lintRules) {
|
|
90
|
+
rules.push(...plugin.lintRules)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return rules
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Collect @lint.ignore tags from compiled outputs' fileTags */
|
|
97
|
+
function collectIgnores(outputs: CompilerOutput[]): LintIgnore[] {
|
|
98
|
+
const ignores: LintIgnore[] = []
|
|
99
|
+
|
|
100
|
+
for (const output of outputs) {
|
|
101
|
+
for (const obj of output.fileTags) {
|
|
102
|
+
for (const tag of obj.tags) {
|
|
103
|
+
if (tag.namespace === 'lint' && tag.tag === 'ignore') {
|
|
104
|
+
const args = tag.args
|
|
105
|
+
let ruleName: string | undefined
|
|
106
|
+
let reason: string | undefined
|
|
107
|
+
|
|
108
|
+
if (Array.isArray(args)) {
|
|
109
|
+
ruleName = args[0] as string | undefined
|
|
110
|
+
reason = args[1] as string | undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ruleName && reason) {
|
|
114
|
+
ignores.push({
|
|
115
|
+
ruleName,
|
|
116
|
+
reason,
|
|
117
|
+
objectName: obj.objectName,
|
|
118
|
+
sourceFile: output.sourceFile,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return ignores
|
|
127
|
+
}
|