Skip to content

feat: warn about properties an element doesn't have in views and stylesheets - #67

Merged
ewanharris merged 7 commits into
mainfrom
claude/issue-17-handoff-review-gol36j
Oct 15, 2026
Merged

ewanharris merged 7 commits into
mainfrom
claude/issue-17-handoff-review-gol36j

Conversation

@ewanharris

Copy link
Copy Markdown
Member

Closes #66

The server's first diagnostic. Alloy compiles widht: 10 in a .tss file and colr="red" on a <Label> without a word, and the property just never applies. Now the editor shows a warning where the property is written, with a quick fix to the name it was probably meant to be.

What it reports

  • Stylesheets: a property that none of the elements the rule styles has. The rule is judged against all of its elements together, through app.tss, themes, platform folders and widgets. So a class shared by a <Label> and a <Window> can set text and orientationModes without either being flagged.
  • Views: an attribute the element's type doesn't have. Alloy's own attributes, events, and the properties Alloy reads itself are left alone: the Toolbar's ActionBar properties, and customToolbar on the parent <Window>.
  • Nested properties: font: { fntSize } and font.fntSize="…" are checked by their path.
  • Once per place it's written, not once per element the rule reaches.

What it never guesses at

  • Elements with no types: <Require>, <Widget>, <Module>, ns=/module= elements and native modules. A rule that reaches any of them isn't checked.
  • Anything inside abstract markup. <ButtonBar><Labels><Label image="…"/></Labels></ButtonBar> becomes an entry of the labels array, not a Label.
  • Projects whose types are an older major than their SDK. That's every project on SDK 10 or 11, since no types were published for either. The 9.x types they fall back to don't know the properties those SDKs added, so each would read as a mistake and the quick fix would suggest a wrong name. The log says once per project why there are no warnings.

Delivery

  • Push, not pull: findings go out through textDocument/publishDiagnostics as warnings, with source titanium and code unknown-property. Alloy projects only.
  • When: every open view and stylesheet is checked 300ms after typing stops. A view edit can add or remove a warning in a stylesheet open beside it, so all of them are re-checked, not just the one that changed.
  • Stale results: a check of text that has since changed is thrown away, and closing a file takes its warnings back.
  • The quick fix is worked out again from the file rather than read back from the diagnostic, because carrying data on a diagnostic needs a capability not every client has. It names the warning it fixes.
  • No setting. The server has none by design (Settings over workspace/configuration #14). vscode-titanium gets a setting of its own that hides them on the client.

Speed

On tidev/kitchensink-v2, the uncached check took 3.9s for app.tss, which styles all 78 views. Two caches bring it to about 170ms, and every other file to a few milliseconds:

  • ProjectService.membersOf keeps its answers for as long as the TypeScript program lasts.
  • Each check asks about each type only once.

Checked against a real app

tidev/kitchensink-v2 @ 4d43b4c (SDK 13.1.1, real @types/titanium 13.3.0, 78 views, 999 rules) gives 3 findings, and each holds up:

app/styles/controls/stepper.tss           .stepper → mininum   (did you mean 'minimum'?)
app/views/controls/views/cardview.xml     borderColor          (declared `never` on Ti.UI.Android.CardView)
app/styles/controls/dialogs/optionDialog.tss  #dialog[platform=ios] → preferred   (not in the types)

The first is a real bug in that app. It was 18 findings before the abstract-markup and Alloy-property rules.

Tests

1277 tests pass. Coverage is 99.61% of lines and 97.01% of branches, and lint and the type check are clean.

  • Core: the check itself, findings across a project and its themes, the message, and the quick fix's edit, including quotes and dotted names.
  • Server: warnings published and cleared, the stylesheet re-checked when its view changes, stale results dropped, warnings cleared on close, the fix linked to its warning, no warnings without types or with older types.
  • Over stdio: a warning arrives and is cleared once fixed. It uses a new fixture, alloy-matched-project, on SDK 9, because every other Alloy fixture is on SDK 10 or 12 against the 9.2.2 stub types.

Out of scope

Each can be its own issue:

  • checking values, such as color: 42
  • unused selectors, which need a check across every platform
  • checks across files: $.id, handlers, translation keys
  • TypeScript's own errors in JavaScript
  • a better answer for SDK 10 and 11 than no warnings

🤖 Generated with Claude Code

https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4


Generated by Claude Code

ewanharris and others added 6 commits October 14, 2026 12:29
… have

Alloy is silent on this. `widht: 10` in a `.tss` and `colr="red"` in a view
both compile without a warning, reach the generated controller verbatim and do
nothing at run time.

The question is one `@types/titanium` already answers, so there is no table of
properties here: `core/cascade.ts` says which rules reach which element and
what each sets, `ProjectService.membersOf` says what that element's type has,
and this is the comparison. A pure function of the views and rules it is
given, so the same call serves the server, a build and a test.

Three rules decide a finding, and each is a false positive otherwise. Measured
against tidev/kitchensink-v2 — 78 views, 999 rules, 567 elements — they take
it from 18 findings to 3, and all three stand up:

- Reported once per place it is written, not once per element it reaches. A
  `"Label"` rule in a view of nine labels is one authored mistake.
- A rule is judged against every element it styles together, so a class shared
  by a Label and a Window keeps `text` for one and `orientationModes` for the
  other. Unknown means unknown to all of them.
- An element the project has no types for silences the rule it is in. A native
  module's proxies are not in the package at all, and nothing is what we know.

Two kinds of element need Alloy's own rules rather than the types:

- Anything inside abstract markup is an entry of a property rather than a
  proxy. `<ButtonBar>` translates `<Labels>` to `BarItemTypes` and its children
  become the `labels` array, where `image` belongs to an entry and not to a
  Label. Twelve of the eighteen were this.
- `Ti.UI.Toolbar.js` hands `displayHomeAsUp` and its siblings to the activity
  when a Window names the toolbar as its ActionBar, and reads `customToolbar`
  off that Window. Alloy passes all of them to the proxy, so the cascade is
  right to report them and they are still not mistakes.

What survives on KitchenSink is one real bug — `mininum` for `minimum` on a
Stepper, so the minimum is never set — one property the package types as
`never` on `Ti.UI.Android.CardView`, and one it does not carry at all.

`isAbstractTag` is lifted out of `startsLocal`, which already had the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QY3KqD5KAhfTKWFNTiEFGB
Checking a stylesheet asks after the same few types once per element per
property. On tidev/kitchensink-v2 that is 4467 calls for 50 types in
app.tss alone, and each one rendered every member's type and documentation
from scratch: 3.9s for one file.

membersOf now answers from memory until the program changes, keyed on the
program itself so an edit that could change a type is never answered stale.
The list is shared, so it is read only.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4
…fix it

findingsInView checks a view's own attributes. findingsInStyle checks a
stylesheet against every view it styles at once, because a rule is judged
against all its elements together: app.tss against every view, widgets
included, and a theme no build selects against nothing.

Each check asks a type about its members once rather than once per
property. Even a source that remembers its answers has to find out whether
its program has changed first, and within one check it has not. Together
with the cache in the host, KitchenSink's app.tss takes 170ms rather than
3.9s, and every other file a few milliseconds. The findings are the same
three.

messageFor names the types as Titanium code writes them, and
replacementFor gives the edit for a suggestion: the last segment of the
name, inside any quotes, checked against the text rather than trusted.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4
Every open view and stylesheet is checked once typing stops for 300ms,
rather than only the one that changed: what a stylesheet gets wrong depends
on the views it styles, so an edit to one can add or take away a warning in
the other. A document that changed or closed while it was being checked is
left alone, since another check is already queued. Closing a document takes
its warnings back.

The quick fix is worked out again from the file rather than read back from
the diagnostic, because carrying data on a diagnostic needs a capability not
every client has. It names the warning it answers when the client sends it.

There is no setting to turn this off. The server has none by design (#14),
and every editor already has its own controls for a language server's
diagnostics.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4
Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4
DefinitelyTyped published no @types/titanium for SDK 10 or 11, so a project
on either resolves to 9.x. That is safe for completion and hover, which only
offer what the types know, and wrong for a check that a name exists: every
property those SDKs added is real and reads as unknown, and the quick fix
suggests a name that is wrong.

So a project whose types are an older major than its SDK is told why once in
the log and warned about nothing. Wherever the types came from, the project's
own node_modules included. A release that trails inside the SDK's own major
is still checked, since nearly every project is in that position and the
members a minor adds are few.

Every Alloy fixture was in that position itself, on SDK 10 or 12 against the
9.2.2 stub, so the tests of the warnings now lend the stub as a 10.x release,
and the stdio test uses a new fixture on SDK 9.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4
Copilot AI balanced review requested due to automatic review settings October 15, 2026 00:38

Copilot AI 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.

🟡 Changes recommended

Untyped targets and shared-rule aggregation produce false warnings, while nested-path handling suppresses valid findings.

4 open findings
What changed in this PR

Adds the server’s first diagnostics: warnings and spelling fixes for unknown properties in Alloy views and stylesheets.

Changes:

  • Adds property checking, nested-path analysis, and typo suggestions.
  • Publishes debounced diagnostics and offers quick fixes.
  • Caches type members and adds core, adapter, and protocol tests.
File Description
src/​test/​server/​service.test.ts Tests diagnostics and quick fixes.
src/​test/​server/​server.test.ts Tests warning publication over stdio.
src/​test/​server/​lsp-client.ts Adds notification waiting.
src/​test/​server/​fake-connection.ts Captures diagnostics and supports fix requests.
src/​test/​fixtures/​alloy-matched-project/​tiapp.xml Defines a matching-major SDK fixture.
src/​test/​fixtures/​alloy-matched-project/​node_modules/​@types/​titanium/​package.json Links fixture stub types.
src/​test/​fixtures/​alloy-matched-project/​app/​views/​index.xml Adds a minimal fixture view.
src/​test/​fixtures/​alloy-matched-project/​app/​styles/​app.tss Adds fixture global styles.
src/​test/​fixtures/​alloy-matched-project/​app/​config.json Defines fixture Alloy configuration.
src/​test/​core/​typescript/​versions.test.ts Tests SDK compatibility gating.
src/​test/​core/​typescript/​host.test.ts Tests member-cache reuse and invalidation.
src/​test/​core/​registry.test.ts Includes the new fixture.
src/​test/​core/​check.test.ts Tests property checking and replacements.
src/​server/​service.ts Schedules diagnostics and builds quick fixes.
src/​server/​convert.ts Converts findings into protocol warnings.
src/​core/​view.ts Allows readonly member results.
src/​core/​typescript/​versions.ts Adds types-to-SDK compatibility checks.
src/​core/​typescript/​host.ts Caches members per TypeScript program.
src/​core/​tags.ts Exposes abstract-tag detection.
src/​core/​check.ts Implements property analysis and suggestions.
README.md Documents warnings and limitations.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/core/check.ts Outdated
Comment thread src/core/check.ts Outdated
Comment thread src/core/check.ts Outdated
Comment thread src/core/check.ts Outdated
…ually says

Review on #67 found four ways a rule shared between elements, or a nested
property, was judged wrongly:

- <Require> and <Widget> have no Titanium type, so the cascade leaves them
  out, and a class they share with a Label was checked against the Label
  alone. They are now in the rule's reach and, like a native module, silence
  it.
- <Module module="ti.map"> resolves to Ti.UI.View, and so does any tag with a
  module attribute, though a native module creates them. Both are now opaque.
  An ns attribute is not: ns="Ti.UI.iOS" names a Titanium type, and an ns
  that names a module already resolves to a type the project does not have.
- A property Alloy reads for itself returned early, so it never counted as
  known, and a class shared by a Toolbar and a Label was flagged for
  displayHomeAsUp. It is now known for the rule.
- An empty answer for a nested path meant opaque, so a Window having no font
  hid a Label's fntSize, and a misspelt outer key such as fnt was never
  reported. Each type now walks the path and answers known, unknown at a
  depth, unanswerable (a value that is not an object) or opaque, and only
  opaque silences a rule. A misspelt outer key is reported once, on that key.

KitchenSink is unchanged: the same three findings, and app.tss in 170ms.

Refs #66

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AAKDFVwAZoxefLNvX45pE4
@ewanharris
ewanharris merged commit 665c239 into main Oct 15, 2026
24 checks passed
@ewanharris
ewanharris deleted the claude/issue-17-handoff-review-gol36j branch October 15, 2026 19:37
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.

Diagnostics: flag stylesheet and view properties the element doesn't have

3 participants