@zmdb/compiler 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +40 -0
- package/dist/codegen/index.d.ts +50 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +364 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/scan.d.ts +105 -0
- package/dist/codegen/scan.d.ts.map +1 -0
- package/dist/codegen/scan.js +450 -0
- package/dist/codegen/scan.js.map +1 -0
- package/dist/codegen/witness.d.ts +76 -0
- package/dist/codegen/witness.d.ts.map +1 -0
- package/dist/codegen/witness.js +690 -0
- package/dist/codegen/witness.js.map +1 -0
- package/dist/config/contract.d.ts +41 -0
- package/dist/config/contract.d.ts.map +1 -0
- package/dist/config/contract.js +5 -0
- package/dist/config/contract.js.map +1 -0
- package/dist/config/index.d.ts +53 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +277 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.zmdb.generated.d.ts +12 -0
- package/dist/config/index.zmdb.generated.js +28 -0
- package/dist/config/index.zmdb.witness.d.ts +3 -0
- package/dist/config/index.zmdb.witness.d.ts.map +1 -0
- package/dist/config/index.zmdb.witness.js +20 -0
- package/dist/config/index.zmdb.witness.js.map +1 -0
- package/dist/configured-plugin.d.ts +11 -0
- package/dist/configured-plugin.d.ts.map +1 -0
- package/dist/configured-plugin.js +27 -0
- package/dist/configured-plugin.js.map +1 -0
- package/dist/emit/index.d.ts +103 -0
- package/dist/emit/index.d.ts.map +1 -0
- package/dist/emit/index.js +1356 -0
- package/dist/emit/index.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +2 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/lint/ast.d.ts +17 -0
- package/dist/lint/ast.d.ts.map +1 -0
- package/dist/lint/ast.js +77 -0
- package/dist/lint/ast.js.map +1 -0
- package/dist/lint/host-types.d.ts +9 -0
- package/dist/lint/host-types.d.ts.map +1 -0
- package/dist/lint/host-types.js +2 -0
- package/dist/lint/host-types.js.map +1 -0
- package/dist/lint/index.d.ts +23 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +53 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts +3 -0
- package/dist/lint/rules/no-distributed-nullable-tags.d.ts.map +1 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js +71 -0
- package/dist/lint/rules/no-distributed-nullable-tags.js.map +1 -0
- package/dist/lint/rules/no-empty-patch.d.ts +3 -0
- package/dist/lint/rules/no-empty-patch.d.ts.map +1 -0
- package/dist/lint/rules/no-empty-patch.js +26 -0
- package/dist/lint/rules/no-empty-patch.js.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts +3 -0
- package/dist/lint/rules/no-interpolated-sql.d.ts.map +1 -0
- package/dist/lint/rules/no-interpolated-sql.js +35 -0
- package/dist/lint/rules/no-interpolated-sql.js.map +1 -0
- package/dist/lint/rules/no-unbounded-find.d.ts +3 -0
- package/dist/lint/rules/no-unbounded-find.d.ts.map +1 -0
- package/dist/lint/rules/no-unbounded-find.js +26 -0
- package/dist/lint/rules/no-unbounded-find.js.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts +3 -0
- package/dist/lint/rules/no-unknown-json-column.d.ts.map +1 -0
- package/dist/lint/rules/no-unknown-json-column.js +35 -0
- package/dist/lint/rules/no-unknown-json-column.js.map +1 -0
- package/dist/lint/rules/require-sql-on-number.d.ts +3 -0
- package/dist/lint/rules/require-sql-on-number.d.ts.map +1 -0
- package/dist/lint/rules/require-sql-on-number.js +34 -0
- package/dist/lint/rules/require-sql-on-number.js.map +1 -0
- package/dist/lint/types.d.ts +12 -0
- package/dist/lint/types.d.ts.map +1 -0
- package/dist/lint/types.js +2 -0
- package/dist/lint/types.js.map +1 -0
- package/dist/metro/metro.d.ts +22 -0
- package/dist/metro/metro.d.ts.map +1 -0
- package/dist/metro/metro.js +242 -0
- package/dist/metro/metro.js.map +1 -0
- package/dist/protobuf/decode.d.ts +20 -0
- package/dist/protobuf/decode.d.ts.map +1 -0
- package/dist/protobuf/decode.js +289 -0
- package/dist/protobuf/decode.js.map +1 -0
- package/dist/protobuf/descriptor.d.ts +16 -0
- package/dist/protobuf/descriptor.d.ts.map +1 -0
- package/dist/protobuf/descriptor.js +303 -0
- package/dist/protobuf/descriptor.js.map +1 -0
- package/dist/protobuf/encode.d.ts +20 -0
- package/dist/protobuf/encode.d.ts.map +1 -0
- package/dist/protobuf/encode.js +203 -0
- package/dist/protobuf/encode.js.map +1 -0
- package/dist/protobuf/grpc-ir.d.ts +16 -0
- package/dist/protobuf/grpc-ir.d.ts.map +1 -0
- package/dist/protobuf/grpc-ir.js +2 -0
- package/dist/protobuf/grpc-ir.js.map +1 -0
- package/dist/protobuf/plan.d.ts +42 -0
- package/dist/protobuf/plan.d.ts.map +1 -0
- package/dist/protobuf/plan.js +158 -0
- package/dist/protobuf/plan.js.map +1 -0
- package/dist/reflect/callsites.d.ts +40 -0
- package/dist/reflect/callsites.d.ts.map +1 -0
- package/dist/reflect/callsites.js +153 -0
- package/dist/reflect/callsites.js.map +1 -0
- package/dist/reflect/index.d.ts +106 -0
- package/dist/reflect/index.d.ts.map +1 -0
- package/dist/reflect/index.js +1421 -0
- package/dist/reflect/index.js.map +1 -0
- package/dist/reflect/session.d.ts +118 -0
- package/dist/reflect/session.d.ts.map +1 -0
- package/dist/reflect/session.js +228 -0
- package/dist/reflect/session.js.map +1 -0
- package/dist/testing/index.d.ts +69 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +210 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/transform/index.d.ts +74 -0
- package/dist/transform/index.d.ts.map +1 -0
- package/dist/transform/index.js +572 -0
- package/dist/transform/index.js.map +1 -0
- package/dist/unplugin/index.d.ts +53 -0
- package/dist/unplugin/index.d.ts.map +1 -0
- package/dist/unplugin/index.js +130 -0
- package/dist/unplugin/index.js.map +1 -0
- package/dist/unplugin/inline-bench.d.ts +29 -0
- package/dist/unplugin/inline-bench.d.ts.map +1 -0
- package/dist/unplugin/inline-bench.js +82 -0
- package/dist/unplugin/inline-bench.js.map +1 -0
- package/package.json +110 -0
- package/src/codegen/index.ts +451 -0
- package/src/codegen/scan.ts +574 -0
- package/src/codegen/witness.ts +819 -0
- package/src/config/contract.ts +48 -0
- package/src/config/index.ts +384 -0
- package/src/config/index.zmdb.generated.d.ts +12 -0
- package/src/config/index.zmdb.generated.js +28 -0
- package/src/config/index.zmdb.witness.ts +22 -0
- package/src/configured-plugin.ts +35 -0
- package/src/emit/index.ts +1496 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +336 -0
- package/src/lint/ast.ts +96 -0
- package/src/lint/host-types.ts +9 -0
- package/src/lint/index.ts +80 -0
- package/src/lint/rules/no-distributed-nullable-tags.ts +76 -0
- package/src/lint/rules/no-empty-patch.ts +26 -0
- package/src/lint/rules/no-interpolated-sql.ts +45 -0
- package/src/lint/rules/no-unbounded-find.ts +26 -0
- package/src/lint/rules/no-unknown-json-column.ts +36 -0
- package/src/lint/rules/require-sql-on-number.ts +34 -0
- package/src/lint/types.ts +11 -0
- package/src/metro/metro.ts +313 -0
- package/src/protobuf/decode.ts +375 -0
- package/src/protobuf/descriptor.ts +353 -0
- package/src/protobuf/encode.ts +277 -0
- package/src/protobuf/grpc-ir.ts +17 -0
- package/src/protobuf/plan.ts +244 -0
- package/src/reflect/callsites.ts +198 -0
- package/src/reflect/index.ts +1723 -0
- package/src/reflect/session.ts +276 -0
- package/src/testing/index.ts +298 -0
- package/src/transform/index.ts +690 -0
- package/src/unplugin/index.ts +182 -0
- package/src/unplugin/inline-bench.ts +92 -0
package/src/errors.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
symlinkSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { type NamingStrategy } from '@zmdb/schema/naming';
|
|
16
|
+
|
|
17
|
+
import { codegen } from './codegen/index.js';
|
|
18
|
+
import { artifactPaths, isGeneratedPath } from './codegen/witness.js';
|
|
19
|
+
import type { CompilerDiagnostic } from './errors.js';
|
|
20
|
+
import { openPreparedSession, type ReflectSession } from './reflect/session.js';
|
|
21
|
+
|
|
22
|
+
export { watchCodegen, type CodegenOptions, type CodegenResult, type WatchOptions } from './codegen/index.js';
|
|
23
|
+
|
|
24
|
+
export type { CompilerDiagnostic } from './errors.js';
|
|
25
|
+
export { zmdbAot, type ConfiguredZmdbAotOptions } from './configured-plugin.js';
|
|
26
|
+
|
|
27
|
+
export interface CompileProjectOptions {
|
|
28
|
+
readonly project: string;
|
|
29
|
+
readonly files?: readonly string[];
|
|
30
|
+
readonly naming?: NamingStrategy;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CompiledArtifact {
|
|
34
|
+
/** Rewritten application source. Its path is the matching member of `CompileResult.files`. */
|
|
35
|
+
readonly source: string;
|
|
36
|
+
readonly witnessPath: string;
|
|
37
|
+
readonly runtimePath: string;
|
|
38
|
+
readonly declarationPath: string;
|
|
39
|
+
readonly witness: string;
|
|
40
|
+
readonly runtime: string;
|
|
41
|
+
readonly declaration: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CompileResult {
|
|
45
|
+
readonly project: string;
|
|
46
|
+
readonly files: readonly string[];
|
|
47
|
+
readonly artifacts: readonly CompiledArtifact[];
|
|
48
|
+
readonly diagnostics: readonly CompilerDiagnostic[];
|
|
49
|
+
readonly dependencies: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WriteCompileResultOptions {
|
|
53
|
+
readonly check?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WriteCompileResult {
|
|
57
|
+
readonly written: readonly string[];
|
|
58
|
+
readonly deleted: readonly string[];
|
|
59
|
+
readonly stale: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface SelectedProject {
|
|
63
|
+
readonly files: readonly string[];
|
|
64
|
+
readonly shadowFiles: readonly string[];
|
|
65
|
+
readonly dependencies: readonly string[];
|
|
66
|
+
readonly diagnostics: readonly CompilerDiagnostic[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compile one project without changing it.
|
|
71
|
+
*
|
|
72
|
+
* The existing codegen pipeline runs against a disposable sibling shadow. Keeping the
|
|
73
|
+
* shadow at the same directory depth preserves relative `extends` and `paths` entries,
|
|
74
|
+
* while the real project remains byte-for-byte untouched until `writeCompileResult`.
|
|
75
|
+
*/
|
|
76
|
+
export async function compileProject(options: CompileProjectOptions): Promise<CompileResult> {
|
|
77
|
+
const project = resolve(options.project);
|
|
78
|
+
const root = dirname(project);
|
|
79
|
+
const shadow = mkdtempSync(join(dirname(root), `.zmdb-compile-${basename(root)}-`));
|
|
80
|
+
try {
|
|
81
|
+
copyProject(root, shadow);
|
|
82
|
+
const shadowProject = join(shadow, relative(root, project));
|
|
83
|
+
let session: ReflectSession;
|
|
84
|
+
try {
|
|
85
|
+
session = openPreparedSession({ project: shadowProject }, config =>
|
|
86
|
+
writeGeneratedProject(shadowProject, config.fileNames),
|
|
87
|
+
);
|
|
88
|
+
} catch (error: unknown) {
|
|
89
|
+
return {
|
|
90
|
+
project,
|
|
91
|
+
files: [],
|
|
92
|
+
artifacts: [],
|
|
93
|
+
diagnostics: [
|
|
94
|
+
{
|
|
95
|
+
code: 'ZMDB_PROJECT',
|
|
96
|
+
message: error instanceof Error ? error.message : String(error),
|
|
97
|
+
file: project,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
dependencies: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const selected = selectProjectFiles(session, project, shadowProject, options.files);
|
|
106
|
+
if (selected.diagnostics.length > 0) {
|
|
107
|
+
return {
|
|
108
|
+
project,
|
|
109
|
+
files: selected.files,
|
|
110
|
+
artifacts: [],
|
|
111
|
+
diagnostics: selected.diagnostics,
|
|
112
|
+
dependencies: selected.dependencies,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const generated = codegen({
|
|
117
|
+
project: session.project,
|
|
118
|
+
files: selected.shadowFiles,
|
|
119
|
+
session,
|
|
120
|
+
...(options.naming === undefined ? {} : { naming: options.naming }),
|
|
121
|
+
});
|
|
122
|
+
const diagnostics = generated.problems.map((message): CompilerDiagnostic => ({ code: 'ZMDB_COMPILE', message }));
|
|
123
|
+
const artifacts = selected.files.flatMap((file, index): readonly CompiledArtifact[] => {
|
|
124
|
+
const shadowFile = selected.shadowFiles[index];
|
|
125
|
+
if (shadowFile === undefined) return [];
|
|
126
|
+
const shadowPaths = artifactPaths(shadowFile);
|
|
127
|
+
if (!existsSync(shadowPaths.witness) || !existsSync(shadowPaths.js) || !existsSync(shadowPaths.dts)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
const paths = artifactPaths(file);
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
source: readFileSync(shadowFile, 'utf8'),
|
|
134
|
+
witnessPath: paths.witness,
|
|
135
|
+
runtimePath: paths.js,
|
|
136
|
+
declarationPath: paths.dts,
|
|
137
|
+
witness: readFileSync(shadowPaths.witness, 'utf8'),
|
|
138
|
+
runtime: readFileSync(shadowPaths.js, 'utf8'),
|
|
139
|
+
declaration: readFileSync(shadowPaths.dts, 'utf8'),
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
project,
|
|
146
|
+
files: selected.files,
|
|
147
|
+
artifacts,
|
|
148
|
+
diagnostics,
|
|
149
|
+
dependencies: selected.dependencies,
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
session.close();
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
rmSync(shadow, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeGeneratedProject(project: string, files: readonly string[]): string {
|
|
160
|
+
const root = dirname(project);
|
|
161
|
+
const prepared = join(root, '.zmdb-compile.tsconfig.json');
|
|
162
|
+
const extended = relative(root, project).replaceAll(sep, '/');
|
|
163
|
+
const roots = files.map(file => relative(root, file).replaceAll(sep, '/')).toSorted();
|
|
164
|
+
writeFileSync(
|
|
165
|
+
prepared,
|
|
166
|
+
`${JSON.stringify(
|
|
167
|
+
{
|
|
168
|
+
extends: extended.startsWith('.') ? extended : `./${extended}`,
|
|
169
|
+
files: roots,
|
|
170
|
+
include: ['**/*.zmdb.witness.ts'],
|
|
171
|
+
exclude: [],
|
|
172
|
+
},
|
|
173
|
+
undefined,
|
|
174
|
+
2,
|
|
175
|
+
)}\n`,
|
|
176
|
+
);
|
|
177
|
+
return prepared;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Apply or check one previously compiled result. No compilation occurs here. */
|
|
181
|
+
export async function writeCompileResult(
|
|
182
|
+
result: CompileResult,
|
|
183
|
+
options: WriteCompileResultOptions = {},
|
|
184
|
+
): Promise<WriteCompileResult> {
|
|
185
|
+
const expected = new Map<string, string>();
|
|
186
|
+
const artifactByWitness = new Map(result.artifacts.map(artifact => [pathKey(artifact.witnessPath), artifact]));
|
|
187
|
+
|
|
188
|
+
for (const file of result.files) {
|
|
189
|
+
const paths = artifactPaths(file);
|
|
190
|
+
const artifact = artifactByWitness.get(pathKey(paths.witness));
|
|
191
|
+
if (artifact === undefined) continue;
|
|
192
|
+
expected.set(file, artifact.source);
|
|
193
|
+
expected.set(paths.witness, artifact.witness);
|
|
194
|
+
expected.set(paths.js, artifact.runtime);
|
|
195
|
+
expected.set(paths.dts, artifact.declaration);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const stale: string[] = [];
|
|
199
|
+
const deletions: string[] = [];
|
|
200
|
+
for (const file of result.files) {
|
|
201
|
+
const paths = artifactPaths(file);
|
|
202
|
+
if (artifactByWitness.has(pathKey(paths.witness))) continue;
|
|
203
|
+
for (const path of [paths.witness, paths.js, paths.dts]) {
|
|
204
|
+
if (existsSync(path)) {
|
|
205
|
+
deletions.push(path);
|
|
206
|
+
stale.push(path);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const [path, source] of expected) {
|
|
212
|
+
if (readText(path) !== source) stale.push(path);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const orderedStale = [...new Set(stale)].toSorted();
|
|
216
|
+
const orderedDeleted = [...new Set(deletions)].toSorted();
|
|
217
|
+
if (options.check === true) {
|
|
218
|
+
return { written: [], deleted: orderedDeleted, stale: orderedStale };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const path of orderedDeleted) unlinkSync(path);
|
|
222
|
+
const written: string[] = [];
|
|
223
|
+
let sequence = 0;
|
|
224
|
+
for (const path of orderedStale) {
|
|
225
|
+
const source = expected.get(path);
|
|
226
|
+
if (source === undefined) continue;
|
|
227
|
+
writeAtomic(path, source, sequence++);
|
|
228
|
+
written.push(path);
|
|
229
|
+
}
|
|
230
|
+
return { written, deleted: orderedDeleted, stale: orderedStale };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function selectProjectFiles(
|
|
234
|
+
session: ReflectSession,
|
|
235
|
+
project: string,
|
|
236
|
+
shadowProject: string,
|
|
237
|
+
requested: readonly string[] | undefined,
|
|
238
|
+
): SelectedProject {
|
|
239
|
+
const root = dirname(project);
|
|
240
|
+
const shadowRoot = dirname(shadowProject);
|
|
241
|
+
const members = new Map(
|
|
242
|
+
session
|
|
243
|
+
.sourceFileNames()
|
|
244
|
+
.filter(file => isProjectSource(file, shadowRoot))
|
|
245
|
+
.map(shadowFile => {
|
|
246
|
+
const file = normalize(join(root, relative(shadowRoot, shadowFile)));
|
|
247
|
+
return [pathKey(file), { file, shadowFile: normalize(shadowFile) }] as const;
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
const diagnostics: CompilerDiagnostic[] = [];
|
|
251
|
+
const selected: { readonly file: string; readonly shadowFile: string }[] = [];
|
|
252
|
+
const seen = new Set<string>();
|
|
253
|
+
const candidates =
|
|
254
|
+
requested === undefined
|
|
255
|
+
? [...members.values()].map(member => member.file)
|
|
256
|
+
: requested.map(file => normalize(isAbsolute(file) ? file : resolve(root, file)));
|
|
257
|
+
|
|
258
|
+
for (const file of candidates) {
|
|
259
|
+
const key = pathKey(file);
|
|
260
|
+
if (seen.has(key)) {
|
|
261
|
+
diagnostics.push({ code: 'ZMDB_DUPLICATE_FILE', message: `duplicate project file ${file}`, file });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
seen.add(key);
|
|
265
|
+
if (isGeneratedPath(file)) {
|
|
266
|
+
diagnostics.push({ code: 'ZMDB_GENERATED_INPUT', message: `generated input is not compilable: ${file}`, file });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const member = members.get(key);
|
|
270
|
+
if (member === undefined) {
|
|
271
|
+
diagnostics.push({
|
|
272
|
+
code: 'ZMDB_PROJECT_MEMBER',
|
|
273
|
+
message: `${file} is not a source member of ${project}`,
|
|
274
|
+
file,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
selected.push(member);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ordered = selected.toSorted((left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0));
|
|
282
|
+
const selectedPaths = new Set(ordered.map(member => pathKey(member.file)));
|
|
283
|
+
const dependencies = [...members.values()]
|
|
284
|
+
.filter(member => !selectedPaths.has(pathKey(member.file)))
|
|
285
|
+
.map(member => member.file)
|
|
286
|
+
.toSorted();
|
|
287
|
+
return {
|
|
288
|
+
files: ordered.map(member => member.file),
|
|
289
|
+
shadowFiles: ordered.map(member => member.shadowFile),
|
|
290
|
+
dependencies,
|
|
291
|
+
diagnostics,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isProjectSource(file: string, root: string): boolean {
|
|
296
|
+
if (file.includes(`${sep}node_modules${sep}`) || file.includes('/node_modules/')) return false;
|
|
297
|
+
if (/\.d\.[cm]?ts$/.test(file) || !/\.[cm]?tsx?$/.test(file) || isGeneratedPath(file)) return false;
|
|
298
|
+
const inside = relative(root, file);
|
|
299
|
+
return inside.length > 0 && !inside.startsWith('..') && !isAbsolute(inside);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function copyProject(root: string, shadow: string): void {
|
|
303
|
+
cpSync(root, shadow, {
|
|
304
|
+
recursive: true,
|
|
305
|
+
filter(source) {
|
|
306
|
+
const name = basename(source);
|
|
307
|
+
return name !== '.git' && name !== 'node_modules';
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
const modules = join(root, 'node_modules');
|
|
311
|
+
if (existsSync(modules)) symlinkSync(modules, join(shadow, 'node_modules'), 'dir');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function pathKey(path: string): string {
|
|
315
|
+
const value = normalize(path);
|
|
316
|
+
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function readText(path: string): string | undefined {
|
|
320
|
+
try {
|
|
321
|
+
return readFileSync(path, 'utf8');
|
|
322
|
+
} catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function writeAtomic(path: string, source: string, sequence: number): void {
|
|
328
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
329
|
+
const temporary = join(dirname(path), `.${basename(path)}.zmdb-${String(process.pid)}-${String(sequence)}`);
|
|
330
|
+
try {
|
|
331
|
+
writeFileSync(temporary, source);
|
|
332
|
+
renameSync(temporary, path);
|
|
333
|
+
} finally {
|
|
334
|
+
if (existsSync(temporary)) rmSync(temporary, { force: true });
|
|
335
|
+
}
|
|
336
|
+
}
|
package/src/lint/ast.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { VisitorNode } from './host-types.js';
|
|
2
|
+
|
|
3
|
+
type CallExpressionNode = VisitorNode<'CallExpression'>;
|
|
4
|
+
type ImportDeclarationNode = VisitorNode<'ImportDeclaration'>;
|
|
5
|
+
type ImportSpecifierNode = Extract<ImportDeclarationNode['specifiers'][number], { type: 'ImportSpecifier' }>;
|
|
6
|
+
type ProgramNode = VisitorNode<'Program'>;
|
|
7
|
+
type PropertyNode = VisitorNode<'Property'>;
|
|
8
|
+
type TSInterfaceDeclarationNode = VisitorNode<'TSInterfaceDeclaration'>;
|
|
9
|
+
type TSTypeNode = VisitorNode<'TSIntersectionType'>['types'][number];
|
|
10
|
+
|
|
11
|
+
const TAG_MODULES: ReadonlySet<string> = new Set(['@zmdb/schema/tags', 'zmdb/tags']);
|
|
12
|
+
const TAG_EXPORTS: ReadonlySet<string> = new Set([
|
|
13
|
+
'AnyRelation',
|
|
14
|
+
'Codec',
|
|
15
|
+
'Ext',
|
|
16
|
+
'Fts',
|
|
17
|
+
'HasDefault',
|
|
18
|
+
'Length',
|
|
19
|
+
'ManyToMany',
|
|
20
|
+
'ManyToOne',
|
|
21
|
+
'Max',
|
|
22
|
+
'MaxLength',
|
|
23
|
+
'Min',
|
|
24
|
+
'MinLength',
|
|
25
|
+
'Numeric',
|
|
26
|
+
'OneToMany',
|
|
27
|
+
'OneToOne',
|
|
28
|
+
'Pattern',
|
|
29
|
+
'PrimaryKey',
|
|
30
|
+
'Proto',
|
|
31
|
+
'ProtoField',
|
|
32
|
+
'References',
|
|
33
|
+
'Rule',
|
|
34
|
+
'Sensitive',
|
|
35
|
+
'Serial',
|
|
36
|
+
'Sql',
|
|
37
|
+
'Table',
|
|
38
|
+
'Unique',
|
|
39
|
+
'WireAs',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export interface ImportedTagBindings {
|
|
43
|
+
readonly tables: ReadonlySet<string>;
|
|
44
|
+
readonly tags: ReadonlySet<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function importedName(specifier: ImportSpecifierNode): string {
|
|
48
|
+
return specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function importedTagBindings(program: ProgramNode): ImportedTagBindings {
|
|
52
|
+
const tables = new Set<string>();
|
|
53
|
+
const tags = new Set<string>();
|
|
54
|
+
|
|
55
|
+
for (const statement of program.body) {
|
|
56
|
+
if (statement.type !== 'ImportDeclaration' || !TAG_MODULES.has(statement.source.value)) continue;
|
|
57
|
+
for (const specifier of statement.specifiers) {
|
|
58
|
+
if (specifier.type !== 'ImportSpecifier') continue;
|
|
59
|
+
const imported = importedName(specifier);
|
|
60
|
+
if (TAG_EXPORTS.has(imported)) tags.add(specifier.local.name);
|
|
61
|
+
if (imported === 'Table') tables.add(specifier.local.name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { tables, tags };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isTableDeclaration(node: TSInterfaceDeclarationNode, bindings: ImportedTagBindings): boolean {
|
|
69
|
+
return node.extends.some(
|
|
70
|
+
heritage =>
|
|
71
|
+
heritage.expression.type === 'Identifier' &&
|
|
72
|
+
bindings.tables.has(heritage.expression.name) &&
|
|
73
|
+
heritage.typeArguments?.params.length === 1,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function isImportedTagReference(node: TSTypeNode, bindings: ImportedTagBindings): boolean {
|
|
78
|
+
return (
|
|
79
|
+
node.type === 'TSTypeReference' && node.typeName.type === 'Identifier' && bindings.tags.has(node.typeName.name)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function staticMemberName(callee: CallExpressionNode['callee']): string | undefined {
|
|
84
|
+
if (callee.type !== 'MemberExpression') return undefined;
|
|
85
|
+
if (!callee.computed && callee.property.type === 'Identifier') return callee.property.name;
|
|
86
|
+
if (callee.computed && callee.property.type === 'Literal' && typeof callee.property.value === 'string') {
|
|
87
|
+
return callee.property.value;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function staticPropertyName(property: PropertyNode): string | undefined {
|
|
93
|
+
if (!property.computed && property.key.type === 'Identifier') return property.key.name;
|
|
94
|
+
if (property.key.type === 'Literal' && typeof property.key.value === 'string') return property.key.value;
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RuleTester } from 'oxlint/plugins-dev';
|
|
2
|
+
|
|
3
|
+
type Rule = Parameters<RuleTester['run']>[1];
|
|
4
|
+
|
|
5
|
+
export type HostLintRule = Extract<Rule, { create: unknown }>;
|
|
6
|
+
|
|
7
|
+
type LintVisitor = ReturnType<HostLintRule['create']>;
|
|
8
|
+
|
|
9
|
+
export type VisitorNode<Name extends keyof LintVisitor> = Parameters<NonNullable<LintVisitor[Name]>>[0];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { noDistributedNullableTags } from './rules/no-distributed-nullable-tags.js';
|
|
2
|
+
import { noEmptyPatch } from './rules/no-empty-patch.js';
|
|
3
|
+
import { noInterpolatedSql } from './rules/no-interpolated-sql.js';
|
|
4
|
+
import { noUnboundedFind } from './rules/no-unbounded-find.js';
|
|
5
|
+
import { noUnknownJsonColumn } from './rules/no-unknown-json-column.js';
|
|
6
|
+
import { requireSqlOnNumber } from './rules/require-sql-on-number.js';
|
|
7
|
+
import type { LintRule } from './types.js';
|
|
8
|
+
|
|
9
|
+
export type { LintRule } from './types.js';
|
|
10
|
+
|
|
11
|
+
interface LintPlugin {
|
|
12
|
+
readonly meta: {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly version: string;
|
|
15
|
+
};
|
|
16
|
+
readonly rules: Readonly<Record<string, LintRule>>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Severity = 'error' | 'warn';
|
|
20
|
+
|
|
21
|
+
interface FlatConfig {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly plugins: Readonly<Record<string, LintPlugin>>;
|
|
24
|
+
readonly rules: Readonly<Record<string, Severity>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface LintConfigs {
|
|
28
|
+
readonly recommended: readonly FlatConfig[];
|
|
29
|
+
readonly strict: readonly FlatConfig[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const rules: Readonly<Record<string, LintRule>> = {
|
|
33
|
+
'no-distributed-nullable-tags': noDistributedNullableTags,
|
|
34
|
+
'no-empty-patch': noEmptyPatch,
|
|
35
|
+
'no-interpolated-sql': noInterpolatedSql,
|
|
36
|
+
'no-unbounded-find': noUnboundedFind,
|
|
37
|
+
'no-unknown-json-column': noUnknownJsonColumn,
|
|
38
|
+
'require-sql-on-number': requireSqlOnNumber,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const plugin: LintPlugin = {
|
|
42
|
+
meta: {
|
|
43
|
+
name: '@zmdb/compiler',
|
|
44
|
+
version: '1.0.0-beta.1',
|
|
45
|
+
},
|
|
46
|
+
rules,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const configs: LintConfigs = {
|
|
50
|
+
recommended: [
|
|
51
|
+
{
|
|
52
|
+
name: 'zmdb/recommended',
|
|
53
|
+
plugins: { zmdb: plugin },
|
|
54
|
+
rules: {
|
|
55
|
+
'zmdb/no-distributed-nullable-tags': 'error',
|
|
56
|
+
'zmdb/no-empty-patch': 'warn',
|
|
57
|
+
'zmdb/no-interpolated-sql': 'error',
|
|
58
|
+
'zmdb/no-unbounded-find': 'warn',
|
|
59
|
+
'zmdb/no-unknown-json-column': 'error',
|
|
60
|
+
'zmdb/require-sql-on-number': 'warn',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
strict: [
|
|
65
|
+
{
|
|
66
|
+
name: 'zmdb/strict',
|
|
67
|
+
plugins: { zmdb: plugin },
|
|
68
|
+
rules: {
|
|
69
|
+
'zmdb/no-distributed-nullable-tags': 'error',
|
|
70
|
+
'zmdb/no-empty-patch': 'error',
|
|
71
|
+
'zmdb/no-interpolated-sql': 'error',
|
|
72
|
+
'zmdb/no-unbounded-find': 'error',
|
|
73
|
+
'zmdb/no-unknown-json-column': 'error',
|
|
74
|
+
'zmdb/require-sql-on-number': 'error',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default plugin;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { importedTagBindings, isImportedTagReference, isTableDeclaration, type ImportedTagBindings } from '../ast.js';
|
|
2
|
+
import type { HostLintRule, VisitorNode } from '../host-types.js';
|
|
3
|
+
|
|
4
|
+
type TSTypeNode = VisitorNode<'TSIntersectionType'>['types'][number];
|
|
5
|
+
type TSUnionTypeNode = VisitorNode<'TSUnionType'>;
|
|
6
|
+
|
|
7
|
+
const message = 'Move null and undefined outside the tagged intersection; nullish values cannot carry zmdb tags.';
|
|
8
|
+
|
|
9
|
+
function isNullish(node: TSTypeNode): boolean {
|
|
10
|
+
return node.type === 'TSNullKeyword' || node.type === 'TSUndefinedKeyword';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const noDistributedNullableTags: HostLintRule = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Keep nullable arms outside intersections with zmdb declaration tags.',
|
|
18
|
+
recommended: true,
|
|
19
|
+
},
|
|
20
|
+
fixable: 'code',
|
|
21
|
+
messages: { distributed: message },
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
let bindings: ImportedTagBindings = { tables: new Set<string>(), tags: new Set<string>() };
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
Program(node) {
|
|
29
|
+
bindings = importedTagBindings(node);
|
|
30
|
+
},
|
|
31
|
+
TSInterfaceDeclaration(node) {
|
|
32
|
+
if (!isTableDeclaration(node, bindings)) return;
|
|
33
|
+
|
|
34
|
+
for (const property of node.body.body) {
|
|
35
|
+
if (property.type !== 'TSPropertySignature') continue;
|
|
36
|
+
const annotation = property.typeAnnotation?.typeAnnotation;
|
|
37
|
+
if (annotation?.type !== 'TSIntersectionType') continue;
|
|
38
|
+
|
|
39
|
+
let nullableUnion: TSUnionTypeNode | undefined;
|
|
40
|
+
const tags: TSTypeNode[] = [];
|
|
41
|
+
let unsafe = false;
|
|
42
|
+
|
|
43
|
+
for (const member of annotation.types) {
|
|
44
|
+
if (member.type === 'TSUnionType') {
|
|
45
|
+
const nullishCount = member.types.filter(isNullish).length;
|
|
46
|
+
if (nullishCount === 1 && nullableUnion === undefined) {
|
|
47
|
+
nullableUnion = member;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (isImportedTagReference(member, bindings)) tags.push(member);
|
|
52
|
+
else unsafe = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (unsafe || nullableUnion === undefined || tags.length === 0) continue;
|
|
56
|
+
const nullish = nullableUnion.types.find(isNullish);
|
|
57
|
+
if (nullish === undefined) continue;
|
|
58
|
+
const data = nullableUnion.types.filter(member => !isNullish(member));
|
|
59
|
+
if (data.length === 0) continue;
|
|
60
|
+
|
|
61
|
+
const tagText = tags.map(tag => context.sourceCode.getText(tag)).join(' & ');
|
|
62
|
+
const replacement = [
|
|
63
|
+
...data.map(member => `(${context.sourceCode.getText(member)} & ${tagText})`),
|
|
64
|
+
context.sourceCode.getText(nullish),
|
|
65
|
+
].join(' | ');
|
|
66
|
+
|
|
67
|
+
context.report({
|
|
68
|
+
node: annotation,
|
|
69
|
+
messageId: 'distributed',
|
|
70
|
+
fix: fixer => fixer.replaceText(annotation, replacement),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { staticMemberName } from '../ast.js';
|
|
2
|
+
import type { HostLintRule } from '../host-types.js';
|
|
3
|
+
|
|
4
|
+
const message = 'update(id, {}) performs no write; it reads and returns the matching row.';
|
|
5
|
+
|
|
6
|
+
export const noEmptyPatch: HostLintRule = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Report update calls whose literal patch is empty.',
|
|
11
|
+
recommended: true,
|
|
12
|
+
},
|
|
13
|
+
messages: { emptyPatch: message },
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (staticMemberName(node.callee) !== 'update') return;
|
|
20
|
+
const patch = node.arguments[1];
|
|
21
|
+
if (patch?.type !== 'ObjectExpression' || patch.properties.length !== 0) return;
|
|
22
|
+
context.report({ node: patch, messageId: 'emptyPatch' });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { staticMemberName, staticPropertyName } from '../ast.js';
|
|
2
|
+
import type { HostLintRule, VisitorNode } from '../host-types.js';
|
|
3
|
+
|
|
4
|
+
type TemplateLiteralNode = VisitorNode<'TemplateLiteral'>;
|
|
5
|
+
|
|
6
|
+
const message = 'Do not interpolate values into SQL text; use driver parameters.';
|
|
7
|
+
|
|
8
|
+
function isSqlSink(node: TemplateLiteralNode): boolean {
|
|
9
|
+
const parent = node.parent;
|
|
10
|
+
if (
|
|
11
|
+
parent.type === 'Property' &&
|
|
12
|
+
parent.parent.type === 'ObjectExpression' &&
|
|
13
|
+
staticPropertyName(parent) === 'text' &&
|
|
14
|
+
parent.parent.properties.some(
|
|
15
|
+
property => property.type === 'Property' && staticPropertyName(property) === 'parameters',
|
|
16
|
+
)
|
|
17
|
+
) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
parent.type === 'CallExpression' &&
|
|
22
|
+
staticMemberName(parent.callee) === 'execute' &&
|
|
23
|
+
parent.arguments.some(argument => argument === node)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const noInterpolatedSql: HostLintRule = {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
docs: {
|
|
31
|
+
description: 'Report interpolated template literals used directly as SQL text.',
|
|
32
|
+
recommended: true,
|
|
33
|
+
},
|
|
34
|
+
messages: { interpolation: message },
|
|
35
|
+
schema: [],
|
|
36
|
+
},
|
|
37
|
+
create(context) {
|
|
38
|
+
return {
|
|
39
|
+
TemplateLiteral(node) {
|
|
40
|
+
if (node.expressions.length === 0 || !isSqlSink(node)) return;
|
|
41
|
+
context.report({ node, messageId: 'interpolation' });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|