Skip to content

fix(lsp): expression binding diagnostics and completion filtering#413

Open
bennypowers wants to merge 4 commits into
mainfrom
fix/lsp/expression-binding-diagnostics
Open

fix(lsp): expression binding diagnostics and completion filtering#413
bennypowers wants to merge 4 commits into
mainfrom
fix/lsp/expression-binding-diagnostics

Conversation

@bennypowers

@bennypowers bennypowers commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Filter unresolved type identifiers from completion suggestions (silence > noise)
  • Classify template expression types (${expr}) in the TS document layer
  • Skip value diagnostics for Lit binding prefixes (.prop, @event, ?bool)
  • Skip value diagnostics for expression-valued attributes
  • Use tree-sitter grammar dispatch in validateAttributeValue instead of string matching
  • Strip binding prefix in attribute name diagnostics before manifest lookup

Details

Completion filtering: parseUnionType now only suggests quoted string literals
and boolean literals. Bare identifiers like NavigationPrimaryPalette (unresolved
type aliases), null, undefined, and primitives are silently dropped.

Expression classification: when a TS template attribute value contains ${...},
the TS tree is walked to find the matching template_substitution node and classify
the expression as literal, this-member, identifier, or complex. Classification
happens in the document layer; diagnostics layer consumes it.

Diagnostics: property bindings (.prop) pass JS values not HTML strings, event
bindings (@event) are function handlers, boolean bindings (?attr) control
presence via expression truthiness. All skip value validation. Expression-valued
attributes (ExpressionKind != "") also skip validation since we cannot type-check
the expression without a full type checker.

Attribute name diagnostics now strip ./?/@ prefixes before manifest lookup
and skip unknown-attribute diagnostics for @ event bindings.

Test plan

  • Red-first tests for completion filtering (5 cases)
  • Regression test: plain HTML attributes still validated
  • All existing LSP tests pass
  • make lint clean

Summary by CodeRabbit

  • New Features

    • Improved recognition of template expressions in custom-element attributes.
    • Added support for distinguishing literal, identifier, member, and complex attribute expressions.
    • Attribute diagnostics now understand binding prefixes and skip event-binding attributes.
    • Completion suggestions now focus on valid quoted string and boolean union values.
  • Bug Fixes

    • Plain HTML attribute values continue to receive validation diagnostics.
    • Reduced incorrect completion suggestions for unresolved identifiers, null, and undefined.

parseUnionType now only suggests quoted string literals and boolean
literals. Bare identifiers (unresolved type aliases like
NavigationPrimaryPalette), null, undefined, and primitives are
silently dropped. Silence is better DX than suggesting type names
as attribute values.

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ExpressionKind and ExpressionDetail fields to AttributeMatch.
When a TS template attribute value contains ${...}, the TS tree is
walked to find the matching template_substitution node and classify
the expression:

- literal: string/number/boolean literals (detail = value)
- this-member: this.propertyName (detail = member name)
- identifier: bare const/variable reference (detail = identifier)
- complex: anything else (binary, ternary, call, etc.)

Classification happens in the document layer where tree-sitter
access is available. Diagnostics layer (next step) will consume
ExpressionKind/Detail for type-aware validation.

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r type dispatch

Attribute value diagnostics:
- Skip validation for . (property), @ (event), ? (boolean) bindings
- Skip validation when ExpressionKind is set (${...} expressions)
- Dispatch on tree-sitter grammar name (predefined_type, union_type,
  literal_type, array_type, generic_type) instead of string matching

Attribute name diagnostics:
- Strip Lit binding prefix (./?/@) before manifest lookup
- Skip unknown-attribute diagnostic for @ event bindings

Regression test verifies plain HTML attributes still validated.

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace fragile grammar-name allowlist in findSubstitutionByContent
with byte-range pruning. The old approach skipped nodes by checking
grammar name against a hardcoded list (program, template_string,
call_expression), missing intermediate nodes like method_definition
or class_body that sit between the root and the template literal.

Also remove string-based ${...} boundary parsing -- the TS tree
node's expression child has the correct content directly, avoiding
issues with nested braces in expression values.

Assisted-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add template-expression metadata to attribute matches, make attribute diagnostics binding- and grammar-aware, and restrict union completions to quoted string and boolean literals. Tests cover completion filtering and continued validation of plain HTML attribute values.

Changes

Attribute analysis and completion

Layer / File(s) Summary
Template expression classification
lsp/types/document.go, lsp/document/typescript/document.go
AttributeMatch now carries expression kind/detail metadata, and TypeScript template parsing classifies overlapping substitutions such as literals, identifiers, and member expressions.
Binding-aware attribute diagnostics
lsp/methods/textDocument/publishDiagnostics/attributeDiagnostics.go, lsp/methods/textDocument/publishDiagnostics/attributeValueDiagnostics.go, lsp/methods/textDocument/publishDiagnostics/*_test.go
Attribute parsing records ., ?, and @ prefixes; event bindings and classified expressions are skipped during value validation, while parsed type grammars drive remaining validation. Plain HTML validation is covered by a regression test.
Literal union completions
lsp/methods/textDocument/completion/completion.go, lsp/methods/textDocument/completion/completion_type_parsing_test.go
Union completions now include quoted string literals and true/false, excluding unresolved identifiers, null, and undefined, with table-driven tests for these cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant findCustomElements
  participant parseHTMLInTemplate
  participant collectTemplateAttributes
  participant TypeScriptSyntaxTree
  findCustomElements->>parseHTMLInTemplate: pass tsRoot
  parseHTMLInTemplate->>collectTemplateAttributes: collect template attributes
  collectTemplateAttributes->>TypeScriptSyntaxTree: locate template_substitution
  TypeScriptSyntaxTree-->>collectTemplateAttributes: return expression classification
  collectTemplateAttributes-->>findCustomElements: return AttributeMatch metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main LSP changes: binding-aware diagnostics and completion filtering.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lsp/expression-binding-diagnostics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Build Artifacts

OS x64 arm64
Linux cem-linux-x64 cem-linux-arm64
macOS cem-darwin-x64 cem-darwin-arm64
Windows cem-win32-x64 cem-win32-arm64

Built from 8787335 @ fix/lsp/expression-binding-diagnostics

@github-actions

Copy link
Copy Markdown

LSP Benchmark Results

Benchmark PR Mean (ms) Base Mean (ms) Delta Success Rate Status
Startup 3.90612 3.817025 0.09 (2.3%) ➖ 100%
Hover 0.476507 0.533505 -0.06 (-10.7%) ✅ 100%
Completion 2.402874 2.516392 -0.11 (-4.5%) ✅ 100%
Diagnostics 2041.54695 2029.726971 11.82 (0.6%) ➖ 100%
Attribute Hover 0 0 0.00 (0.0%) ➖ 100%
References 30.4625 30.745 -0.28 (-0.9%) ✅ 30%

View this benchmark run in GitHub Actions

💡 Tip: Raw JSON results are available in workflow artifacts if needed.


Generate Benchmarks

Branch Total Time (s) # Runs Avg Time/run (s) Output Size (kb) Perf/kb (s/kb)
Base main 4.28751 6 0.714585 184 0.0233017
PR fix/lsp/expression-binding-diagnostics 4.41745 6 0.736242 184 0.0240079
Δ +0.1299 0 +0.0217 0 +0.0007 🐢

Perf/kb delta ratio: 0.97x 🐢

View this benchmark run in GitHub Actions

💡 Tip: Raw JSON outputs are available in workflow artifacts if needed.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
lsp/methods/textDocument/completion/completion_type_parsing_test.go (1)

26-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add test cases for boolean literals and double-quoted literals.

The new parseUnionType explicitly handles true/false (line 467 of completion.go) and isQuotedLiteral supports both single and double quotes, but neither path is exercised by the current test cases. All literal cases use single quotes only, and no union containing boolean literals is tested.

🧪 Suggested additional test cases
 		{
 			"only identifiers",
 			"SomeType | AnotherType",
 			nil,
 			0,
 			"Pure unresolved identifiers should produce no completions",
 		},
+		{
+			"boolean literals in union",
+			"true | false",
+			[]string{"true", "false"},
+			2,
+			"Boolean literals should be suggested as completion values",
+		},
+		{
+			"double-quoted literals",
+			`"red" | "green"`,
+			[]string{"red", "green"},
+			2,
+			"Double-quoted string literals should be suggested",
+		},
 		{
 			"primitive string",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lsp/methods/textDocument/completion/completion_type_parsing_test.go` around
lines 26 - 103, Extend TestTypeBasedCompletions_FiltersUnresolvedIdentifiers
with table cases covering true/false boolean literals and double-quoted
literals, including a union combining them where appropriate. Assert the
expected labels and completion counts so parseUnionType and isQuotedLiteral
exercise both supported paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lsp/document/typescript/document.go`:
- Around line 507-524: classifyExpression currently searches from
template.startByte with brittle +100 padding, causing later attributes to
resolve the wrong substitution. Anchor findFirstSubstitution to the specific
attribute value’s offset/range from match, derive the search end from that value
boundary, and preserve correct ExpressionKind and ExpressionDetail
classification for multi-expression templates.

---

Nitpick comments:
In `@lsp/methods/textDocument/completion/completion_type_parsing_test.go`:
- Around line 26-103: Extend
TestTypeBasedCompletions_FiltersUnresolvedIdentifiers with table cases covering
true/false boolean literals and double-quoted literals, including a union
combining them where appropriate. Assert the expected labels and completion
counts so parseUnionType and isQuotedLiteral exercise both supported paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dff64c24-ed38-4ed0-ac5a-7166312ab8e4

📥 Commits

Reviewing files that changed from the base of the PR and between fb40be6 and 2cc03df.

📒 Files selected for processing (7)
  • lsp/document/typescript/document.go
  • lsp/methods/textDocument/completion/completion.go
  • lsp/methods/textDocument/completion/completion_type_parsing_test.go
  • lsp/methods/textDocument/publishDiagnostics/attributeDiagnostics.go
  • lsp/methods/textDocument/publishDiagnostics/attributeValueDiagnostics.go
  • lsp/methods/textDocument/publishDiagnostics/attributeValueDiagnostics_test.go
  • lsp/types/document.go

Comment on lines +507 to +524
func classifyExpression(match *types.AttributeMatch, template TemplateContext, tsRoot *ts.Node, docContent []byte) {
templateEnd := template.startByte + uint(len(match.Value)+100)
sub := findFirstSubstitution(tsRoot, template.startByte, templateEnd)
if sub == nil {
match.ExpressionKind = "complex"
return
}

cursor := sub.Walk()
defer cursor.Close()
children := sub.NamedChildren(cursor)
if len(children) == 0 {
match.ExpressionKind = "complex"
return
}

classifyExpressionNode(&children[0], match, docContent)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'findFirstSubstitution|templateEnd|classifyExpression' lsp/document/typescript/document.go
fd -e go typescript | xargs rg -nl 'classifyExpression|ExpressionKind'

Repository: bennypowers/cem

Length of output: 2265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '430,590p' lsp/document/typescript/document.go
printf '\n---\n'
sed -n '1,220p' lsp/document/typescript/types.go
printf '\n---\n'
rg -n 'type AttributeMatch|ExpressionKind|ExpressionDetail|TemplateContext|closestValue|template.startByte|startByte' lsp/document/typescript -g '*.go'

Repository: bennypowers/cem

Length of output: 9423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '300,380p' lsp/document/typescript/document.go
printf '\n---\n'
sed -n '380,450p' lsp/document/typescript/document.go
printf '\n---\n'
rg -n 'template_substitution|ExpressionKind|classifyExpression\(' lsp -g '*.go'

Repository: bennypowers/cem

Length of output: 7368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' lsp/types/document.go
printf '\n---\n'
rg -n 'type AttributeMatch|ValueRange|ExpressionKind|ExpressionDetail' lsp/types -g '*.go'

Repository: bennypowers/cem

Length of output: 7888


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' lsp/methods/textDocument/publishDiagnostics/attributeValueDiagnostics.go
printf '\n---\n'
sed -n '1,180p' lsp/methods/textDocument/publishDiagnostics/attributeDiagnostics.go

Repository: bennypowers/cem

Length of output: 16554


Anchor classification to the attribute value, not the template start classifyExpression always starts at template.startByte, so every ${...} in the same template is resolved against the first matching substitution. That can misclassify later attributes in multi-expression templates and leave ExpressionKind/ExpressionDetail wrong. Use the attribute’s own value offset/range instead of the template origin; the +100 padding is also brittle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lsp/document/typescript/document.go` around lines 507 - 524,
classifyExpression currently searches from template.startByte with brittle +100
padding, causing later attributes to resolve the wrong substitution. Anchor
findFirstSubstitution to the specific attribute value’s offset/range from match,
derive the search end from that value boundary, and preserve correct
ExpressionKind and ExpressionDetail classification for multi-expression
templates.

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