Skip to content

fix(check): allow dictionary table constructor fields - #1291

Merged
CppCXY merged 2 commits into
EmmyLuaLs:mainfrom
hgschmie:fix/dictionary-constructor-fields
Oct 14, 2026
Merged

CppCXY merged 2 commits into
EmmyLuaLs:mainfrom
hgschmie:fix/dictionary-constructor-fields

Conversation

@hgschmie

@hgschmie hgschmie commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

A table<string, Signal> constructor reports every entry as inject-field
when the standard library is loaded. The field checker resolves the dictionary
base to the standard library's table declaration and treats its empty named
field surface as a closed record.

Skip closed-record checking only for the canonical global table type whose
resolved declaration belongs to the standard library. User-defined global or
file-private classes named table retain field checking. Alias expansion and
compound-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:

---@class Signal
---@field name string
KEY = 'depot'
---@type table<string, Signal>
signals = { [KEY] = { name = KEY } }

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. Diagnostic
assertions use has_no_diagnostic, which enables and filters exactly the
specified InjectField or MissingFields code.

Validation: all 2,407 workspace tests with all features, workspace/all-target/
all-feature Clippy with warnings denied, and rustfmt pass.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 named table, silently disabling field checks for it.
  • It relies on the std-lib table being 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 table type id / LuaType::Table), or checking generic.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_checks asserts has_no_diagnostic(...) == false, which only proves some diagnostic exists, not that it's the expected InjectField/MissingFields one. 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 new table branch 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

  1. Replace the name-string comparison with a structural/identity check against the std-lib table type.
  2. Narrow the skip so it doesn't bypass intersection/union handling — e.g., only skip the record-field validation, not the entire function.
  3. Strengthen tests: assert the specific diagnostic code, add a user-defined table generic negative case, and add an intersection/union case.
  4. Remove redundant inner use statements 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.

@hgschmie
hgschmie marked this pull request as ready for review October 13, 2026 18:56
@hgschmie
hgschmie force-pushed the fix/dictionary-constructor-fields branch from 1550a05 to 0d0408b Compare October 14, 2026 05:42
@hgschmie

Copy link
Copy Markdown
Contributor Author

rebased to current main

@CppCXY
CppCXY merged commit 7096fef into EmmyLuaLs:main Oct 14, 2026
16 checks passed
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.

2 participants