No rule matches that.
Version and deprecation
Portability against the UI5 version your system actually runs (--ui5, default 1.71) and the distribution it serves (--distribution).
an aggregation TAG introduced after your target version
The same mistake as member-too-new, with a far worse blast radius, which is why it is its own rule and an error. A post-floor property is dropped silently and the control still renders; a post-floor aggregation is a lowercase tag the release does not know, so UI5 falls back to resolving it as a control class and 404s on sap/<lib>/<name>.js — the whole view fails to load, not just the part below the tag. <footer> on a sap.m.Dialog is the recurring case: the public footer aggregation is ~1.110, so on 1.71 UI5 requests sap/m/footer.js. Use buttons (1.21.1); UI5 lays them out in an overflow toolbar by itself.
view->ele( `Dialog` )->ele( `footer` ) " @since 1.110 on a 1.71 target
a URL pinned to the commercial SAPUI5 host
The same portability family as sapui5-only-control: sdk.openui5.org serves the open distribution, ui5.sap.com / *.hana.ondemand.com serve SAPUI5. An app whose assets or bootstrap point at the commercial host breaks the moment it runs against an OpenUI5-only landscape.
control already deprecated at your target version
Reported only once the deprecation is in effect at the version you target — a control deprecated as of 1.149 stays silent for a 1.71 target.
control introduced after your target UI5 version
The control does not exist on the system you are targeting (--ui5, default 1.71), so the view will not load there — however well it works on a newer one. Waive a deliberate case with --allow sap.m.Control.
an enum VALUE introduced after your target version
The property predates version tracking, the type is old — but the specific value is not: sap.m.ButtonType.Critical is @since 1.73 on a property that existed forever. The member-level @since sits on the property, never on the value, so this was invisible to every gate (and a documented manual-check burden downstream). The snapshot now keeps the per-value @since from the enum's JSDoc; a value without one predates version tracking and stays silent. Waive a deliberate case with --allow sap.m.Control.member, like the other floor rules.
view->tag( `Button` )->a( n = `type` v = `Critical` ) " @since 1.73 on a 1.71 target
a ${$parameters>/name} the event only gained later
An event parameter read back in a t_arg that the event did not carry yet at your floor. Resolved per event, not per parameter name, so an identically named parameter on another event does not mask it.
an icon that left the font again
The font is nearly, but not quite, additive. binary was in the font for exactly one release — 1.104 — and gone again after it; the glyph is spelled non-binary (in the font since 1.96) everywhere else. A view written against that one release renders no icon on every release after it. Since a target version is a floor, the app also runs on the releases where the name is gone, which is why this is reported independently of the target.
an icon that reached the font after your target version
The same silence as unknown-icon, one release boundary later: the glyph exists today and did not exist on the release you target, so the control renders without an icon there and nowhere else. information arrived in 1.80 (use message-information) and clear-all in 1.86 (use eraser) — both shipped past a green CI and were found by a user on 1.71. The registry's floor is 1.71, so a name already present there is recorded as "at or before" and a target below 1.71 is never judged.
view->tag( `Button` )->a( n = `icon` v = `sap-icon://information` ) " @since 1.80 on a 1.71 target
property or event already deprecated at your target version
As control-deprecated, per member. The replacement is usually named in the deprecation text the finding quotes.
property, event or aggregation introduced after your target version
Same as control-too-new, one level down. Members without an @since count as always available — they predate version tracking. Waive with --allow sap.m.Control.member.
needs SAPUI5, absent from OpenUI5
Only with --distribution openui5. SAPUI5 ships libraries OpenUI5 does not — sap.ui.comp (Smart controls), sap.suite.*, sap.ushell, sap.fe, sap.viz — so a SmartTable is fine on SAPUI5 and a guaranteed runtime error on OpenUI5.
ToolbarSpacer/ToolbarSeparator inside a sap.m.Bar
The nastiest kind of version defect: every control and every property in the view exists on the target release, and it still renders wrong — only the CSS changed. ToolbarSpacer and ToolbarSeparator render a <div> and are laid out as intended only inside a sap.m.Toolbar, which is a flex container. sap.m.Bar is not, before 1.76: .sapMBarLeft/.sapMBarRight were position: absolute + text-align with the children in normal flow, where a block-level child starts a new line — and .sapMBarContainer { overflow: hidden } at the bar's 3rem height cuts away everything from that line on. So a separator between two groups of icons does not draw a rule: it deletes every icon after it, without a word. And a Bar is rarely written on purpose — sap.m.Page headerContent is forwarded into the internal Bar's contentRight, which is how this reached three overview headers at once. The fix is not a different separator, it is no separator: put only inline controls in a bar and express the grouping with a margin class (sapUiMediumMarginBegin on the first control of the next group). Reported only for a target below 1.76.
view->ele( `headerContent` )->tag( `ToolbarSeparator` )
sap-icon://textFormatting — a glyph the font has in no release
An unknown icon name is not an error anywhere: IconPool resolves it at render time, finds nothing, and the control renders with no icon at all. Nothing is logged — not in the browser console, not in ui5lint, not in abaplint — so it ships and surfaces months later as "not all icons are shown". Case matters and not in the way it looks: IconPool.getIconInfo parses the URI and reads parts.hostname, which is lower-cased, so textFormatting is not "nearly right" — it matches nothing, in every release, forever. The name is text-formatting. Judged against data/icons.json, the icon registry of every OpenUI5 minor from 1.71 up; a collection-qualified name (sap-icon://tnt/actor) belongs to a custom font and is never judged.
view->tag( `Button` )->a( n = `icon` v = `sap-icon://textFormatting` )
abap2UI5 semantics
The defects that stay silent at runtime, because they live in the relationship between the ABAP class and the view it builds. No UI5 tooling can see them.
_bind( ) on an event — a dead control
The slot is an event, so a data binding in it never becomes a handler. The control renders and does nothing. Use client->_event( ).
a binding written into an association attribute
Only properties and aggregations can be data-bound. XMLTemplateProcessor handles an association attribute by taking its value verbatim as a control ID (createId(sValue), or a comma/space split for a 0..n association) — BindingInfo.parse is never called on it. So the braces travel into an id nothing answers to, the association stays empty, and neither the parser, the render gate nor the console says a word. Drive an association imperatively instead, with a CONTROL_BY_ID setter — which is also why settable-property-via-action deliberately never pushes an association towards a binding.
a( n = `selectedSection` v = client->_bind( section ) ) " association -> id "{/SECTION}"
" follow_up_action( … t_arg = VALUE #( ( `opl` ) ( `setSelectedSection` ) ( section ) ) )
a local variable bound
The instance is serialized across the roundtrip, the method stack is not — so the value is gone when the answer comes back. Bind an instance attribute.
a PROTECTED/PRIVATE attribute bound
Only PUBLIC attributes are serialized into the model (z2ui5_cl_ui5_srv_model filters on visibility), so binding one from another section fails the first roundtrip with BINDING_ERROR — No class attribute for binding found. Move the attribute to the PUBLIC SECTION. Judged by the root of the bound name (a structure component travels with its root), and only when the class declares a PUBLIC SECTION to compare against. Found live: an samples-controls port bound expanded from its PROTECTED section and had never worked in a running system — its LIVE_TEST deviation was telling the truth the whole time.
PROTECTED SECTION.
DATA expanded TYPE abap_bool. " BINDING_ERROR on the first roundtrip
" … )->a( n = `expanded` v = client->_bind( expanded ) )
a TYPE REF TO attribute bound without dereferencing
The model serializer walks DATA, not references — client->_bind( ) on an attribute declared TYPE REF TO … throws at runtime. Bind the dereferenced data (client->_bind( ref->* )) or a plain data attribute. Both sample fixes that established this pattern were found by users hitting the exception in a running system.
DATA mt_data TYPE REF TO data.
" …
)->a( n = `items` v = client->_bind( mt_data ) " throws
)->a( n = `items` v = client->_bind( mt_data->* ) " binds the table
several controls on one line of a multi-line chain
One element per line is what makes the indentation able to show the tree at all — a line holding three controls hides three levels of it. Only ELEMENTS count: an attribute on the same line as the control it belongs to hides nothing, so )->tag( Text )->a( n = text v = {TITLE} ) is the compact one-control form half the samples (and abap2UI5's own startup app) are written in and is never reported. Closing calls do not count either, because )->end( )->end( ). as a chain's last line is an established ending. Reported only for a chain already written across several lines, so the one-liner stays available. A hint, like its neighbour.
)->tag( `Input` )->tag( `Button` )->tag( `Text` ) " three controls, one line
a chain not in the abap2UI5 house layout (opt-in)
The only rule here that encodes a HOUSE STYLE rather than an inconsistency, and the only one that names a step. Its two neighbours judge a chain against itself and stay silent on any layout that is merely a choice; this one judges it against one canonical form: one call per line including attributes (stricter than chain-element-per-line, which lets an attribute share its control's line), four spaces per level of the tree, and the closing call alone in the column of the element it closes. It exists because that form now has a corpus behind it — abap2UI5, abap2UI5/samples and abap2UI5/samples-controls were unified onto it in 2026-08, and the drift it catches had passed every other gate: 77 ports whose whole chain sat one level too deep, which chain-indentation cannot see because a uniformly wrong rhythm is still a rhythm. If your house style is a different one, switch it off — "chain-house-layout": false — rather than reformatting to somebody else's taste. Every finding carries fixes, so --fix rewrites the chain; the rewrite only ever touches whitespace between chain segments and the indent of a continuation line that is not itself content, so it cannot change what the view builds.
--fix: The chain is rewritten into the canonical layout: each call moved onto its own line at the column its depth in the tree gives it, and the continuation lines of a call's arguments moved with it. Only whitespace BETWEEN chain segments is touched — a literal, a comment's text and the inside of an argument list are copied through, and blank lines and comment lines between segments are kept where they are. The rewrite is checked to be whitespace-only before it is offered, so it cannot change what the view builds.
)->ele( `Shell` )->ele( `Page` ) " two levels on one line, and the step is 0
)->tag( `Text` )->a( n = `text` v = `x` ) " attribute on its control's line
a builder call whose indentation contradicts the tree it builds
A builder chain is the one part of an abap2UI5 class that nothing else formats: abaplint has indentation and in_statement_indentation switched off (a chain is a single statement spanning fifty lines), so neither abaplint --fix nor the auto-format workflow ever touches its inner lines. And the reader has no other picture of the view — the XML the builder emits is one long line by construction, so the ABAP indentation IS the view's structure. When it drifts, the tree in the file stops matching the tree in the browser. What is judged is only that: a sibling written at a different column than the siblings it shares a parent with, or a call written to the LEFT of the element it belongs to. The SIZE of the indent step is not judged — two and four are both house styles in the wild, and the first child under a node defines the column its siblings are held to, so a chain that keeps its own rhythm is never reported. Neither is the column of end( ) (the hanging close is established), nor v = alignment, nor line length, nor a chain written entirely on one line. A hint: the view renders identically either way.
)->ele( `Page`
)->tag( `Input`
)->tag( `Button` " a sibling of Input, written a level out
an escaped CSS brace written inside a |…| template
An escape only reaches the serialized attribute when the backslash is taken verbatim, which is what a backtick literal does. Inside an ABAP string template the backslash is the template's own escape: \{ collapses to a bare { before the builder ever sees it, and the view dies exactly as if nothing had been escaped. Write the stylesheet in a backtick literal — or, if it has to be a template, double the backslash (\\\{). Invisible to unescaped-brace-in-style, which reads the source and sees a backslash in front of every brace; only the literal's kind tells the two apart.
DATA(css) = |<style>.a \{color:red\}</style>|. " collapses
DATA(ok) = `<style>.a \{color:red\}</style>`. " survives
a CONTROL_BY_ID wire naming a method the frontend denylist refuses
The wire's ALLOWED side is open by design — any public control method runs, so ordinary setters and toggles need no whitelist entry. The DENIED side is a closed set, and it is silent in the same way a wrong id is: FrontendAction logs "method not allowed" and returns, so the ABAP compiles, the view renders and the button does nothing. Denied are the methods that would break the framework's own invariants — teardown and reparenting (destroy, exit, setParent, addDependent, placeAt), model and binding swaps (setModel, setBinding*, bind*/unbind*), event-handler tampering (attach*/detach*, fireEvent), the render lifecycle (rerender, invalidate) and the GENERIC reflection mutators that take the member name as an argument (addAggregation, removeAllAggregation, setAssociation, …). The NAMED per-aggregation methods are allowed and are never reported: removeAllItems and destroyContent touch only children the control itself owns, exactly like the long-allowed removeItem.
follow_up_action( val = client->cs_event-control_by_id
t_arg = VALUE #( ( `list` ) ( `destroy` ) ) )
the same FOR iterator name twice in one method
Fine on ABAP 7.50+, where the iterator is local to its VALUE #( ) expression — but a 7.02 downport (abaplint --fix, and the transpiler behind a Node-based runtime) materializes each one as DATA <name> TYPE i in the method body, and the second declaration fails activation with "variable already defined". Use distinct names (i, j, k) per VALUE block.
a binding written with escaped braces inside a backtick literal
Brace escaping is a |…| TEMPLATE rule. In a template the backslash is ABAP's own escape, so \{ reaches the builder as a bare { — which is exactly what a binding needs. A backtick literal has no escape processing at all: the backslash is taken verbatim and lands in the serialized attribute, where UI5 reads \{ path: … \} as text rather than a binding and either renders it raw or fails to parse the view. Write plain braces in a backtick literal; keep the escapes for the template form.
a( n = `items` v = `\{ path: 'message>/' \}` ) " the backslash reaches the XML
a( n = `items` v = `{ path: 'message>/' }` ) " correct in a backtick literal
a( n = `items` v = |\{ path: '{ p }' \}| ) " correct in a template
get_event_arg( n ) past the t_arg the event declares
The arguments are static — they are written at the raise site — so reading past them is never anything but a mistake: initial in ABAP, a 500 in the transpiled runtime, and either way not the value the handler works with. Judged only for a literal index, inside the handler of an event the class raises itself with client->_event( ), and never across a method boundary: an event arriving from a message_box_display( onclose = ) callback or a frontend action carries arguments from a source this pass cannot see.
client->_event( val = `PICK` t_arg = VALUE #( ( `${$source>/id}` ) ) )
" … WHEN `PICK`. client->get_event_arg( 2 )
a bare-brace t_arg literal (` {COL} `)
The runtime sends it verbatim, but only $-prefixed expressions are resolved by UI5 — so get_event_arg( ) receives an empty value, with no error anywhere. Write ` ${COL} . A template that starts with a {0}` placeholder is fine: that form is quoted.
--fix: The missing $ is inserted in the literal form; a |…| template is left alone.
_event( ) on a property
The mirror image: an event handler written into a data slot. Use client->_bind( ).
an event handler on a control hard-disabled with a literal
A bound enabled can flip at runtime, but a literal enabled="false" never does — the control can never fire, so the handler wired next to it is dead code that reads like a live wire. A hint, because a 1:1 port of a sample demonstrating the disabled *state* legitimately carries the original's handler. Bind enabled if it should ever flip.
)->tag( `Button`
)->a( n = `press` v = client->_event( `SAVE` )
)->a( n = `enabled` v = `false` " SAVE can never fire
an event nothing reacts to
Usually a dead control — but in abap2UI5 an event also forces a roundtrip, and that alone synchronises the model back into ABAP. So this is a hint, never an error, and it is skipped entirely when handler names are not literals.
an id-addressed wire naming an id no view declares
The frontend resolves the first t_arg as a control id. When no view of the class gives a control that id — a typo, a renamed control, an id that only ever existed in another app — the lookup fails and the wire does nothing: no exception, no failed render, a button that looks connected. CONTROL_BY_ID at least logs the miss; SET_FOCUS, SCROLL_TO, SCROLL_INTO_VIEW and KEYBOARD_SET_MODE return without even a console line. Judged only when every id attribute of the class is a literal; a class that builds ids at runtime is left alone.
a( n = `id` v = `messageView` )
" … follow_up_action( val = client->cs_event-control_by_id t_arg = VALUE #( ( `messageview` ) ( `navigateBack` ) ) )
the class builds its view with the frozen z2ui5_cl_xml_view, so the view was not checked at all
This is the only finding here about what was *not* judged. z2ui5_cl_xml_view was the abap2UI5 view builder until z2ui5_cl_ui5_view_builder replaced it, and it moved into the frozen src/99 rather than being deleted — so a class written on it still compiles and still renders, and nothing anywhere raises an eyebrow. This linter reconstructs a view from the current builder's five verbs (ele, tag, a, end, stringify); the old API is a different one, so there is no view to judge and every other rule is silent for lack of anything to read. Until this rule existed the file was not even collected: an entire app on the retired builder came back as "no checkable app classes" and exit 0, which reads like approval. It matters more than it used to, because the old API is what almost all public abap2UI5 material shows and therefore what a language model reproduces when asked to write an app — the wrong answer arrives already looking like the right one. Rewrite the chain on z2ui5_cl_ui5_view_builder and the whole gate applies again. An app that is deliberately staying on the old builder for now says so once, in its config: "rules": { "frozen-view-builder": "warning" }.
DATA(view) = z2ui5_cl_xml_view=>factory( ). " frozen — nothing below this line is checked
view->page( )->button( text = `hi` ).
client->get( )-viewname — removed from ty_s_get
The VIEWNAME component was removed from z2ui5_if_types=>ty_s_get (it always carried an empty string), so the read no longer compiles — but nothing in a systemless pipeline says so before activation: abaplint has no signature knowledge of the framework interfaces, and the render gate never sees the class fail. The same blindness popover-display-val covers.
DATA(viewname) = client->get( )-viewname. " no longer compiles
an absolute binding path written as text — {/PATH} or path: '/PATH'
The runtime only registers what client->_bind( ) was given, so a textual path either addresses nothing (that half is unknown-binding-path) or duplicates a bind that exists elsewhere — and then silently breaks the moment the attribute is renamed, because no compiler follows a string. Derive the path instead: client->_bind( var ) for the {binding}, or the bare-path form client->_bind( val = var path = abap_true ) interpolated into a binding-info template. An OData entity path with a key predicate ({/Products('4711')}) in a class that switches its default model to an OData service is exempt — that path addresses the service, not an ABAP variable.
a( n = `title` v = `{/TITLE}` ) " breaks on rename
a( n = `title` v = client->_bind( title ) ) " moves with the variable
a JSON action payload the runtime silently downgrades
The object-kind control methods (setSticky, setHiddenInPopin, setP13nData) run their argument through castArg, whose catch turns a literal that does not parse as JSON into {} — a setSticky with a typo'd payload then *un-sticks* everything instead of failing. For the enum-array payloads the values are judged too: an unknown sap.m.Sticky / sap.ui.core.Priority key is dropped by UI5 with the same silence. BINDING_CALL's compound filter-groups JSON is judged the same way, including each row's operator.
t_arg = VALUE #( ( `table1` ) ( `setSticky` ) ( `ColumnHeaders` ) ) " not JSON -> {}
t_arg = VALUE #( ( `table1` ) ( `setSticky` ) ( `["ColumnHeaders"]` ) )
a frontend-action t_arg outside the set the runtime accepts
A client->_event_client( ) / client->follow_up_action( ) wire is dispatched in the browser by name, and a name outside the whitelist raises nothing anywhere: FrontendAction logs to the console and the control does nothing when pressed. Judged only for literal arguments and only where the runtime's set is closed — the CONTROL_GLOBAL object and its method, the BINDING_CALL method, and CONTROL_BY_ID's obsolete empty view slot (which shifts the method out of position). CONTROL_BY_ID's method list is open by design and is never judged.
client->_event_client( val = client->cs_event-control_global
t_arg = VALUE #( ( `MESSAGE_TOASTER` ) ( `show` ) ( `hi` ) ) )
a shortcut combo that names no key
The registration normalizes the combo (Ctrl+Shift+S → ctrl+shift+s, aliases like cmd/return included) and refuses one that consists of modifiers only — logged once, never registered, and every later keydown simply does nothing. The scope argument is judged separately: a slot key or a declared control id (via frontend-action-unknown-id).
t_arg = VALUE #( ( `Ctrl+Shift` ) ( `SAVE` ) ) " modifiers only — binds nothing
t_arg = VALUE #( ( `Ctrl+Shift+S` ) ( `SAVE` ) )
a json = abap_true bind on a scalar-typed property
_bind( json = abap_true ) splices the bound string into the model as a JSON node — built for properties typed object/any (an integration Card's manifest), which no typed ABAP value can be. On a string/int/float/boolean property the spliced node arrives as the wrong JSON type — strict mode and UI5 2.x reject it — and the splice is outbound-only: the return path skips json attributes, so an edit made through a two-way binding is silently discarded on the next roundtrip. Bind the plain attribute instead; json is for objects.
DATA manifest TYPE string. " contains JSON
)->a( n = `value` v = client->_bind( val = manifest json = abap_true ) " Input.value is string-typed
a raw JSON literal written into a view attribute
UI5 parses an attribute value starting with { as a binding, so a JSON object literal ({"sap.card":…) is read as a binding path and the attribute ends up empty — the classic way to lose an integration Card's manifest. Keep the JSON in the model and bind it: client->_bind( manifest ).
a liveChange wire that round-trips per keystroke
abap2UI5 serializes round-trips: an event fired while one is in flight is dropped, not queued. A liveChange wired to client->_event( ) therefore sees the value of the last *completed* trip and skips the ones typed in between — the bound field lags under fast input and converges only when typing pauses. Prefer a two-way binding (the model updates without any event) or the control's final-value event (change/search/submit); keep the live wire only when every intermediate value genuinely must reach ABAP. _event_client and follow_up_action are frontend-only and are not judged.
)->a( n = `liveChange` v = client->_event( `SEARCH` ) " lossy under fast typing
" instead: bind two-way and react to the final-value event
)->a( n = `value` v = client->_bind( search_term )
)->a( n = `change` v = client->_event( `SEARCH` )
a hand-rolled init flag instead of client->check_on_init( )
The framework already knows whether this is the first run of the app instance — client->check_on_init( ) is the lifecycle contract. A boolean attribute that gates the first render duplicates that knowledge as serialized state: it ships to the browser on every roundtrip for nothing, and subtle ordering bugs grow around the moment it flips. One mass migration replaced this pattern in 111 sample classes at once. Only the unambiguous shape is reported: an IF on the attribute being initial/false whose branch both sets it true and hands a view over — a lazy-load guard that displays nothing is left alone.
IF check_initialized = abap_false. " reported
check_initialized = abap_true.
client->view_display( render( ) ).
ENDIF.
" instead:
IF client->check_on_init( ).
client->view_display( render( ) ).
ENDIF.
a lifecycle dispatcher with no check_on_navigated( ) branch at all
check_on_init( ) means "this app INSTANCE never ran", not "the app starts" — abap2UI5 flips the flag after the very first roundtrip. So it is false on three roundtrips that put the app back on screen: a called app leaving through nav_app_leave( ), one of the built-in z2ui5_cl_pop_* value helps returning (those run over nav_app_call too), and a bookmarked draft being restored. All three raise check_on_navigated( ) alone; with no branch for it main( ) does nothing, the response carries no display, and the model is pushed into a MAIN slot still holding the other app's view. The screen stays wrong with no error anywhere — which is why an app written this way works perfectly until the day something navigates into it. This is the complement of missing-view-display-on-navigated, which judges a branch that exists but never displays; the two never fire on the same class. An app whose display is not gated by the lifecycle at all — a view_display( ) after the IF/ELSEIF chain, or the client->nav_app_leave( ) a popup helper ends on — is correct as it stands and is not reported.
IF client->check_on_init( ).
model_init( ).
view_display( ).
ELSEIF client->check_on_navigated( ). " without this the app goes blank after a hop
view_display( ).
ELSEIF client->check_on_event( ).
on_event( ).
ENDIF.
a check_on_navigated( ) branch that never re-displays the view
When a called app leaves, the browser still shows THAT app's view — returning control alone changes nothing on screen. The check_on_navigated( ) branch has to hand a view back with client->view_display( ). A branch that only reads the result and falls through leaves the screen showing the wrong app, with no error anywhere. view_model_update( ) used to count as a re-display here and no longer does: it is an empty method now (obsolete-model-update), and the automatic model push that replaced it reaches the MAIN slot — which is still holding the called app's view.
ELSEIF client->check_on_navigated( ).
result = client->get_app( client->get( )-s_draft-id ).
client->view_display( render_view( ) ). " without this: the sub-app stays on screen
an abap2UI5 object outside the released src/02 package
abap2UI5 releases exactly one package — src/02, six objects: z2ui5_if_app, z2ui5_if_client, z2ui5_if_exit, z2ui5_if_types, z2ui5_cl_ui5_http_handler, z2ui5_cl_ui5_view_builder. Everything else the repository ships says in its own package description that it is not for consumers: src/01 is "abap2UI5 — internal use only", src/99 is frozen legacy that "ships solely so existing downstream installations keep compiling", and src/00 holds renamed copies of AJSON, S-RTTI and abap-util. None of them carries a compatibility promise or announces a change: one upstream commit renamed the entire core layer (z2ui5_cl_core_* → z2ui5_cl_ui5_*) and moved the old view builder and HTTP handler into the frozen package on the same day. An app that names one of those compiles today and fails to activate after the next abapGit pull, with no deprecation in between — and nothing in a systemless pipeline says so beforehand. Judged only against names the linter knows are framework objects (the frozen package by name, the internal packages by the prefixes upstream reserves), so your own z2ui5_-prefixed classes are never reported. z2ui5_if_types is released rather than merely tolerated, which matters because the released z2ui5_if_client~get( ) returns z2ui5_if_types=>ty_s_get — an app that declares a variable of that type cannot avoid the name.
DATA(json) = z2ui5_cl_ajson=>create_empty( ). " vendored copy, renamed on the next sync
z2ui5_cl_pop_to_confirm=>factory( ). " frozen — use the popups addon
DATA(html) = z2ui5_cl_util=>xml_stringify( data ). " retired utility class
client->_bind_edit( ) — superseded by client->_bind( )
_bind is two-way as well, and _bind_edit is a pure alias for it. A call passing custom_mapper_back or custom_filter_back used to be exempt, because _bind has no such parameters — that exemption is gone with the parameters' meaning: they are still accepted for source compatibility but no longer evaluated, per-direction mapping does not exist any more. Such a call is reported like every other, but without the autofix: the arguments have to go with the rename, and dropping an argument is not a rename.
--fix: Rewritten to client->_bind( ), the arguments untouched — except where the call passes custom_mapper_back/custom_filter_back, which is reported without a fix.
a( n = `value` v = client->_bind_edit( name ) ) " → client->_bind( name )
client->_event_client( ) — superseded by client->follow_up_action( )
The same call, with the same val / view / t_arg. Since follow_up_action( ) gained a RETURNING parameter it is the same call in the same *position* too: where its result is consumed — the view-attribute form v = client->_event_client( … ) — it takes the IF result IS SUPPLIED branch straight to mo_srv_event->get_event_client( ), which is _event_client( )'s entire body. One method now both schedules a frontend action and wires one, so the second name is a leftover. The one non-equivalence is follow_up_action( )'s CASE, which intercepts cs_event-set_nav_routing / set_push_state / set_app_state_active before that branch: those three are backend-side navigation options, not frontend handlers, so a view attribute wired to one of them never dispatched anyway.
--fix: Rewritten to client->follow_up_action( ), the arguments untouched.
a( n = `press` v = client->_event_client( val = client->cs_event-popup_close ) )
a( n = `press` v = client->follow_up_action( val = client->cs_event-popup_close ) )
view_model_update( ) & friends — empty methods, the model is pushed automatically
The framework compares the model state before main( ) with the state after it returned and, when they differ, sends it to every open view slot by itself. view_model_update( ), nest_view_model_update( ), nest2_view_model_update( ), popup_model_update( ) and popover_model_update( ) are therefore deliberately empty methods, kept in z2ui5_if_client only so existing apps keep compiling. A leftover call is not merely dead weight — it reads as "the model is pushed here" at a place where nothing at all happens. Delete it. The one thing that went with them is the ability to force an *unchanged* model back onto the client (a control that wrote a bound property without sending it back): rebuild the view with view_display( ) for that.
--fix: The call is deleted, together with the line when it has that line to itself; a line shared with other code or a trailing comment keeps everything but the call.
client->popup_model_update( ). " does nothing — delete it
popover_display( by_id = … ) anchored to an id no view declares
A popover opens by a control — by_id names its anchor. With a literal id no view of the class declares, the fragment loads, displayPopover finds no openBy control, logs it and destroys the fragment again: nothing opens, nothing renders red, and the property gate saw a perfectly valid fragment. Judged under the same trust condition as frontend-action-unknown-id — only when every id attribute of the class is a literal.
)->a( n = `id` v = `btnInfo` )
" …
client->popover_display( xml = popover->stringify( ) by_id = `btninfo` ).
popover_display( val = … ) — the parameter is xml
The one asymmetry in the display family: popup_display( ) imports val, popover_display( ) imports xml. A val = guessed by analogy does not compile — but nothing in a systemless pipeline says so before activation, so the mistake rides along until the class first meets a compiler. One of the most common first-try mistakes in generated code.
--fix: The parameter name is rewritten to xml, the argument untouched.
client->popover_display( val = popover->stringify( ) ). " does not compile
client->popover_display( xml = popover->stringify( ) ). " correct
raw JavaScript shipped to the browser — via follow_up_action or the view
abap2UI5's frontend is a renderer: behaviour travels as data (bindings, cs_event- actions), never as code. Three shapes break that line, and all three run unchecked in the browser, invisible to every gate and to anyone reading the ABAP: a non-name val in follow_up_action( ) (the raw-JS escape hatch — inserted verbatim as custom_js), a hand-written handler string on an event attribute (UI5 evaluates it as JavaScript), and a <script> tag inside an attribute value (the core:HTML route). Use a cs_event- frontend action, a client->_event*( ) wire or backend logic instead. A repo that deliberately allows the escape hatch can lower or disable the rule in its abap2ui5lint.jsonc.
client->follow_up_action( val = `sap.ui.getCore().byId('x').focus()` ). " raw JS
)->a( n = `press` v = `z2ui5.oView.doSomething()` ) " handler string
" instead:
client->follow_up_action( val = client->cs_event-set_focus t_arg = VALUE #( ( `x` ) ) ).
lifecycle checks in separate IF blocks instead of one IF/ELSEIF chain
The lifecycle flags (check_on_init, check_on_event, check_on_navigated, …) are not all mutually exclusive, so separate IF blocks can execute more than one branch on a single roundtrip — the classic symptom is work done twice after a navigation. One IF/ELSEIF chain makes the branches exclusive by construction. The guard idiom is exclusive too and is never reported: an IF block that leaves the method (IF client->check_on_event( \GO\ ). … RETURN. ENDIF.) cannot flow into the next block.
IF client->check_on_init( ).
" …
ELSEIF client->check_on_navigated( ). " ELSEIF, not a second IF
" …
ENDIF.
a CONTROL_BY_ID set…( ) where the control has a bindable property of that name
The project rule is *prefer a bindable property over a frontend action*: a two-way bound property keeps the state in the model, where it survives a view rebuild, a draft restore and the browser Back button — a frontend action does not, and it also needs a round-trip to be re-applied. Only properties are reported: an association (sap.uxap.ObjectPageLayout.selectedSection) and an aggregation cannot be data-bound at all, so driving those imperatively is the only way and is never flagged. A hint, not an error — an imperative call can still be the right answer when the sample's point is the imperative API itself.
follow_up_action( val = client->cs_event-control_by_id
t_arg = VALUE #( ( `sideContent` ) ( `setShowSideContent` ) ( `true` ) ) )
" -> a( n = `showSideContent` v = client->_bind( show_side ) )
the last t_arg entry is empty and never arrives
get_t_arg buffers an empty argument and flushes it only when a later non-empty one follows, so an empty entry between filled ones keeps its slot and a TRAILING one disappears. The handler's get_event_arg( n ) for that position reads initial, with no error anywhere. The framework pads a missing trailing argument only for a nullable declared kind on a control method, which does not apply to a backend _event.
mProperties & friends — private UI5 internals
The mProperties/mAggregations/mBindingInfos/mEventRegistry member tables are UI5 implementation details with no API contract — they are renamed or restructured across UI5 patches without notice, so a wire or expression that reads them works on the version it was written against and breaks silently on the next one. Restructure to a two-way binding or a public parameter.
an ABAP boolean written straight into the view
It arrives as 'X' or ' ', and UI5 reads any non-empty string as true — so visible = abap_false makes the control visible. The classic silent inversion. The way out is the builder's own boolean parameter: z2ui5_cl_ui5_view_builder takes the flag through a( b = … ), which renders true/false itself.
--fix: A bare token is moved onto the boolean parameter — a( v = flag ) becomes a( b = flag ); an expression is left alone.
literal CSS braces in a <style> block
UI5's XMLView parser reads an unescaped { in an attribute value as the start of a binding, so a stylesheet injected through a core:HTML content attribute takes the whole view down with a binding parse error. Write every brace as \{ and \}. Judged between <style> and </style>, so a {0} toast template or a ${$parameters>/…} wire elsewhere in the same builder chain is never mistaken for CSS.
DATA(css) = `<style>.box \{color:red\}</style>`.
a hand-written {/TYPO} the derived model has no path for
The field just stays empty — no error, anywhere. Inside a bound aggregation a relative {TYPO} is resolved against the row, so a misspelled column field is caught too, but only where the row shape is known from the class's TYPES. Never guessed.
a literal action name outside the frontend dispatch table
A client->_event_client( ) / client->follow_up_action( ) naming its action as a string literal is dispatched exactly like the cs_event- constant — but the constant is compile-checked and the literal is not, and FrontendAction.execute looks the name up in its handler table and does nothing at all on a miss: no exception, not even a console line. Case matters — the runtime never upper-cases, so set_title misses where SET_TITLE works. Anything not name-shaped is follow_up_action's raw-JavaScript escape hatch and is not judged.
client->follow_up_action( val = `SET_TITEL` t_arg = VALUE #( ( `Hi` ) ) ). " swallowed silently
" instead:
client->follow_up_action( val = client->cs_event-set_title t_arg = VALUE #( ( `Hi` ) ) ).
a name> binding against a model the app does not have
abap2UI5 serves exactly one data model per view slot — the default one, serialized from the class's PUBLIC attributes — plus the framework's own device> and message> (and http> on a switched path). A prefix outside that set resolves to no model at all, and UI5 leaves the property unset without a word. It is the most common leftover of a ported demo-kit sample, whose original names its models freely ({ui>/rowMode}, {i18n>KEY}): the fix is to fold the field into the default model with client->_bind( ), not to add a model — and there is no i18n model by design, because translation is a backend concern. A model registered by a SET_ODATA_MODEL wire of the same class counts as available; a class that registers one under a non-literal name is not judged at all.
a( n = `text` v = `{i18n>title}` )
a( n = `text` v = client->_bind( title ) )
a literal view slot outside MAIN / NEST / NEST2 / POPUP / POPOVER
The view parameter (and SET_SIZE_LIMIT's view key) names one of the five slots, case-sensitively: the server compares it as an ABAP string and the browser uses it as an object key. The natural guesses all miss — main (lower case), and NESTED for cs_view-nested, whose VALUE is NEST. For CONTROL_BY_ID a wrong slot is worse than none: a named slot suppresses the global id fallback, so the wire dies although the id exists in an open view.
client->_event_client( val = client->cs_event-control_by_id
view = `NESTED` " cs_view-nested is NEST
t_arg = VALUE #( ( `table1` ) ( `focus` ) ) ).
a PUBLIC attribute nothing in the class ever touches
Only PUBLIC attributes are serialized into the model (z2ui5_cl_ui5_srv_model filters on visibility), so every one of them is shipped to the browser on every roundtrip. One that is never bound, never read and never written is pure transport weight. Deliberately narrower than "not bound in any view": an attribute used only in ABAP code is not dead, it is *state* — PUBLIC is precisely how a value survives the roundtrip. Only a name that appears exactly once in the whole class, its own declaration, is reported, and only as a hint: an attribute can still be read from outside the class, which no single source file can see.
a view is built but never handed to the client
An empty page and no error: the builder ran, the result was never passed to client->view_display( ) (or a nested-view, popup, popover or nav call).