1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
//! The provenance header line every generated file carries, and the exact
//! byte ordering it sits in.
//!
//! This lives in `scythe-codegen` rather than in the `scythe` CLI that
//! actually writes the files, for one reason: `tests/tool_validation.rs` in
//! this crate hands assembled files to the real language tools (`php -l`,
//! `ruby -c`, `gofmt`, ...), and it can only substantiate the claim that the
//! header sits in a position each language accepts if it assembles the same
//! bytes the CLI writes. A second copy of the comment-prefix table, or of
//! the preamble/header/body ordering, kept privately in the test harness,
//! would drift from the real one silently — and it would drift precisely in
//! the case the harness exists to catch.
use Cow;
use crateCodegenBackend;
/// Line-comment token to embed the provenance header behind, derived from
/// `manifest().backend.language`.
///
/// `language` is a `String`, not a Rust enum, but exactly 10 distinct values
/// are used across every one of the 106 shipped manifests, so this one match
/// table is the single place backends' comment syntax is derived from —
/// no per-backend declaration, and no manifest schema change. Unrecognized
/// values fall back to `//`: every backend shipped today matches one of the
/// listed languages, and `//` is a safe default for any future one that
/// doesn't.
/// Trailing text appended to the header line for languages whose *default*
/// linter configuration rejects it, derived from
/// `manifest().backend.language` the same way [`comment_prefix`] is.
///
/// Python only, for one concrete reason: ruff's default `line-length` is 88,
/// while the header runs to roughly 99 characters (`# scythe:provenance
/// v=0.13.0 backend=python-psycopg3 engine=postgresql schema=sch1:` plus 16
/// hex digits). Without a suppression, *every* file scythe generates for a
/// Python target reports `E501 Line too long` on line 1 under `ruff check
/// --select E` — a lint error the consumer did not write and cannot fix
/// without either hand-editing generated code or loosening their own
/// line-length. `# noqa: E501` is the same mechanism, and the same
/// two-spaces-before-`#` spelling, that scythe's Python backends already use
/// for the `# noqa: F401` on their conditionally-emitted imports.
///
/// The suffix is invisible to the verifier: `parse_provenance_header`
/// tokenizes the text after the sentinel on whitespace and skips any token
/// without an `=`, so `#`, `noqa:`, and `E501` are all ignored and the four
/// `key=value` fields still parse. Pinned by
/// `assemble_output_python_header_carries_noqa_and_still_round_trips` in
/// `scythe-cli`, where the parser lives.
///
/// Shortening the header instead was not an option: the width is driven by
/// the backend name, the engine alias, and a 16-hex-digit fingerprint, none
/// of which can be trimmed without losing information the verifier compares.
///
/// Known trade-off: a project that both raises `line-length` past ~99 *and*
/// opts into `RUF100` (unused-noqa) will see the suppression reported as
/// unnecessary. That is strictly the lesser problem — it takes an opt-in
/// rule plus a non-default line length to hit, whereas E501 fires on ruff's
/// defaults — and it is the same trade-off the existing `# noqa: F401`
/// lines already make.
/// Strip `\n` and `\r` from a provenance field value before it is embedded
/// in the header line.
///
/// Only [`header_line`]'s `engine` argument needs this: `version` is the
/// caller's own package version, `backend.name()` is a hardcoded per-backend
/// literal, and `schema` is always `sch1:` plus 16 lowercase hex characters
/// — none of those three can contain a line terminator. `engine` is the
/// `[[sql]]` `engine = "..."` value, deserialized verbatim from the user's
/// `scythe.toml` with no validation upstream (`normalize_engine` only
/// consults it to pick a dialect; the raw string is what a caller passes
/// through to here). A value containing `\n` or a lone `\r` would terminate
/// the comment early — everything after it would land on its own physical
/// line with no comment prefix, becoming live, uncommented content in the
/// generated file. That breaks the exact guarantee this module is built on:
/// the header always reads as an ordinary comment, never as code.
/// Sanitizing at the point of embedding (rather than at config parse time)
/// means the guarantee holds regardless of how `engine` arrives — this call
/// site today, or any future one — not just for callers that happen to
/// validate it first.
///
/// The verifier (`scythe check`) must sanitize its configured engine the
/// same way before comparing it against the parsed header's `engine` field:
/// the header always holds the sanitized value, so comparing it against a
/// raw, unsanitized value would permanently false-flag SC-PRV04 for any
/// config whose engine string needed sanitizing.
/// Build the provenance header line that [`assemble_file`] prepends to every
/// generated file, right after [`CodegenBackend::file_preamble`] and before
/// [`CodegenBackend::file_header`]: the sentinel `scythe check` searches for,
/// commented out using the target language's own line-comment syntax so it
/// reads as an ordinary comment to every downstream compiler, formatter, and
/// human.
///
/// `backend.name()` (not the raw `[[sql.gen]]` `backend = "..."` config
/// value) is what gets embedded — `get_backend` accepts several aliases per
/// backend (`"sqlx"`, `"rust"`, and `"rust-sqlx"` all construct the same
/// backend), and `name()` is the one canonical form every alias agrees on.
/// The verifier's SC-PRV03 check compares against this same `backend.name()`,
/// not the config alias, for exactly this reason.
///
/// `version` is supplied by the caller rather than read from this crate's
/// own `CARGO_PKG_VERSION`. The number that belongs in the header is the
/// version of the `scythe` binary that produced the file, because that is
/// the number `scythe check` compares against; `scythe-codegen` happens to
/// share the workspace version today, but baking its own constant in here
/// would silently embed the wrong one the moment the two diverge.
///
/// `engine` is sanitized via [`sanitize_field`] before embedding — see that
/// function's doc comment for why only `engine` needs it.
///
/// `queries` is the `q1:<16 hex>` fingerprint of the analyzed query set that
/// produced this file (see `AnalyzedQuery::fingerprint_set` in
/// `scythe-core`), the `queries=` counterpart to `schema`'s `sch1:<16 hex>`
/// — added in #94 so that editing a `.sql` query file without touching the
/// schema is no longer invisible to `scythe check`. It is a plain `&str`,
/// not sanitized like `engine`: unlike `engine` (a free-form config value),
/// it is always produced by `fingerprint_set`, which can only ever return
/// the fixed `q1:` tag plus lowercase hex — there is no path by which it
/// could contain a line terminator.
///
/// A per-language [`header_suffix`] may be appended after the last field —
/// today only Python's `# noqa: E501`, without which the line trips ruff's
/// default 88-character limit in every generated file.
/// Join a backend's `preamble` (text that must be the literal first bytes of
/// the file, e.g. PHP's `<?php`), the provenance `header` line, and the
/// generated `body` into the one byte ordering every generated file uses.
///
/// The preamble is unconditionally first — even ahead of the provenance
/// comment — because the constructs [`CodegenBackend::file_preamble`] carries
/// lose their meaning if anything at all precedes them (see that method's
/// doc comment).
///
/// The blank line between the header line and the body is emitted only when
/// `preamble` is non-empty. This is not cosmetic: for the 8 backends with a
/// preamble override, the old (pre-provenance) `file_header()` text already
/// opened with its own blank line (PHP's `"<?php\n\ndeclare(...)"`, Ruby's
/// `"# frozen_string_literal: true\n\n# Auto-generated..."`), so the
/// separator reproduces those exact bytes once the provenance line is
/// accounted for. For the other 44 backends, `file_header()` never had a
/// leading blank line, so an unconditional separator would silently insert
/// one that was never there before — invisible in the assembled output, but
/// a real, provable byte-level regression. Conditioning the separator on
/// `preamble.is_empty()` reproduces the old bytes in both cases:
/// - preamble non-empty (PHP): `"<?php\n"` + header + `"\n"` + `"declare..."`;
/// strip the header line → `"<?php\n\ndeclare..."`, the old bytes exactly.
/// - preamble empty (Go): header + `""` + `"// Code generated..."`;
/// strip the header line → `"// Code generated..."`, the old bytes exactly.