Skip to content

refactor(protogen): sweep stale protos by ownership marker instead of deleting upfront - #451

Open
Kybxd wants to merge 3 commits into
masterfrom
refactor/protogen-outdir-sweep
Open

refactor(protogen): sweep stale protos by ownership marker instead of deleting upfront#451
Kybxd wants to merge 3 commits into
masterfrom
refactor/protogen-outdir-sweep

Conversation

@Kybxd

@Kybxd Kybxd commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

GenAll used to empty the outdir before generating anything, keeping only the files listed in ProtoFiles. Three problems came out of that.

1. The protection list was derived from an unrelated option

ProtoFiles specifies the predefined proto files to compile, yet it doubled as the deletion allowlist:

return prepareOutdir(outdir, gen.InputOpt.ProtoFiles, delExisted)

So a handwritten proto file living directly in the outdir was removed unless it happened to be listed there.

2. Deleting before validating was destructive

A run failing halfway left the outdir emptied. Combined with preserveFieldNumbers, whose snapshot is in-memory only, the next run had no field number baseline any more, hence silently reassigned field numbers from 1 and broke binary compatibility — exactly what the option exists to prevent.

3. Conflict detection depended on the outdir being emptied

Two workbooks generating the same proto file were detected by testing the existence of the file:

if existed { return xerrors.Newf("file already exists: %s", path) }

This only works if the outdir was emptied first, so it was unavailable to GenWorkbook, where such a conflict silently overwrote the previously generated file. Note filenameWithSubdirPrefix defaults to false, so excel/a/Item.xlsx and excel/b/Item.xlsx do collide by default.

Solution

The proto files generated in this run are now tracked in memory, which becomes the source of truth for both concerns.

Conflict detection queries the tracked paths instead of the filesystem, so it always applies, and both conflicting workbooks are named. Reported as the new error code E1000 so it can be localized:

error[E1000]: 生成的proto文件重复
错误原因: 工作簿 "conf/server/Shop.xlsx" 和 "conf/server/Activity/Shop.xlsx" 生成了相同的proto文件 "pkg/protoconf/proto/shop_conf.proto"
修复建议: 重命名其中一个工作簿,或为其设置唯一的别名

It deliberately names the workbook paths, not wb.GetName() — the latter is the snake-cased basename, which is always identical for conflicting workbooks and therefore useless for telling them apart.

Sweeping happens after all proto files are generated successfully, removing recursively the ones generated by previous runs but not by this one, identified by the header protogen always writes:

const generatedFileHeaderPrefix = "// Code generated by tableau"

The version part is intentionally excluded, so protos generated by other versions of tableau are still recognized. The exporter now emits the header from this same constant, so the two can never drift.

The sweep only happens in GenAll, which is authoritative over the whole outdir; GenWorkbook generates only the specified workbooks, so the others are not stale.

Thus both the delExisted and checkProtoFileConflicts parameters are gone, and prepareOutdir degenerates into MkdirAll.

Outcome

before after
Handwritten proto in outdir removed unless listed in ProtoFiles always kept
Failed run outdir emptied, field number baseline lost previous protos intact
Conflict detection GenAll only GenAll + GenWorkbook
Conflict message file already exists: xxx E1000, names both workbook paths
Sweep scope top level only recursive

This also retroactively fixes a latent issue for descriptor set mode (#TBD), where ProtoFiles is unset and every proto in the outdir would have been deleted.

Behavior changes

  • A .proto without the tableau generated header is no longer removed by GenAll. More correct, but a change.
  • The sweep is now recursive. Safe, because ownership is decided per file.

Tests

  • Test_sweepOutdir_keepsHandwrittenProtoInOutdir — guards problem 1 directly
  • Test_sweepOutdir_removesStaleGeneratedProto
  • Test_registerGeneratedProtoFile_conflict — asserts E1000 and that both distinct paths appear
  • Test_isGeneratedProtoFile — 6 subtests incl. another tableau version, protoc-gen-go, empty and truncated files
  • Test_sweepOutdir / Test_prepareOutdir — rewritten for the new semantics

Full go test ./... and go test -race ./internal/protogen/ pass.

Verified end-to-end with cmd/tableauc: a stale generated proto is swept, a handwritten one is kept, and the conflict scenario reproduces the E1000 message above in both locales.

Note

"A failed run leaves the previous protos intact" is guaranteed structurally (processSecondPass returns early on error, sweepOutdir runs after it) and covered by unit tests, but I could not construct an end-to-end failing run to demonstrate it.

… deleting upfront

GenAll used to empty the outdir before generating anything, keeping only the
files listed in the ProtoFiles option, which had several problems:

- The protection list was derived from an unrelated option: ProtoFiles specifies
  the predefined proto files to compile, yet it doubled as the deletion allowlist,
  so a handwritten proto file living directly in the outdir was removed unless it
  happened to be listed there. Notably it was always removed in descriptor set
  mode, where ProtoFiles is unset.
- Deleting before validating was destructive: a run failing halfway left the
  outdir emptied. Together with preserveFieldNumbers, whose warm-up cache is
  in-memory only, the next run had no field number baseline any more, hence
  silently reassigned field numbers from 1 and broke binary compatibility, which
  is exactly what the option is meant to prevent.
- Conflict detection relied on emptying the outdir beforehand, as two books
  generating the same proto file were detected by testing the existence of the
  file. So it was unavailable to GenWorkbook, where such a conflict silently
  overwrote the previously generated one.

Now the proto files generated in this run are tracked in memory, which is the
source of truth for both concerns:

- Conflict detection queries the tracked paths instead of the filesystem, so it
  always applies, and both conflicting books are named in the error message.
- After all proto files are generated successfully, the outdir is swept
  recursively to remove the ones generated by previous runs but not by this one,
  identified by the generated file header that protogen always writes. Hence the
  handwritten proto files are kept no matter where they live, and a failed run
  leaves the previously generated ones intact.

The sweep only happens in GenAll, which is authoritative over the whole outdir,
whereas GenWorkbook generates only the specified books, so the others are not
stale. Thus both the delExisted and checkProtoFileConflicts parameters are gone.

The conflict is reported as the new error code E1000, so as to be localized. It
names the workbook paths rather than the workbook names, as the latter are the
snake-cased basenames, which are always the same for the conflicting workbooks,
hence useless for telling them apart.

NOTE: the preserveFieldNumbers snapshot in preprocess is kept as defense in
depth. The generated proto files are truncated in place during generation, and an
exporter happens to query the snapshot, which initializes it lazily, before
truncating its own file. But a workbook whose worksheets are all filtered out
truncates its file without querying at all, which would otherwise let another
exporter snapshot a truncated file, hence silently reassign field numbers.
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf CI / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedAug 14, 2026, 8:16 AM

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.69014% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.63%. Comparing base (f42dc67) to head (0a86527).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
internal/protogen/util.go 73.91% 3 Missing and 3 partials ⚠️
internal/protogen/protogen.go 88.63% 3 Missing and 2 partials ⚠️
internal/protogen/exporter.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #451      +/-   ##
==========================================
- Coverage   75.64%   75.63%   -0.01%     
==========================================
  Files          88       88              
  Lines        9533     9599      +66     
==========================================
+ Hits         7211     7260      +49     
- Misses       1747     1759      +12     
- Partials      575      580       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Kybxd
Kybxd requested a review from wenchy August 13, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant