Repository navigation
fix(check): allow dictionary table constructor fields - #1291
Conversation
There was a problem hiding this comment.
Code Review
Summary
The change adds an early-return in check_against_type to skip field validation when the target is a table<K, V> generic, plus tests. The intent (dictionary types have open key spaces) is reasonable, but the implementation has correctness and robustness concerns.
Issues
1. String-based type name matching is fragile (High)
LuaType::Generic(generic) if generic.get_base_type_id().get_name() == "table" => {
return;
}- Matching on the literal string
"table"is brittle. It will also match any user-defined generic class/alias namedtable, silently disabling field checks for it. - It relies on the std-lib
tablebeing registered under exactly that name. If the base type is aliased or renamed, the guard silently stops working (or over-fires). - Prefer checking the actual base type identity (e.g., comparing against the known std-lib
tabletype id /LuaType::Table), or checkinggeneric.get_base_type_id()against the semantic model's std-lib table declaration, rather than a raw name string.
2. return skips all validation, not just record-field checks (Medium)
The comment says "this checker only validates record fields," but the early return also bypasses the Intersection/Union/other branches below. If a table<K, V> appears as a component of an intersection or union, the remaining components are no longer checked. Consider whether the skip should be scoped to just the record-field path rather than returning from the whole function.
3. Ordering / interaction with other match arms (Medium)
The new arm is placed first. Confirm that a table<K, V> generic can never legitimately need the subsequent arms (e.g., intersection handling). If table<K,V> can be intersected with a record type, this early return would mask real errors. A comment or a more targeted guard would help.
4. Test coverage gaps (Low–Medium)
dictionary_constructor_does_not_disable_class_field_checksassertshas_no_diagnostic(...) == false, which only proves some diagnostic exists, not that it's the expectedInjectField/MissingFieldsone. It could pass for an unrelated diagnostic. Assert on the specific diagnostic/code.- No negative test for a user-defined generic named
table(the false-positive case from issue #1). - No test for
table<K,V>nested inside an intersection/union, which is exactly the path affected by issue #2. - The
Box<string>case in the third test doesn't exercise the newtablebranch at all; it's fine as a regression guard but doesn't validate the new logic.
5. Minor: test duplication
The two new tests repeat use crate::{DiagnosticCode, VirtualWorkspace}; use googletest::prelude::*; inside each function. If the module already imports these (the existing tests above likely do), these inner imports are redundant.
Recommendations
- Replace the name-string comparison with a structural/identity check against the std-lib
tabletype. - Narrow the skip so it doesn't bypass intersection/union handling — e.g., only skip the record-field validation, not the entire function.
- Strengthen tests: assert the specific diagnostic code, add a user-defined
tablegeneric negative case, and add an intersection/union case. - Remove redundant inner
usestatements if already imported at module scope.
Verdict
The direction is correct, but the string-based guard is the main blocker — it can both under- and over-match. Please address issue #1 (and ideally #2) before merging.
1550a05 to
0d0408b
Compare
|
rebased to current main |
A
table<string, Signal>constructor reports every entry asinject-fieldwhen the standard library is loaded. The field checker resolves the dictionary
base to the standard library's
tabledeclaration and treats its empty namedfield surface as a closed record.
Skip closed-record checking only for the canonical global
tabletype whoseresolved declaration belongs to the standard library. User-defined global or
file-private classes named
tableretain field checking. Alias expansion andcompound-type dispatch happen first; returning for a dictionary component does
not stop its enclosing intersection from checking other components. This keeps
the assignment checker's existing dictionary policy without adding dictionary
key/value validation or changing union selection.
Reproduction:
Five regression tests cover local/global constructors, computed/literal keys,
aliases, call arguments, reassignment, custom global/private
table<T>classes,intersection aliases in both orders, nullable unions, and retained class field
checks. The original constructor cases fail on unmodified upstream; the custom
table<T>regression also fails with the initial name-only guard. Diagnosticassertions use
has_no_diagnostic, which enables and filters exactly thespecified
InjectFieldorMissingFieldscode.Validation: all 2,407 workspace tests with all features, workspace/all-target/
all-feature Clippy with warnings denied, and rustfmt pass.