Skip to content

Commit 4ea92fc

Browse files
authored
Merge branch 'main' into chore/remove-unconstructed-variants
2 parents f2f58d4 + 9ed91ad commit 4ea92fc

10 files changed

Lines changed: 484 additions & 30 deletions

File tree

‎.github/workflows/verify.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,15 @@ jobs:
279279
run: uv python install 3.12
280280
- name: Install dependencies
281281
run: uv sync --locked --all-groups --all-extras
282+
- name: Fetch pull request base for the new-duplicate check
283+
if: github.event_name == 'pull_request' || github.event_name == 'merge_group'
284+
run: git fetch --no-tags --depth=1 origin "$BASE_SHA"
285+
env:
286+
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
282287
- name: Run static checks
283288
run: make check-ci
289+
env:
290+
DUPES_BASE: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || 'origin/main' }}
284291
- name: Run native checks
285292
run: make rust-check
286293
- name: Run unit and integration tests

‎Makefile‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
SHELL := /bin/bash
22

3-
.PHONY: verify verify-quick verify-pg coverage cli-preview rust-check check-native-layering check-native-stub check-build-features check-python-boundary check-release-gate check-docs-skill \
3+
.PHONY: verify verify-quick verify-pg coverage cli-preview rust-check check-native-layering check-dupes-new check-native-stub check-build-features check-python-boundary check-release-gate check-docs-skill \
44
check-e2e-shards test-e2e-duckdb test-e2e-duckdb-build-core \
55
test-e2e-duckdb-build-incremental \
66
test-e2e-duckdb-cli-data test-e2e-duckdb-cli test-e2e-duckdb-inspection \
@@ -562,6 +562,22 @@ dupes:
562562
uv run fensu dupes
563563

564564

565+
# Warn mode: report new duplicated code since DUPES_BASE without failing the build.
566+
DUPES_BASE ?= origin/main
567+
568+
check-dupes-new:
569+
@if ! git rev-parse --verify --quiet "$(DUPES_BASE)^{commit}" >/dev/null; then \
570+
echo "warning: skipped new-duplicate check: base $(DUPES_BASE) is not available"; \
571+
exit 0; \
572+
fi; \
573+
uv run fensu dupes --since "$(DUPES_BASE)" --fail-on-new; rc=$$?; \
574+
if [ $$rc -eq 1 ]; then \
575+
echo "warning: new duplicated code above (warn mode: this check does not fail yet)"; \
576+
elif [ $$rc -ne 0 ]; then \
577+
exit $$rc; \
578+
fi
579+
580+
565581
check:
566582
uv run ruff format .
567583
uv run ruff check --fix .
@@ -671,7 +687,7 @@ verify-quick:
671687
exit $$status
672688

673689

674-
check-ci: check-e2e-shards check-native-layering check-native-stub check-build-features check-python-boundary check-release-gate check-docs-skill rust-lint
690+
check-ci: check-dupes-new check-e2e-shards check-native-layering check-native-stub check-build-features check-python-boundary check-release-gate check-docs-skill rust-lint
675691
uv run ruff format --check .
676692
uv run ruff check .
677693
uv run ty check src tests

‎fensu.toml‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,37 @@ warn = ["XSB076", "XSB077"]
15451545

15461546
[targets.rust.rule_options.XSB076]
15471547
schema_paths = ["crates/sqlbuild-orchestration/src/store"]
1548+
allowed = [
1549+
"sqb_admin_initialization.initialized_at=Audit history read through audit exports: when the first administrator was created.",
1550+
"sqb_effective_roles.effective_since=Audit history read through audit exports: when a role's resolved grants took effect.",
1551+
"sqb_locations.created_by=Audit history read through audit exports: who created the location.",
1552+
"sqb_password_credentials.changed_at=Audit history read through audit exports: when a password last changed.",
1553+
"sqb_platform_sink_deliveries.delivered_at=Audit history read through audit exports: when an event reached the platform sink.",
1554+
"sqb_role_assignments.granted_at=Audit history read through audit exports: when a role was granted.",
1555+
"sqb_role_assignments.granted_by=Audit history read through audit exports: who granted a role.",
1556+
"sqb_role_assignments.revoked_by=Audit history read through audit exports: who revoked a role.",
1557+
"sqb_security_audit_deliveries.delivered_at=Audit history read through audit exports: when an event reached a security audit sink.",
1558+
"sqb_warehouse_microbatches.execution_run_id=Reader comes with the UI epic (D132), which filters on it: the run that executed the batch.",
1559+
"sqb_warehouse_microbatches.record_type=Reader comes with the UI epic (D132), which filters on it: the batch state.",
1560+
"sqb_warehouse_migrations.decision=Reader comes with the UI epic (D132), which filters on it.",
1561+
"sqb_warehouse_migrations.destination_model=Reader comes with the UI epic (D132), which filters on it.",
1562+
"sqb_warehouse_old_name_views.migration_event_id=Reader comes with the UI epic (D132), which filters on it.",
1563+
]
1564+
1565+
[targets.rust.rule_options.XSB077]
1566+
construction_required = [
1567+
"Action=A permission is live only when a route requires it; role tables and classifiers do not count.",
1568+
]
1569+
exceptions = [
1570+
"Action::ViewEvidence=Kept under D129; reader comes with the UI epic.",
1571+
"InaccessibleReason=Wire enum that serde builds from Python scope records.",
1572+
"RoleDecision::Rejected=Serde builds it from an administrator's decide request.",
1573+
"ScopeDiagnosticCode=Wire enum that serde builds from Python scope diagnostics.",
1574+
"TickOutcome::Covered=The scheduler writes it as SQL text and serde reads it back.",
1575+
"TickOutcome::Requested=The scheduler writes it as SQL text and serde reads it back.",
1576+
"TickOutcome::Skipped=The scheduler writes it as SQL text and serde reads it back.",
1577+
"UsageKind=Wire enum that serde builds from Python usage records.",
1578+
]
15481579

15491580
[[targets.rust.rule_exceptions]]
15501581
rule = "FPRSR307"

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ dev = [
8181
"pytest-cov>=7.0.0",
8282
"pytest-xdist>=3.8.0",
8383
"ruff>=0.12.0",
84-
"fensu>=0.28.3",
84+
"fensu>=0.29.0",
8585
"testcontainers>=4.8.1",
8686
"ty>=0.0.1a19",
8787
]

‎scripts/fensu_policy/_helpers/unused_state.py‎

Lines changed: 161 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
BRACKET_CLOSE,
1414
BRACKET_OPEN,
1515
LIST_SEPARATOR,
16+
NATIVE_PATH_SEPARATOR,
1617
PARENTHESIS_CLOSE,
1718
PARENTHESIS_OPEN,
19+
REASON_SEPARATOR,
1820
REMOVED_CODE,
1921
RUST_ATTRIBUTE_PATTERN,
2022
RUST_BARE_WORD_PATTERN,
@@ -23,10 +25,17 @@
2325
RUST_IMPL_HEADER_PATTERN,
2426
RUST_LINE_CONTINUATION,
2527
RUST_LITERAL_PATTERN,
28+
RUST_LOOKBEHIND,
2629
RUST_MACRO_INVOCATION_PATTERN,
30+
RUST_MATCHES_MACRO_PATTERN,
2731
RUST_PATH_PAIR_PATTERN,
32+
RUST_PATTERN_FOLLOWER_PATTERN,
33+
RUST_PATTERN_LEADER_PATTERN,
34+
RUST_QUALIFIED_VARIANT_PATTERN,
35+
RUST_SELF,
2836
RUST_SELF_PATH_PATTERN,
2937
RUST_SIMPLE_ESCAPES,
38+
RUST_TABLE_ITEM_PATTERN,
3039
RUST_TEST_FILE_NAME,
3140
RUST_TEST_MODULE_PATTERN,
3241
RUST_TEST_PATH_SEGMENTS,
@@ -69,6 +78,11 @@
6978
_SELF_PATH: re.Pattern[str] = re.compile(RUST_SELF_PATH_PATTERN)
7079
_VARIANT_IMPORT: re.Pattern[str] = re.compile(RUST_VARIANT_IMPORT_PATTERN)
7180
_BARE_WORD: re.Pattern[str] = re.compile(RUST_BARE_WORD_PATTERN)
81+
_QUALIFIED: re.Pattern[str] = re.compile(RUST_QUALIFIED_VARIANT_PATTERN)
82+
_TABLE_ITEM: re.Pattern[str] = re.compile(RUST_TABLE_ITEM_PATTERN)
83+
_MATCHES_MACRO: re.Pattern[str] = re.compile(RUST_MATCHES_MACRO_PATTERN)
84+
_PATTERN_FOLLOWER: re.Pattern[str] = re.compile(RUST_PATTERN_FOLLOWER_PATTERN)
85+
_PATTERN_LEADER: re.Pattern[str] = re.compile(RUST_PATTERN_LEADER_PATTERN)
7286
_SQL_COMMENTS: re.Pattern[str] = re.compile(SQL_COMMENT_PATTERN, re.DOTALL)
7387
_SQL_TOKENS: re.Pattern[str] = re.compile(SQL_TOKEN_PATTERN)
7488
_SQL_WORD: re.Pattern[str] = re.compile(SQL_WORD_PATTERN)
@@ -256,19 +270,108 @@ def enum_variants(*, path: str, code: str) -> list[EnumVariant]:
256270
return variants
257271

258272

259-
def named_variants(*, variants: list[EnumVariant], codes: dict[str, str]) -> set[EnumVariant]:
260-
"""Variants named by a path, by `Self` in an impl's file, or by an imported variant."""
273+
def named_variants(
274+
*, variants: list[EnumVariant], codes: dict[str, str], strict: frozenset[str] = frozenset()
275+
) -> set[EnumVariant]:
276+
"""Variants named in code; a `strict` enum's variants count only where built as a value."""
261277

262278
bodies: dict[str, list[tuple[int, int]]] = {}
263279
variant: EnumVariant
264280
for variant in variants:
265281
bodies.setdefault(variant.path, []).append(variant.body)
266282
names: set[tuple[str, str]] = set()
283+
constructed: set[tuple[str, str]] = set()
267284
path: str
268285
code: str
269286
for path, code in codes.items():
270-
names.update(_file_names(code=_without_spans(text=code, spans=bodies.get(path, []))))
271-
return {variant for variant in variants if (variant.enum, variant.variant) in names}
287+
visible: str = _without_spans(text=code, spans=bodies.get(path, []))
288+
names.update(_file_names(code=visible))
289+
if strict:
290+
constructed.update(_constructed_names(code=visible, strict=strict))
291+
return {
292+
variant
293+
for variant in variants
294+
if (variant.enum, variant.variant) in (constructed if variant.enum in strict else names)
295+
}
296+
297+
298+
def reasoned_entries(*, entries: tuple[str, ...]) -> tuple[dict[str, str], list[str]]:
299+
"""`name=reason` option entries by name, and the entries that lack a name or a reason."""
300+
301+
reasons: dict[str, str] = {}
302+
malformed: list[str] = []
303+
entry: str
304+
for entry in entries:
305+
name, _, reason = entry.partition(REASON_SEPARATOR)
306+
if name.strip() and reason.strip():
307+
reasons[name.strip()] = reason.strip()
308+
else:
309+
malformed.append(entry)
310+
return reasons, malformed
311+
312+
313+
def _constructed_names(*, code: str, strict: frozenset[str]) -> set[tuple[str, str]]:
314+
value_code: str = _without_tables(code=code)
315+
implemented: set[str] = set()
316+
header: re.Match[str]
317+
for header in _IMPL_HEADER.finditer(value_code):
318+
implemented.update(set(_SQL_WORD.findall(header.group(0))) & strict)
319+
names: set[tuple[str, str]] = set()
320+
occurrence: re.Match[str]
321+
for occurrence in _QUALIFIED.finditer(value_code):
322+
owner: str = occurrence.group(1)
323+
enums: set[str] = implemented if owner == RUST_SELF else {owner} & strict
324+
if enums and not _pattern_position(
325+
code=value_code, start=occurrence.start(), end=occurrence.end()
326+
):
327+
names.update(itertools.product(enums, (occurrence.group(2),)))
328+
return names
329+
330+
331+
def _without_tables(*, code: str) -> str:
332+
spans: list[tuple[int, int]] = [
333+
(item.start(), _statement_end(text=code, start=item.end()))
334+
for item in _TABLE_ITEM.finditer(code)
335+
]
336+
macro: re.Match[str]
337+
for macro in _MATCHES_MACRO.finditer(code):
338+
closing: int = _closing(
339+
text=code, opening=macro.end() - 1, pair=(PARENTHESIS_OPEN, PARENTHESIS_CLOSE)
340+
)
341+
pieces: list[tuple[int, str]] = _top_level_pieces(text=code, start=macro.end(), end=closing)
342+
if len(pieces) > 1:
343+
spans.append((pieces[1][0], closing))
344+
return _without_spans(text=code, spans=spans)
345+
346+
347+
def _statement_end(*, text: str, start: int) -> int:
348+
depth: int = 0
349+
offset: int
350+
for offset in range(start, len(text)):
351+
character: str = text[offset]
352+
depth += (character in (PARENTHESIS_OPEN, BRACKET_OPEN, BRACE_OPEN)) - (
353+
character in (PARENTHESIS_CLOSE, BRACKET_CLOSE, BRACE_CLOSE)
354+
)
355+
if character == SQL_STATEMENT_SEPARATOR and depth == 0:
356+
return offset
357+
return len(text)
358+
359+
360+
def _pattern_position(*, code: str, start: int, end: int) -> bool:
361+
after: int = end
362+
while after < len(code) and code[after].isspace():
363+
after += 1
364+
if after < len(code) and code[after] in (PARENTHESIS_OPEN, BRACE_OPEN):
365+
pair: tuple[str, str] = (
366+
(PARENTHESIS_OPEN, PARENTHESIS_CLOSE)
367+
if code[after] == PARENTHESIS_OPEN
368+
else (BRACE_OPEN, BRACE_CLOSE)
369+
)
370+
after = _closing(text=code, opening=after, pair=pair) + 1
371+
return (
372+
_PATTERN_FOLLOWER.match(code, after) is not None
373+
or _PATTERN_LEADER.search(code[max(0, start - RUST_LOOKBEHIND) : start]) is not None
374+
)
272375

273376

274377
def _file_names(*, code: str) -> set[tuple[str, str]]:
@@ -432,3 +535,57 @@ def _column_name(*, piece: str) -> str | None:
432535
if first in SQL_COLUMN_CONSTRAINT_WORDS or _SQL_WORD.fullmatch(first) is None:
433536
return None
434537
return first
538+
539+
540+
def allowed_state(
541+
*, definitions: list[StateDefinition], unread: list[StateDefinition], entries: tuple[str, ...]
542+
) -> tuple[list[StateDefinition], list[str]]:
543+
"""Unread state not allowlisted, and malformed or stale entries for tables defined here."""
544+
545+
reasons: dict[str, str]
546+
malformed: list[str]
547+
reasons, malformed = reasoned_entries(entries=entries)
548+
defined_tables: set[str] = {definition.table for definition in definitions}
549+
unread_keys: set[str] = {_state_key(definition=definition) for definition in unread}
550+
stale: list[str] = [
551+
f"allowlisted {key} is read or no longer defined; remove the entry"
552+
for key in reasons
553+
if key.partition(SQL_QUALIFIER)[0] in defined_tables and key not in unread_keys
554+
]
555+
problems: list[str] = [f"allowlist entry {entry!r} needs `name=reason`" for entry in malformed]
556+
kept: list[StateDefinition] = [
557+
definition for definition in unread if _state_key(definition=definition) not in reasons
558+
]
559+
return kept, problems + stale
560+
561+
562+
def excepted_variants(
563+
*, variants: list[EnumVariant], unnamed: list[EnumVariant], entries: tuple[str, ...]
564+
) -> tuple[list[EnumVariant], list[str]]:
565+
"""Unnamed variants not excepted, and problems: malformed or no-longer-needed entries."""
566+
567+
reasons: dict[str, str]
568+
malformed: list[str]
569+
reasons, malformed = reasoned_entries(entries=entries)
570+
declared: set[str] = {variant.enum for variant in variants}
571+
covered: set[str] = set()
572+
kept: list[EnumVariant] = []
573+
variant: EnumVariant
574+
for variant in unnamed:
575+
keys: set[str] = {variant.enum, f"{variant.enum}{NATIVE_PATH_SEPARATOR}{variant.variant}"}
576+
covered.update(keys & set(reasons))
577+
if not keys & set(reasons):
578+
kept.append(variant)
579+
stale: list[str] = [
580+
f"exception {key} no longer matches an unnamed variant; remove the entry"
581+
for key in reasons
582+
if key.partition(NATIVE_PATH_SEPARATOR)[0] in declared and key not in covered
583+
]
584+
problems: list[str] = [f"exception entry {entry!r} needs `name=reason`" for entry in malformed]
585+
return kept, problems + stale
586+
587+
588+
def _state_key(*, definition: StateDefinition) -> str:
589+
if definition.column is None:
590+
return definition.table
591+
return f"{definition.table}{SQL_QUALIFIER}{definition.column}"

‎scripts/fensu_policy/constants.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,35 @@
444444
SQL_WRITE_WORDS: frozenset[str] = frozenset(
445445
{"insert", "into", "values", "update", "set", "delete", "conflict", "do"}
446446
)
447+
REASON_SEPARATOR: str = "="
448+
RUST_LOOKBEHIND: int = 16
449+
RUST_MATCHES_MACRO_PATTERN: str = r"\bmatches!\s*\("
450+
RUST_PATTERN_FOLLOWER_PATTERN: str = r"\s*(?:=>|\|(?!\|)|if\b|==|!=|=(?![=>]))"
451+
RUST_PATTERN_LEADER_PATTERN: str = r"(?:(?<!\|)\|(?!\|)|\blet|==|!=)\s*$"
452+
RUST_QUALIFIED_VARIANT_PATTERN: str = r"\b([A-Za-z_]\w*)\s*::\s*([A-Za-z_]\w*)\b(?!\s*::)"
453+
RUST_SELF: str = "Self"
454+
RUST_TABLE_ITEM_PATTERN: str = (
455+
r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?\s+)?"
456+
r"(?:use\b|(?:const|static)\s+(?:mut\s+)?[A-Z_][A-Z0-9_]*\s*:)"
457+
)
458+
VARIANT_CONSTRUCTION_REQUIRED: RuleOption[tuple[str, ...]] = RuleOption.string_list(
459+
name="construction_required",
460+
default=(),
461+
description=(
462+
"`Enum=reason` entries whose variants count only where code constructs them as a value, "
463+
"not in tables, patterns or comparisons."
464+
),
465+
)
466+
VARIANT_EXCEPTIONS: RuleOption[tuple[str, ...]] = RuleOption.string_list(
467+
name="exceptions",
468+
default=(),
469+
description=(
470+
"`Enum=reason` or `Enum::Variant=reason` entries for variants built outside Rust code."
471+
),
472+
)
473+
CONTROL_STORE_ALLOWED: RuleOption[tuple[str, ...]] = RuleOption.string_list(
474+
name="allowed",
475+
default=(),
476+
description="`table.column=reason` entries for state read outside production code.",
477+
)
478+
POLICY_CONFIG_FILE: str = "fensu.toml"

0 commit comments

Comments
 (0)