abap2UI5-linter rules

Every rule the abap2UI5 view linter can report — the id it prints, what it means, and what to do about it.

49 error 28 warning 10 hint 8 autofixable

The id is what the linter prints at the end of every reported line, what the rules block of abap2ui5lint.jsonc is keyed by, and what a abap2ui5lint-disable-next-line comment names.

No rule matches that.

Severity, and how to waive a rule

Every finding is always reported; --fail-on only decides the exit code (default: warning).

errorthe app breaks: a dump, a control that will not load, a value UI5 rejects, or a defect that silently destroys the view
warningit works where it was written, but not necessarily on the target system — or the data behind it is not what the author thinks it is
hintworth knowing, never wrong by itself

One line, in the source it applies to:

" abap2ui5lint-disable-next-line unknown-binding-path -- filled in a LOOP
)->a( n = `text` v = `{PRICE}` )

One repo, in abap2ui5lint.jsonc:

{
  "rules": {
    "missing-accessibility": false,
    "member-deprecated": "hint",
    "event-without-handler": { "severity": "warning", "exclude": ["/test/"] }
  }
}

Controls and members

Everything the view writes, resolved against the UI5 metadata snapshot generated from the OpenUI5 sources.

invalid-aggregation-child error

a control the aggregation's type does not accept

The aggregation declares a type, and the control put inside it does not inherit from it. UI5 refuses to add the child, so the part of the view below it silently disappears.

invalid-property-value error

Button type="Emphasised" — outside sap.m.ButtonType

The literal is not a member of the enum the property is typed with, or it is not a number on an int/float property, or not a boolean on a boolean one. UI5 in future mode refuses the view rather than falling back to a default. Bindings and expressions are never value-checked — their value is a runtime matter.

too-many-children error

two controls in a 0..1 aggregation

A single-cardinality aggregation was filled more than once. Only one child survives — which one is not something to rely on.

unknown-aggregation error

Page contentt — no such aggregation

An aggregation tag the parent control does not declare. UI5 then looks for a control class by that name and fails.

unknown-control error

sap.m.Shell2 — no such control

The control name is not in the metadata snapshot under any known library. Almost always a typo; UI5 fails to load the class and the view never appears. A control from a custom namespace is out of scope and never reported here.

view->tag( `Buton` )

unknown-event-parameter hint

a ${$parameters>/typo} the event does not declare

The parameter name is resolved against the event's own metadata — a name it does not declare usually arrives empty at get_event_arg( ), with no error anywhere. Judged only against an event the control declares itself, with a parameters block: a subclass can widen an inherited event without redeclaring it (sap.m.DateRangeSelection fires change with from/to/valid while the declared parameters sit on InputBase). A hint, not a warning, because even that is not proof — a control can fire more than its metadata declares (ColorPickerPopover forwards the picker's change parameters verbatim, colorString included, and declares none of that). The finding lists the parameters the event does declare.

a( n = `search` v = client->_event( val = `GO`
   t_arg = VALUE #( ( `${$parameters>/quer}` ) ) ) )  " query, not quer

unknown-property error

Button typ="…" — no such property, event or association

The attribute does not exist on the control or anywhere in its inheritance chain. UI5 in future mode rejects the view. A control whose chain leaves the snapshot is never reported as missing a member — the linter does not guess.

view->tag( `Button` )->a( n = `typ` v = `Emphasized` )

View structure

Defects in the shape of the document itself — several of them make the view builder assert before a view is ever produced.

aggregation-in-aggregation error

an aggregation directly inside another one

Invalid XML, and the signature of a missing end( ): UI5 goes looking for a control class by that aggregation name and fails.

attribute-without-element error

a( ) on the bare factory root — nothing to attach it to

An attribute call before any element was opened. There is no element to carry it, and the builder asserts.

display-root-mismatch error

a mvc:View handed to popup_display( ), or a core:FragmentDefinition to view_display( )

The slot decides how the client builds the document: view_display( ) and the nested variants go through XMLView.create, popup_display( ) and popover_display( ) through Fragment.load. A fragment is not a view, and a view has no open( ) — so the wrong pairing fails in the browser. Only reported where the document root and the consuming call are in the same statement; a bare control root is a legitimate fragment and is never reported.

client->popup_display( popup->stringify( ) ).  " popup built with n = `View`

duplicate-aggregation error

the same aggregation opened twice under one control

The second tag replaces the first, so everything built into the first one is gone from the rendered view — without a word from anywhere.

duplicate-id error

the same id twice

A duplicate-ID error at runtime — UI5 IDs are unique per view.

duplicate-property error

the same attribute written twice on one control

The view builder asserts on a repeated attribute rather than letting the second value win silently.

excess-shut error

one shut( ) more than the builder tree is deep

The builder ascends past the root — one end( ) too many. The builder asserts on it (parent IS BOUND), so the app dumps before it renders. The rule id keeps the older spelling shut: renaming it would silently invalidate every baseline entry and rule override that names it.

invalid-expression-binding error

unbalanced braces or parens in {= … }

The expression binding cannot be parsed. UI5 reports a parse error and the attribute stays unbound.

render-error error

the reconstructed view did not survive a real UI5 render

Not a pattern the rules above match, but the outcome of actually loading the view in a headless UI5: whatever the browser said — a control class that will not load, an aggregation UI5 refuses, a binding it cannot parse. It is the render gate's pseudo-rule rather than an entry in RULES, so it is never emitted by a check; it appears only when --render ran and the browser objected. Address it in the config like any other id: rules: { 'render-error': false } or an exclude waives it per file (a waived file that then renders clean is reported as a stale waiver), a severity re-weighs it.

rules: { 'render-error': 'warning' }

source-line-too-long error

a source line over 255 characters — the class does not import

ABAP holds 255 characters per source line, and over it the defect is not a lint finding but a failed import: abapGit reports "Literals across more than one line are not allowed" for the object and carries on with the next one, so what stays behind in the system is an empty class stub. The tree looks imported and the app is gone. Nothing else sees it — every check runs against the files, where the line is merely long. Split the literal into && chunks; when the file is generated, fix the generator, or the next generation restores the line.

undeclared-namespace error --fix

ns = 'form' without an xmlns:form

The prefix is used but never declared on the view root, so the parser cannot resolve the control at all.

--fix: For the conventional prefixes (core, mvc, l/layout, form, f, table, u/unified, uxap, tnt, html, cc) the missing declaration is inserted next to the root's first xmlns write; an unconventional prefix could mean any library and is left alone.

Version and deprecation

Portability against the UI5 version your system actually runs (--ui5, default 1.71) and the distribution it serves (--distribution).

aggregation-too-new error

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

commercial-ui5-host warning

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-deprecated warning

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-too-new warning

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.

enum-value-too-new warning

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

event-parameter-too-new warning

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.

icon-removed warning

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.

icon-too-new warning

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

member-deprecated warning

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.

member-too-new warning

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.

sapui5-only-control error

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.

toolbar-control-in-bar warning

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` )

unknown-icon error

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.

binding-for-event error

_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( ).

binding-on-association error

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 ) ) )

binding-to-local warning

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.

binding-to-nonpublic error

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 ) )

binding-to-reference error

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

chain-element-per-line hint

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

chain-house-layout hint --fix

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

chain-indentation hint

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

collapsed-brace-in-style error

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

denied-control-method error

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` ) ) )

duplicate-for-iterator warning

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.

escaped-brace-in-backtick error

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

event-arg-out-of-range error

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 )

event-arg-unresolved warning --fix

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-for-property error

_event( ) on a property

The mirror image: an event handler written into a data slot. Use client->_bind( ).

event-on-disabled-control hint

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

event-without-handler hint

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.

frontend-action-unknown-id error

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` ) ) )

frozen-view-builder error

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` ).

get-viewname-removed error

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

hardcoded-binding-path warning

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

invalid-action-payload error

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"]` ) )

invalid-frontend-action error

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` ) ) )

invalid-keyboard-shortcut error

a shortcut combo that names no key

The registration normalizes the combo (Ctrl+Shift+Sctrl+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` ) )

json-bind-on-scalar-property warning

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

json-literal-in-attribute error

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 ).

live-event-roundtrip hint

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` )

manual-init-flag warning

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.

missing-on-navigated-branch warning

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.

missing-view-display-on-navigated error

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

non-released-api warning

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

obsolete-binder warning --fix

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 )

obsolete-frontend-event warning --fix

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 ) )

obsolete-model-update warning --fix

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-anchor-unknown-id error

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 error --fix

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-to-frontend warning

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` ) ) ).

separate-lifecycle-ifs warning

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.

settable-property-via-action hint

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 ) )

trailing-empty-event-arg warning

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.

ui5-internal-access warning

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.

unconverted-abap-boolean error --fix

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.

unescaped-brace-in-style error

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>`.

unknown-binding-path warning

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.

unknown-frontend-action error

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` ) ) ).

unknown-model error

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 ) )

unknown-view-slot error

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` ) ) ).

unused-public-attribute hint

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.

view-never-displayed error

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).

Data and usability

The view loads and renders — but not with the data, or not for the user, the author had in mind.

binding-type-mismatch warning

an ABAP character field bound to a numeric or boolean UI5 property

The model ships JSON, so a TYPE string (or c, n, d) field arrives as "100" where the property declared a float. UI5 1.71 coerces it; UI5 2.x and the render gate's future mode reject the view outright ("100" is of type string, expected float). Declare the field with the matching ABAP type, or convert it before it reaches the model. Only reported when the field's type is known from the class's own declarations.

DATA percent TYPE string.
" … )->a( n = `percentValue` v = client->_bind( percent )

collection-bound-to-property error

a table or structure bound to a scalar property

The property receives an object where it expects a value. Nothing throws; the control shows nothing useful.

date-type-without-source error

sap.ui.model.type.Date / DateTime / Time without formatOptions.source

Without a source format option these types expect a JS Date instance in the model. An abap2UI5 model is JSON serialized from ABAP, so the value is always a string (or a timestamp number) and a Date can never reach it — the type raises a FormatException on the first format() and the field stays empty, with nothing in the console for a Text. Add the source format the ABAP field actually carries, e.g. formatOptions: { source: { pattern: 'yyyy-MM-dd' } }. Note the alias form is resolved through the view's core:require, so type: 'DateType' is judged like the full module name.

a( n = `text` v = |\{ path: '{ client->_bind( val = date path = abap_true ) }', type: 'DateType', formatOptions: \{ style: 'short' \} \}| )

missing-accessibility hint

an icon-only Button with no accessible name, a meaningful Image without alt

The control is unusable with a screen reader. Both halves are judged the way UI5 itself treats them. A Button with an icon and no text has no accessible name — unless it carries a tooltip or an ariaLabelledBy association, either of which gives it one. An Image is the case that reads backwards: decorative defaults to true, and for a decorative image UI5 ignores alt entirely ("if the image is set to decorative, this property is ignored"). So an image without decorative is one the framework hides from screen readers on purpose, and asking it for an alt asks for an attribute UI5 drops — only an image the author declared MEANINGFUL with decorative="false" and then left unnamed is reported. Never wrong by itself, so it is a hint — switch it off per repo with "missing-accessibility": false if your corpus has made another decision.

view->tag( `Image` )->a( n = `src` v = `logo.png` )->a( n = `decorative` v = `false` )   " no alt, no ariaLabelledBy

missing-required-aggregation warning

a Table bound to rows but given no columns — renders empty

The control has data but not the aggregation it needs to show any of it. Nothing fails: the table renders, and it renders empty, which is the hardest kind of bug to see in a screenshot.

relative-binding-without-context error

a relative {FIELD} on a control that has no binding context

A relative binding is resolved against the control's binding context. Inside a bound aggregation that context is the row; outside one there is none, and JSONModel._getObject returns undefined — the control renders empty, with no error anywhere. This is the flattened-element-binding trap: the original did bindElement('/Coll/0'), the port seeded that record at the model root and kept the relative {FIELD}. Bind the root field instead (client->_bind( field )). Reported only when the name really is a field of the model root, so a per-row popup whose context arrives at runtime is not judged.

a( n = `title` v = `{NAME}` )   " NAME is a root field -> renders empty
a( n = `title` v = client->_bind( name ) )

uncurated-formatter error

formatter: 'Formatter.round2DP' — not in the curated module

The framework ships ONE formatter module (z2ui5/model/formatter), and its export surface is deliberately tiny: a function is admitted only when it formats exactly the value handed to it and there is a technical reason it cannot be done in ABAP (a JS type the JSON model cannot carry, an icon-font glyph). UI5 resolves the formatter string at binding time, and an unknown name silently yields no value — the property is simply never set, the cell renders blank, nothing turns red. The demo-kit pack (round2DP, dimensions, stockStatusState, stockStatusIcon, deliveryStatusState) and weightState were shipped and then removed upstream, breaking their users exactly this way. If the value you need is not in the curated list, it is not a formatting problem: compute it in model_init and bind the finished field. Judged only for the framework's own alias (Formatter via core:require, or the z2ui5.Formatter global) — an alias pointed at your own module is left alone.

a( n = `state` v = |\{ path: 'STATUS', formatter: 'Formatter.stockStatusState' \}| )  " blank cell
a( n = `state` v = `{STATUS_STATE}` )  " computed in ABAP, bound finished