abap2UI5 linter rules

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

61 error 45 warning 19 hint 34 autofixable

Generated from v0.6.1, against the OpenUI5 1.151.0 metadata snapshot. This page follows main; your pinned CLI reports the rules of the version it is. npx abap2ui5lint --version says which that is.

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. An owner that overrides addAggregation( ) (the snapshot's widensAggregation, e.g. sap.uxap.ObjectPageSubSection) decides for itself what it accepts and is not judged by its declared type.

reported
view->ele( `Table` )->ele( `columns` )->tag( `Button` )
fixed
view->ele( `Table` )->ele( `columns` )->tag( `Column` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

invalid-property-value error --fix

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.

--fix: For an enum-typed property, where exactly one allowed value is the written value up to letter case (emphasized for Emphasized), the finding names it and --fix rewrites the value in place. A numeric or boolean property, and a value no single enum member matches that way, are reported without a suggestion.

reported
view->tag( `Button` )->a( n = `type` v = `Emphasised` )
fixed
view->tag( `Button` )->a( n = `type` v = `Emphasized` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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. Only an aggregation the metadata declares multiple: false is 0..1 — UI5 defaults an undeclared flag to 0..n, and so does this rule. The children written straight under a control fill its default aggregation and are held to the same cardinality.

reported
view->ele( `Page`
    )->ele( `customHeader`
        )->tag( `Bar`
        )->tag( `Bar` )   " customHeader is 0..1 - one of the two is gone
fixed
view->ele( `Page`
    )->ele( `customHeader`
        )->tag( `Bar` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

unknown-aggregation error --fix

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. Two more readings share the id: an aggregation tag written in a namespace other than its parent tag's (<f:content> under a sap.m.Page — UI5 loads it as the control sap.f.content), and an association written as a child tag (<ariaLabelledBy> — write it as an attribute).

--fix: Where exactly one aggregation or association of the owning control is the written name up to letter case — or one control of the tag's own library is, since page under a View is Page with the wrong first letter — the finding names it and --fix rewrites the tag. Otherwise no suggestion is made.

reported
view->ele( `Page` )->ele( `contentt` )->tag( `Button` )
fixed
view->ele( `Page` )->ele( `content` )->tag( `Button` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

unknown-control error --fix

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.

--fix: Where exactly one control of the same library is the written name up to letter case — or one aggregation of the owning control is, since in UI5 XML the first letter decides between the two — the finding names it and --fix rewrites the name in the builder call (or the XML tag). A name no single candidate matches that way is reported without a suggestion; the linter does not guess at edit distances.

reported
view->tag( `Buton` )
fixed
view->tag( `Button` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

unknown-event-parameter hint --fix

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.

--fix: Where exactly one parameter the event declares is the written name up to letter case (Query for query), the finding names it and --fix rewrites the name inside the ${$parameters>/…} reference.

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

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

unknown-property error --fix

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. One caveat the message now carries: the metadata snapshot is a PIN, so a member released AFTER it is absent here and reports in exactly the shape a typo has. That verdict cannot be split without demoting every real typo with it, so the way out is the allow list, which suppresses any finding on one named Control.member"allow": ["sap.ui.unified.DateTypeRange.ariaHasPopup"] — rather than switching the rule off for a whole repository.

--fix: Where exactly one property, event, association or aggregation of the control (its parents included) is the written name up to letter case and separators, the finding names it and --fix rewrites the attribute name in place. Anything less exact than that is reported without a suggestion.

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

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

unresolved-attribute-value hint

an enum-typed attribute whose value the reconstructor could not follow

A builder attribute written with a value this gate cannot resolve — a COND #( ), a variable, a concatenation — is DROPPED from the reconstructed document, so every rule downstream is blind to it. That blind spot is mostly paid for: the attribute NAME is still known, which is all a version check needs, so member-too-new judges it anyway. What cannot be paid for is a CLOSED SET. An enum-typed property is the one member whose value the gate would otherwise have decided outright, and UI5 rejects a value outside the enum by taking the whole view down — so the one check that mattered here went with the value. Deliberately scoped to enum-typed properties: reporting every unresolved attribute would name a blind spot on almost every conditional attribute in a corpus, which is a statistic rather than a finding.

reported
view->tag( `Button` )->a( n = `type` v = COND #( WHEN ok THEN `Accept` ELSE `Reject` ) )   " ButtonType, unchecked
fixed
view->tag( `Button` )->a( n = `type` v = client->_bind( button_type ) )   " …and set BUTTON_TYPE to a ButtonType in ABAP

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->ele( `Table` )->ele( `columns` )->ele( `items` )   " a missing end( ) between them
fixed
view->ele( `Table`
    )->ele( `columns`
        )->tag( `Column`
    )->end(
    )->ele( `items`
        )->tag( `ColumnListItem` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
DATA(view) = z2ui5_cl_ui5_view_builder=>factory( ).
view->a( n = `title` v = `Hi` )   " no element open yet
fixed
DATA(view) = z2ui5_cl_ui5_view_builder=>factory( ).
view->ele( `Page` )->a( n = `title` v = `Hi` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/reconstruct.mjs

boolc-instead-of-xsdbool warning

boolc( ) where the ecosystem mandates xsdbool( )

The abap2UI5 ecosystem's downport pipeline converts xsdbool( ) to boolc( ) automatically for the releases that need it — so xsdbool is the form to write, and a hand-written boolc breaks the conversion in the other direction. They are also not aliases: boolc returns a char1-in-string whose FALSE is a blank ( ), xsdbool an abap_bool whose false is initial — the classic trap where IF boolc( … ) IS INITIAL. is never true. No fix renames one into the other, because that difference is behaviour, not spelling.

reported
DATA(ok) = boolc( x > 1 ).   " c LENGTH 1, not abap_bool
fixed
DATA(ok) = xsdbool( x > 1 ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

byte-order-mark warning --fix

a UTF-8 BOM at the start of a .abap file

abapGit writes the BOM on .xml sidecars and NEVER on .abap source, so a source file carrying one diffs against what the system serializes back — permanently, on every pull, for everyone (the classic "fix abapgit diffs" commit). The sidecar half of the same rule is abaplint's xml_bom; the .abap half had no rule anywhere. A warning, not an error: the import works, the tree just never stops diffing.

--fix: The BOM character is deleted — a pure one-character deletion at offset 0.

reported
CLASS zcl_my_app DEFINITION PUBLIC.   " the invisible EF BB BF in front of CLASS
fixed
CLASS zcl_my_app DEFINITION PUBLIC.   " plain UTF-8; abap2ui5lint --fix strips the BOM

Defined in lib/abap-rules.mjs

crlf-line-ending warning --fix

CRLF line endings — abapGit writes LF only

abapGit serializes every file with LF endings; a file committed with CRLF comes back different on the next pull, for everyone. This is exactly what a .gitattributes used to normalize in abap2UI5 until it was deleted and a gate took over — and a consumer repository has neither. One finding per file, however many lines carry it: a file is CRLF or it is not, and a finding per line would drown everything else the report has to say.

--fix: Every carriage return in front of a line feed is deleted, converting the whole file to LF in one pass.

reported
CLASS zcl_my_app DEFINITION PUBLIC.\r\n   " every line ends CR LF
fixed
CLASS zcl_my_app DEFINITION PUBLIC.\n   " LF only; abap2ui5lint --fix rewrites them

Defined in lib/abap-rules.mjs

delete-index-in-loop error

DELETE itab INDEX sy-tabix inside a LOOP AT over the same table

Reported only where sy-tabix is provably not the loop's own cursor any more — because that is what the measured incidents share. Where an inner construct between the loop header and the DELETE has overwritten it (a READ TABLE, an inner LOOP that ended, a DO), the DELETE hits another table's index — or index 0, which is the TABLE_INVALID_INDEX short dump a live app answered HTTP 500 with. The READ TABLE case holds on every runtime; the ended inner LOOP and the DO are transpiler behaviour — the ABAP kernel restores the enclosing loop's sy-tabix at ENDLOOP and DO writes only sy-index — and they are reported because the same source runs on the transpiler runtime too, where both incidents were measured. The same holds when the DELETE names an ENCLOSING loop's table while an inner loop is open: sy-tabix then belongs to the inner one, and the deleted index is another table's row number. The plain current-row delete — the innermost loop's own, unclobbered cursor — is legal ABAP and stays silent: the kernel adjusts the loop cursor for a delete on the loop table, and so does the transpiler runtime. Build the result instead — collect what is kept and assign it back — or DELETE itab WHERE …; both read as what they do and survive a later statement wandering in between. The DELETE itab INDEX sy-tabix directly after a READ TABLE, outside a loop over that table, is correct and common, and is not reported.

reported
LOOP AT t_rows INTO DATA(row).
  READ TABLE t_map WITH KEY id = row-id INTO DATA(m).
  DELETE t_rows INDEX sy-tabix.   " sy-tabix is t_map's now - wrong row, or dump
ENDLOOP.
fixed
LOOP AT t_rows INTO DATA(row).
  DATA(idx) = sy-tabix.
  READ TABLE t_map WITH KEY id = row-id INTO DATA(m).
  DELETE t_rows INDEX idx.
ENDLOOP.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
DATA(popup) = z2ui5_cl_ui5_view_builder=>factory( )->ele( n = `View` ns = `mvc` ).
client->popup_display( popup->stringify( ) ).   " a popup slot wants a Dialog root
fixed
DATA(popup) = z2ui5_cl_ui5_view_builder=>factory( )->ele( `Dialog` ).
client->popup_display( popup->stringify( ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/reconstruct.mjs

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.

reported
view->ele( `Page`
    )->ele( `content` )->tag( `Button` )->end(
    )->ele( `content` )->tag( `Text` )   " the Button is gone
fixed
view->ele( `Page`
    )->ele( `content`
        )->tag( `Button`
        )->tag( `Text` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

duplicate-id error

the same id twice

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

reported
view->tag( `Button` )->a( n = `id` v = `go` )->tag( `Text` )->a( n = `id` v = `go` )
fixed
view->tag( `Button` )->a( n = `id` v = `go` )->tag( `Text` )->a( n = `id` v = `goLabel` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->tag( `Button` )->a( n = `text` v = `A` )->a( n = `text` v = `B` )
fixed
view->tag( `Button` )->a( n = `text` v = `B` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/reconstruct.mjs

empty-catch-block hint

an empty CATCH block without ##NO_HANDLER

The extended check (SLIN/ATC) runs in real systems, not in a systemless pipeline — so an empty handler ships through a green CI and surfaces as a finding on somebody else's system. ##NO_HANDLER on the CATCH statement is how a deliberately empty handler says so. Purely structural: a CATCH statement followed by nothing before the next CATCH, CLEANUP or ENDTRY. A comment does not fill the block — SLIN reads it the same way, and the pragma is the sanctioned answer, not prose. A hint, like redundant-conv-i: the code is correct, the extended check is what speaks.

reported
TRY.
    risky( ).
  CATCH cx_root.   " SLIN: empty handler
ENDTRY.
fixed
TRY.
    risky( ).
  CATCH cx_root ##NO_HANDLER.   " deliberate, and says so
ENDTRY.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
view->ele( n = `View` ns = `mvc`
    )->a( n = `xmlns` v = `sap.m`
    )->ele( `Page`
        )->tag( `Button`
    )->end(
)->end(
)->end( )   " one end( ) past the root
fixed
view->ele( n = `View` ns = `mvc`
    )->a( n = `xmlns` v = `sap.m`
    )->ele( `Page`
        )->tag( `Button`
    )->end(
)->end( )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/reconstruct.mjs

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.

reported
view->tag( `Text` )->a( n = `text` v = `{= ${/N} > 1 ? 'many' : 'one' }}` )   " one } too many
fixed
view->tag( `Text` )->a( n = `text` v = `{= ${/N} > 1 ? 'many' : 'one' }` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

missing-final-newline warning --fix

no newline at end of file

abapGit ends every file with exactly one terminating newline; a file without one shows as \ No newline at end of file in every diff and comes back changed on the next pull (z2ui5_if_action.intf.xml carried exactly this). Only a source with at least one line break is judged — a one-line string handed to the library is a snippet, not a file abapGit will ever serialize.

--fix: A single newline is appended at the end of the file.

reported
ENDCLASS.   " the file ends right after the dot, with no newline
fixed
ENDCLASS.   " …followed by exactly one newline; abap2ui5lint --fix appends it

Defined in lib/abap-rules.mjs

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.

reported
view->ele( `Page` )->ele( `contnt` )->tag( `Button` )
" XMLView.create refuses the document, so the gate reports it under this id
fixed
view->ele( `Page` )->ele( `content` )->tag( `Button` )
" to downgrade the gate itself instead: rules: { 'render-error': 'warning' }

Defined in lib/baseline.mjs

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.

reported
DATA(html) = `<div class="a">…</div>` && `…`.   " one source line over 255 characters
fixed
DATA(html) = `<div class="a">…</div>` &&
             `…`.   " wrapped, every line under 255

Defined in lib/abap-rules.mjs

trailing-whitespace warning --fix

a line ending in blanks — stripped by the editor, diffs forever

The ABAP editor strips a trailing blank when it saves, so a line that ends in one comes back different on the next pull. It is also the round-trip defect most likely to be WRITTEN: reflowing a chain or a string template leaves blanks behind that no diff view shows. abaplint's whitespace_end owns this in repositories that enable it — and it is per repository and not everywhere, which is how seven app classes once shipped trailing blanks through four green CI workflows.

--fix: The trailing blanks are deleted, line by line — each finding carries the exact span of its own run of blanks and nothing else on the line is touched.

reported
view->tag( `Button` )   " ← two blanks after the closing paren, invisible here
fixed
view->tag( `Button` )   " nothing after the paren; abap2ui5lint --fix trims it

Defined in lib/abap-rules.mjs

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.

reported
view->ele( n = `View` ns = `mvc`
    )->a( n = `xmlns`     v = `sap.m`
    )->a( n = `xmlns:mvc` v = `sap.ui.core.mvc`
    )->ele( n = `SimpleForm` ns = `form` )   " no xmlns:form on the root
fixed
view->ele( n = `View` ns = `mvc`
    )->a( n = `xmlns`      v = `sap.m`
    )->a( n = `xmlns:mvc`  v = `sap.ui.core.mvc`
    )->a( n = `xmlns:form` v = `sap.ui.layout.form`
    )->ele( n = `SimpleForm` ns = `form` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->ele( `Dialog` )->ele( `footer` )   " @since 1.110 on a 1.71 target
fixed
view->ele( `Dialog` )->ele( `buttons` )   " in 1.71

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
DATA(url) = `https://ui5.sap.com/resources/sap-ui-core.js`.
fixed
DATA(url) = `https://sdk.openui5.org/resources/sap-ui-core.js`.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
view->tag( `DateTimeInput` )   " deprecated as of 1.32.8
fixed
view->tag( `DateTimePicker` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->tag( `Avatar` )   " @since 1.73, target 1.71
fixed
view->tag( `Image` )   " or raise the floor: "ui5": "1.73"

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

deprecated-library warning

a UI5 library SAP deprecated in its entirety

Five libraries are deprecated whole rather than control by control: sap.ui.commons and sap.ui.ux3 (1.38), sap.makit (1.38), sap.me (1.34), and the legacy chart classes directly under sap.viz.ui5 (1.32 — sap.viz.ui5.controls.VizFrame is the replacement and is not reported). None of them is in the metadata snapshot every other rule reads, because the harvest only carries what current UI5 documents — so before this rule a view out of sap.ui.commons was not merely unjudged, it was invisible, and that is the one library it actually happens with: half its control names (Button, Label, Dialog, Panel, ComboBox) also exist in sap.m, so XML copied from an old tutorial drags the dead namespace along and still renders something. The list is hand-maintained for the same reason it is needed: it cannot be regenerated from data the snapshot does not carry. Held against your target release like any other deprecation — below the deprecating version nothing is reported.

reported
view->tag( n = `Button` ns = `c` )   " xmlns:c="sap.ui.commons" — gone since 1.38
fixed
view->tag( `Button` )   " sap.m, the library that replaced it

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->tag( `Button` )->a( n = `type` v = `Critical` )   " @since 1.73 on a 1.71 target
fixed
view->tag( `Button` )->a( n = `type` v = `Reject` )   " in 1.71

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->tag( `SearchField` )->a( n = `search` v = client->_event( val   = `SEARCH`
    t_arg = VALUE #( ( `${$parameters>/searchButtonPressed}` ) ) ) )   " @since 1.114
fixed
view->tag( `SearchField` )->a( n = `search` v = client->_event( val   = `SEARCH`
    t_arg = VALUE #( ( `${$parameters>/query}` ) ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

frontend-action-too-new warning

a CONTROL_GLOBAL target the target release does not carry yet

Four of the global targets are resolved with a LAZY sap.ui.require, and deliberately so: a hard dependency on a module the running release does not have 404s and takes the whole component down with it. What that buys in robustness it pays for in silence — below its release the require returns undefined, the dispatch hits its "not available" guard, and the wire does nothing at all. THEMING.setTheme needs 1.118, FORMATTING.setCustomCurrencies/addCustomCurrencies 1.120, INVISIBLE_MESSAGE.announce 1.78, and POPUP.setWithinArea 1.89 (there the module is old and only the method is new). This is the version half of the wire family: member-too-new and its siblings judge what the VIEW writes, and nothing judged what the class SENDS.

reported
follow_up_action( val   = client->cs_event-control_global
                  t_arg = VALUE #( ( `THEMING` ) ( `setTheme` ) ( `sap_horizon` ) ) )   " @since 1.118
fixed
follow_up_action( val   = client->cs_event-control_global
                  t_arg = VALUE #( ( `MESSAGE_TOAST` ) ( `show` ) ( `saved` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
view->tag( `Button` )->a( n = `icon` v = `sap-icon://binary` )   " in the font for 1.104 only
fixed
view->tag( `Button` )->a( n = `icon` v = `sap-icon://source-code` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/icons.mjs

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.

reported
view->tag( `Button` )->a( n = `icon` v = `sap-icon://information` )   " @since 1.80 on a 1.71 target
fixed
view->tag( `Button` )->a( n = `icon` v = `sap-icon://message-information` )   " in 1.71

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/icons.mjs

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.

reported
view->tag( `Bar` )->a( n = `translucent` v = `true` )   " deprecated since 1.18.6
fixed
view->tag( `Bar` )   " the property has no effect any more - drop it

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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. A member that shipped with a base class younger than the control (a base class UI5 extracted later, taking the member with it) is dated at the control's own release.

reported
view->tag( `GenericTile` )->a( n = `systemInfo` v = `PRD` )   " @since 1.92
fixed
view->tag( `GenericTile` )->a( n = `header` v = `PRD` )   " or raise the floor: "ui5": "1.92"

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

relative-asset-url warning

a document-relative asset URL, which an abap2UI5 app has no root to resolve

A UI5 demo-kit sample is served from the SDK page, so ./test-resources/sap/uxap/images/x.png resolves there. An abap2UI5 app is served from the ABAP ICF node and has no such document root: the request 404s and the control silently falls back to its placeholder — the view renders, and nothing is logged where anyone looks. Judged on the metadata type (sap.ui.core.URI), not on attribute names, so src, icon, backgroundImage, objectImageURI and fontURI are reached alike. Scoped to the two demo-kit trees that are always wrong (test-resources/…, resources/sap/…): a project serving its own ICF resources may legitimately write a relative path. A binding is never judged — what the model holds is a runtime question.

reported
a( n = `src` v = `./test-resources/sap/uxap/images/imageID.png` )
fixed
a( n = `src` v = `https://sdk.openui5.org/test-resources/sap/uxap/images/imageID.png` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

sapui5-only-control error

needs SAPUI5, absent from 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. The severity therefore follows what your config says about the target, and this is the only rule of which that is true: with --distribution openui5 (or "distribution": "openui5") it is an error, because the library provably is not there; with --distribution sapui5 it is not reported at all, because you have said it is; and with no distribution configured it is a hint — the linter does not know which system the app deploys to, and a control only one distribution ships is worth knowing about either way. Configure distribution to turn the hint into the answer your repository actually needs.

reported
view->tag( n = `SmartTable` ns = `smart` )   " sap.ui.comp: SAPUI5 only
fixed
view->ele( `Table` )   " sap.m, in OpenUI5 too; or declare "distribution": "sapui5"

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->ele( `headerContent` )->tag( `ToolbarSeparator` )
fixed
view->ele( `headerContent`
    )->tag( `Button`
    )->tag( `Button`
        )->a( n = `class` v = `sapUiMediumMarginBegin` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

unknown-icon error --fix

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.

--fix: Where exactly one glyph of the font is the written name up to letter case and hyphens — textFormatting for text-formatting, the camelCase miss the message describes — the finding names it and --fix rewrites the name after sap-icon://. Wherever the name is written: an attribute value, a constant, a row of a status table.

reported
view->tag( `Button` )->a( n = `icon` v = `sap-icon://textFormatting` )
fixed
view->tag( `Button` )->a( n = `icon` v = `sap-icon://text-formatting` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/icons.mjs

abap2UI5 — the app class

The lifecycle and the API surface of the class itself: whether the view is ever displayed, whether the roundtrip finds it again, and whether the objects it names are ones abap2UI5 released.

class-constructor-visibility error --fix

class_constructor declared outside the PUBLIC SECTION

The static constructor is called by the runtime itself, so its visibility is not a design choice: the compiler requires it public and a class declaring it PROTECTED or PRIVATE does not activate. Nothing in a systemless pipeline sees that — the class lints, the tests run, and the error appears the first time somebody imports the transport.

--fix: The declaration line is deleted where it is and written again directly after the PUBLIC SECTION. of the same class definition, at its own indentation. Only for the one shape that is exactly that: CLASS-METHODS class_constructor. alone on its line, no chained colon form, no comment on the line, and a PROTECTED or PRIVATE SECTION between the public section and the line; anything else is reported without a fix.

reported
PUBLIC SECTION.
  METHODS run.
PRIVATE SECTION.
  CLASS-METHODS class_constructor.
fixed
PUBLIC SECTION.
  CLASS-METHODS class_constructor.
  METHODS run.
PRIVATE SECTION.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

display-after-nav-app-call hint

a display call after nav_app_call( ) in the same flow

nav_app_call( ) hands the roundtrip to another app: what the browser shows next is THAT app's view. A view_display( ) issued behind it, in the same statement sequence, builds a view nobody sees — and a popup_display( ) behind it opens a dialog over the app being left. Nearly always a missing RETURN after the call. Judged on the sequence the call sits in: a display in a nested block, or after an ELSE/WHEN/ENDIF boundary, is another flow and is not reported.

reported
METHOD on_event.
  client->nav_app_call( NEW zcl_detail( id ) ).
  client->view_display( render( ) ).   " never seen
ENDMETHOD.
fixed
METHOD on_event.
  client->nav_app_call( NEW zcl_detail( id ) ).
  RETURN.
ENDMETHOD.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

double-display-in-branch hint

the same slot displayed twice in one flow — the first call is dead

Two view_display( ) calls in the same statement sequence — or two popup_display( ), two nest_view_display( ) — hand the frontend two documents for one slot in one roundtrip, and only the last one is ever shown. The first is dead code, and the view it built is usually a different one: the shape is a display left behind after a refactoring, or a fallthrough display under a CASE whose branches already display. A display inside a nested block counts as its own flow and is not paired with one outside it.

reported
METHOD main.
  client->view_display( render_list( ) ).   " replaced before it is seen
  client->view_display( render_detail( ) ).
ENDMETHOD.
fixed
METHOD main.
  client->view_display( render_detail( ) ).
ENDMETHOD.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
DATA(a) = VALUE ty_t( FOR i = 1 WHILE i <= 3 ( i ) ).
DATA(b) = VALUE ty_t( FOR i = 1 WHILE i <= 3 ( i ) ).   " downported: i declared twice
fixed
DATA(a) = VALUE ty_t( FOR i = 1 WHILE i <= 3 ( i ) ).
DATA(b) = VALUE ty_t( FOR j = 1 WHILE j <= 3 ( j ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

frozen-view-builder warning

the class builds its view with z2ui5_cl_xml_view — still supported, but no view rule can read it

This is the only finding here about what was *not* judged, and it is a recommendation rather than a complaint. z2ui5_cl_xml_view was the abap2UI5 view builder before z2ui5_cl_ui5_view_builder arrived; it still compiles, still renders, and the plan is for it to move into a separate addon rather than to vanish — so an app that stays on it is making a legitimate choice. What that choice costs is this gate. The 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 stays silent for lack of anything to read. Worth saying out loud, because until this rule existed such a class was not even collected: a whole app on the old builder came back as "no checkable app classes" and exit 0, which reads like approval. Rewrite the chain on z2ui5_cl_ui5_view_builder and the whole gate applies again — controls, properties, bindings, render — which is the real argument for switching. Reported as a warning, not an error: nothing here is broken today. An app that is staying on the old builder for now says so once, in its config: "rules": { "frozen-view-builder": "hint" }, or false to switch it off.

reported
DATA(view) = z2ui5_cl_xml_view=>factory( ).   " the old builder — nothing below this line is checked
view->page( )->button( text = `hi` ).
fixed
DATA(view) = z2ui5_cl_ui5_view_builder=>factory( ).
view->ele( `Page` )->tag( `Button` )->a( n = `text` v = `hi` ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/index.mjs

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.

reported
DATA(viewname) = client->get( )-viewname.   " no longer compiles
fixed
DATA(slot) = client->cs_view-main.   " name the slot yourself

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

into-corresponding-inline-decl error

INTO CORRESPONDING FIELDS OF TABLE @DATA(…) — 7.55 syntax

Below 7.55 the system refuses the class with *"Inline data declarations cannot be used together with INTO CORRESPONDING additions"*, plus one follow-up *"Field … is unknown"* for every later read of the table that was never declared — three errors whose cause is the first one. abaplint stays green because its SELECT grammar puts no version gate on the inline declaration (measured at syntax.version v750 with check_syntax and downport on, control probe fired). Plain INTO TABLE @DATA(…) is fine from 7.40 on; it is only the combination with CORRESPONDING that is late. Declare the table with DATA … TYPE STANDARD TABLE OF … WITH EMPTY KEY and select into it.

reported
SELECT * FROM scarr INTO CORRESPONDING FIELDS OF TABLE @DATA(lt_carr).
fixed
DATA lt_carr TYPE STANDARD TABLE OF scarr WITH EMPTY KEY.
SELECT * FROM scarr INTO CORRESPONDING FIELDS OF TABLE @lt_carr.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

lifecycle-is-initial hint --fix

IS INITIAL / IS NOT INITIAL on an abap_bool

The three check_on_*( ) methods return abap_bool, so the branch is the PREDICATIVE CALL — IF client->check_on_init( ). — which is how z2ui5_if_client documents it and how the whole sample corpus is written. Compounds keep the shape (` ELSEIF client->check_on_event( LOCK ). , IF client->check_on_navigated( ) AND mv_ready = abap_true.). IS NOT INITIAL asks a boolean whether it is EMPTY, which is what that question means for a string, and it is one more dialect every later reader of the class has to hold. Only a NEGATIVE branch is spelled out, as = abap_false: there is no negated predicative form. The same rule covers every other abap_bool, in a WHERE clause as much as in an IF. A hint, deliberately — abap_false IS the initial value of a char(1), so the two spellings agree and nothing here is broken; it is the one dialect question in the app guide a finding can settle mechanically. A structure component is never judged: in row-flag IS INITIAL the component may be any type, and the class's own flag` attribute is not evidence about it.

--fix: On a lifecycle call, IS NOT INITIAL becomes the predicative call (the tail is deleted) and IS INITIAL becomes = abap_false — the framework's checks return only abap_true/abap_false, so both rewrites are exact. On a plain abap_bool variable only IS INITIAL is rewritten (to = abap_false, its exact meaning for a char(1)); the NOT form is reported without a fix, because a variable can technically hold any character and choosing between = abap_true and <> abap_false would be a guess.

reported
IF client->check_on_init( ) IS NOT INITIAL.
IF mv_ready IS INITIAL.
fixed
IF client->check_on_init( ).
IF mv_ready = abap_false.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
DATA check_initialized TYPE abap_bool.
IF check_initialized = abap_false.
  check_initialized = abap_true.
  client->view_display( render( ) ).
ENDIF.
fixed
IF client->check_on_init( ).
  client->view_display( render( ) ).
ENDIF.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
IF client->check_on_init( ).
  model_init( ).
  view_display( ).
ELSEIF client->check_on_event( ).
  on_event( ).
ENDIF.
fixed
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.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
ELSEIF client->check_on_navigated( ).
  result = client->get_app( client->get( )-s_draft-id ).
fixed
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

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
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
fixed
DATA(json) = z2ui5_cl_ui5_json=>factory( body ).   " released, src/02
" the popups moved out whole: install abap2UI5-addons/popups and call it there
DATA(html) = view->xml_escape( data ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

obsolete-bind-argument warning --fix

_bind( view = … ) and the custom_mapper/custom_filter pair

Two arguments the binder still ACCEPTS and should no longer be given. view is "inactive, not passed on internally": a call scoping a binding to cs_view-popup reads as if the binding belonged to that slot, and nothing reads the value at all — so it is dead weight that documents a behaviour the framework does not have. custom_mapper and custom_filter are the opposite, and worse: they are still evaluated, but both hand the app a reference to the AJSON copy bundled in src/00 — a mirror of an external project, not a contract abap2UI5 owns — so an app implementing z2ui5_if_ajson_mapping/_filter binds itself to whatever that mirror looks like today. That is the non-released-api argument one level down, at a parameter rather than a class name. Everything they were reached for is declarative on the method now: omit_initial/omit_initial_paths drop initial fields and json splices a JSON node.

--fix: The inactive view argument is deleted, together with the spaces behind it; a value carrying parentheses is an expression whose end this pass does not measure and is reported without a fix. The mapper pair never carries one — they are still evaluated, so dropping one changes what the model carries.

reported
a( n = `value` v = client->_bind( val = name view = client->cs_view-popup ) )
fixed
a( n = `value` v = client->_bind( name ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
a( n = `value` v = client->_bind_edit( name ) )
fixed
a( n = `value` v = client->_bind( name ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
a( n = `press` v = client->_event_client( val = client->cs_event-popup_close ) )
fixed
a( n = `press` v = client->_event( `CLOSE` ) )   " …and popup_destroy( ) in the handler

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
client->popup_model_update( ).   " does nothing
fixed
" nothing - the model travels with the roundtrip; delete the call

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

private-app-attribute warning

a PRIVATE instance attribute on a z2ui5_if_app class

The app's state is persisted with CALL TRANSFORMATION id, and the transpiled runtime re-implements that walk with a dynamic ASSIGN obj->(name), which reaches a PROTECTED attribute and not a private one. sy-subrc comes back 4, the serializer asserts, and every roundtrip answers ASSERTION_FAILED out of lcl_heap.add_object — with nothing in the message naming the attribute that caused it, which is what makes it expensive to find rather than merely wrong. A warning rather than an error, deliberately: a real SAP kernel serializes a private attribute fine, so the class works on the system it was written for and breaks on the transpiled runtime — abap2UI5's own Node backend and every e2e smoke. Six ports carried it while the 53 with a PROTECTED attribute were fine, which is what isolated it. App state belongs in PUBLIC — that is also what makes it reach the model at all — and helpers in PROTECTED. CLASS-DATA is never reported: a static attribute is not instance state and is not serialized with the app.

reported
PRIVATE SECTION.
  DATA t_all TYPE ty_t_row.
fixed
PUBLIC SECTION.
  DATA t_all TYPE ty_t_row.   " PUBLIC for bound state, PROTECTED for a helper

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

redundant-conv-i hint --fix

CONV i( ) assigned to a target that is already TYPE i

The assignment converts by itself, so the CONV says nothing and reads as if a conversion were needed; SLIN reports *"Redundant conversion for type I"*. Both halves of the scoping are load-bearing and were measured before this shipped: the target has to be declared IN THIS FILE (a type living in another class is left alone rather than guessed at), and the CONV has to be the ENTIRE right-hand side — one inside a comparison or an arithmetic expression is load-bearing or at least arguable, and one inside a string template (|{ CONV i( x ) WIDTH = 2 }|) is a real conversion. All three were false errors in the first draft. A hint: the code is correct, and this is an extended-check finding rather than a defect.

--fix: The CONV i( … ) wrapper is unwrapped to its inner expression. Safe by the rule's own scoping: the CONV is guaranteed to be the entire right-hand side and the target is declared TYPE i in this file, so the assignment performs the identical conversion by itself.

reported
DATA count TYPE i.
count = CONV i( lv_text ).
fixed
DATA count TYPE i.
count = lv_text.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

redundant-init-display warning --fix

a lifecycle fork whose two arms do the same thing

check_on_init( ) is "this app INSTANCE never ran", and it implies check_on_navigated( ): every path to an instance's first main( ) raises the navigated flag too — factory_first_start for a fresh start and for a draft restore, factory_system_startup, and prepare_app_stack for both legs of a nav_app_call. The interface states it. So IF check_on_init( ) OR check_on_navigated( ) can never change its verdict on the OR, and an init branch whose only statement is the same display call its ELSEIF check_on_navigated( ) twin makes is four lines saying what the ELSEIF says alone. The fork is only reported where BOTH arms are one identical display call — where the navigated arm does anything else (an on_navigation( ), an app return handled first) the fork really does decide something and the init arm stays.

--fix: The OR form loses the check_on_init( ) call together with the OR beside it, whichever side it is on; the fork loses its whole init arm — from the IF up to the ELSE of the ELSEIF, which then opens the block as the IF. Both are exact by the implication the rule rests on. A span with a comment inside it is reported without a fix, because deleting a comment nobody asked about would be a guess.

reported
IF client->check_on_init( ) OR client->check_on_navigated( ).   " the OR decides nothing
  client->view_display( view->stringify( ) ).
ENDIF.
fixed
IF client->check_on_navigated( ).   " on_init implies it - one branch is enough
  client->view_display( view->stringify( ) ).
ENDIF.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
IF client->check_on_init( ).
  " …
ENDIF.
IF client->check_on_navigated( ).   " a second IF: both branches can run
  " …
ENDIF.
fixed
IF client->check_on_init( ).
  " …
ELSEIF client->check_on_navigated( ).
  " …
ENDIF.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

unconditional-popup-display warning

a popup_display( ) at the top level of main( ) — the dialog is rebuilt on every roundtrip

popup_display( ) and popover_display( ) load the fragment anew and open it every time they are called — the frontend has no "unchanged, keep it" path for a popup the way it has for the main view. A call at the top level of main( ), outside every branch and behind no guard, therefore runs on EVERY event the app receives: typing into a field in the dialog fires its change, the roundtrip rebuilds the dialog, the field loses focus, the scroll position resets. Every popup class in the framework itself displays from its check_on_init( ) branch or behind a RETURN guard. Only z2ui5_if_app~main is judged, and only where no RETURN, LEAVE, CHECK or EXIT precedes the call — a guard block that leaves the method makes everything after it conditional.

reported
METHOD z2ui5_if_app~main.
  IF client->check_on_init( ).
    init( ).
  ENDIF.
  client->popup_display( render_dialog( ) ).   " every roundtrip
ENDMETHOD.
fixed
METHOD z2ui5_if_app~main.
  IF client->check_on_init( ).
    init( ).
    client->popup_display( render_dialog( ) ).   " once
  ENDIF.
ENDMETHOD.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

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.

reported
PUBLIC SECTION.
  DATA mv_note TYPE string.   " and mv_note appears nowhere else in the class
fixed
PROTECTED SECTION.
  DATA mv_note TYPE string.   " …or delete it: nothing binds, reads or writes it

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

value-header-default-reassigned error

a VALUE component assigned in the header and again in a row

A component assigned before the first line spec is a DEFAULT for all following lines, not an overridable one, so assigning it again inside a row makes the system's syntax check refuse the whole constructor: *"The component was specified more than once."* abaplint accepts the construct without a finding — its VALUE grammar does not model the one-assignment rule — so this reached a repository's main branch through a green CI and was found by a user running Code Inspector's SYNTAX_CHECK over a pulled copy. Write the value per row instead, or close the header's scope with a second group: the default binds only to the lines *after* it.

reported
VALUE #( selectable = abap_true ( a = 1 ) ( a = 2 selectable = abap_false ) )
fixed
VALUE #( ( a = 1 selectable = abap_true ) ( a = 2 selectable = abap_false ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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

reported
METHOD z2ui5_if_app~main.
  DATA(view) = z2ui5_cl_ui5_view_builder=>factory( ).
  view->ele( `Page` )->tag( `Button` ).
ENDMETHOD.
fixed
METHOD z2ui5_if_app~main.
  DATA(view) = z2ui5_cl_ui5_view_builder=>factory( ).
  view->ele( `Page` )->tag( `Button` ).
  client->view_display( view->stringify( ) ).
ENDMETHOD.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/lifecycle-rules.mjs

abap2UI5 — frontend wires

Everything travelling to the browser as an event or an action. The frontend resolves these against closed sets and, on a miss, mostly returns without a log — so a wrong wire is the quietest defect in the framework.

control-call-arg-count warning

more t_arg values than the control method declares

The frontend declares the argument kinds of every control method it knows, and castArgs ends on kinds.slice(0, count) — so an argument past the declared ones is never passed to the control. It is not an error anywhere: the call runs, with fewer arguments than the source appears to give it, and the extra value reads as intent to every later reader. back, close, focus, collapseAll and the other no-argument methods are the common case; setExpanded given two is the other. A method the frontend does NOT declare is open by design (any public, non-denylisted one) and is never judged on arity.

reported
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `nav` ) ( `back` ) ( `page2` ) ) )   " back takes none
fixed
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `nav` ) ( `back` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

control-call-arg-kind error

a t_arg value that is not the kind the control method declares

The declared kinds are not decoration — castArg acts on them. An int argument runs through Number( ), so a non-numeric literal arrives as NaN and every comparison against it is false: sap.m.Button's badge setters compare the incoming value against the stored bound, so the value is dropped into an else whose only effect is a Log.warning. A bool argument is raw === "true" || raw === "X" and nothing else, so a literal meant as true in any other spelling — 1, TRUE, abap_true, Y — silently arrives as FALSE and switches the flag the wrong way. The ABAP boolean tokens (X, a space, the empty string) and the literal true/false are the accepted spellings and are never reported.

reported
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `panel` ) ( `setExpanded` ) ( `abap_true` ) ) )
fixed
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `panel` ) ( `setExpanded` ) ( `X` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

control-state-lost-on-rebuild hint

a CONTROL_BY_ID set…( ) that no binding can carry, issued only off the display path

The exact inverse of settable-property-via-action, and its blind spot. That rule fires when the setter matches a bindable property and says "bind it instead"; it is silent for the three shapes where that answer does not exist — an association (setNextStep, setSelectedSection, setActivePage, setCurrentStep), a function-typed property (sap.m.MessagePopover.asyncURLHandler) and a method that is no member at all (setBadgeMinValue: sap.m.Button declares badgeStyle as its only badge property and keeps the bounds in private fields Button.init resets to 1/9999). Those three are live control state, and abap2UI5 does not patch a view — view_display( ) hands new XML to the VIEW_SLOTS action, whose displayMain destroys the MAIN slot (taking POPUP and POPOVER with it) and builds a fresh tree with XMLView.create. Every control in it is a new object carrying what the XML declares and nothing else. A bound property survives that, because the binding re-applies; this state does not. So a class that sets it from an event handler and never re-issues it from the display path loses it on the next rebuild — a restored draft, a called app handing control back, any later view_display( ) — while the ABAP field describing it survives as class state, and the app then contradicts itself. The remedy is to re-issue the call from the method that displays the view (samples-controls app 249 re-sends both badge bounds from the values it kept; app 534 ends view_display( ) on the path_apply( ) that wires the whole wizard path). Deliberately narrow: only a non-literal value is judged, because a constant carries no class state for the rebuilt view to contradict — setCurrentStep( 'ProductInfoStep' ) is a one-shot corrective jump and setSelectedSection( '' ) a reset to null, and re-issuing either on every rebuild would be the defect. A wire is silent when its own method issues a display call, when any method on that path (a helper it calls, a few levels deep) issues the same id+setter, and — the same thing seen from the other side — when it is queued next to a popup_display( )/popover_display( ), since the system-action phase is awaited before the follow-up actions run.

reported
METHOD on_event.
  client->follow_up_action( val   = client->cs_event-control_by_id
                            t_arg = VALUE #( ( `step1` ) ( `setNextStep` ) ( next_step ) ) ).
ENDMETHOD.
fixed
METHOD view_display.
  client->view_display( view->stringify( ) ).
  " the same id+setter, re-issued on every rebuild
  client->follow_up_action( val   = client->cs_event-control_by_id
                            t_arg = VALUE #( ( `step1` ) ( `setNextStep` ) ( next_step ) ) ).
ENDMETHOD.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `list` ) ( `destroy` ) ) )
fixed
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `list` ) ( `removeAllItems` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

event-arg-js-callback error

a JS callback in a t_arg — the whole event handler fails to parse

A $- or {-opening argument is sent to the frontend verbatim and resolved by UI5, which parses the entire handler string with BindingParser.parseExpression. ExpressionParser has no function keyword — its token table knows only false/null/true/in/typeof — and { is the object-literal nud, so .map(function (o) { … }) throws before any argument is read. This is not a wrong argument value: the exception is on the handler, so every argument is lost and the event never reaches the backend. An arrow function fails identically (=> is not a token). An argument that does not open with $ or { is shipped as a JS string literal, so the word "function" inside a toast template is not reported.

reported
t_arg = VALUE #( ( `$event.oSource.getSelectedRows().map(function(r){return r.getId();})` ) )
fixed
t_arg = VALUE #( ( `${$source>/id}` ) )   " …and bind the row's own `selected` property

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
view->tag( `Button` )->a( n = `press` v = client->_event( val = `PICK` t_arg = VALUE #( ( `${$source>/id}` ) ) ) ).
CASE client->get( )-event.
  WHEN `PICK`.
    DATA(second) = client->get_event_arg( 2 ).   " PICK sends one argument
ENDCASE.
fixed
view->tag( `Button` )->a( n = `press` v = client->_event( val = `PICK` t_arg = VALUE #( ( `${$source>/id}` ) ) ) ).
CASE client->get( )-event.
  WHEN `PICK`.
    DATA(id) = client->get_event_arg( 1 ).   " the one argument PICK sends
ENDCASE.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
client->_event( val = `PICK` t_arg = VALUE #( ( `{/ID}` ) ) )   " arrives empty
fixed
client->_event( val = `PICK` t_arg = VALUE #( ( `${/ID}` ) ) )   " resolved

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

event-name-case-mismatch warning --fix

an event raised as save and handled as SAVE — the handler never runs

The event name travels to the browser and back as a string, and client->get_event( ) / check_on_event( ) compare it letter for letter. A raise spelled differently from its handler — _event( save ) against WHEN SAVE` — is a dead control that looks handled: the name is there, in both places, and a case-blind reading (which is what event-without-handler` does, on purpose) sees it as fine. Reported where exactly ONE handled spelling is the raised name up to case; two handled spellings of one name are left alone rather than chosen between. The fix writes the handler's spelling into the raise, because the handler is the one place the name is compared.

--fix: The raised literal is rewritten to the handler's spelling — _event( \save\ ) becomes _event( \SAVE\ ) — inside the literal's own quotes; the arguments around it are untouched.

reported
view->tag( `Button` )->a( n = `press` v = client->_event( `save` ) )
" … CASE client->get_event( ).
"      WHEN `SAVE`.   " never reached
fixed
view->tag( `Button` )->a( n = `press` v = client->_event( `SAVE` ) )
" … CASE client->get_event( ).
"      WHEN `SAVE`.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
)->tag( `Button`
    )->a( n = `press`   v = client->_event( `SAVE` )
    )->a( n = `enabled` v = `false` )   " SAVE can never fire
fixed
)->tag( `Button`
    )->a( n = `press`   v = client->_event( `SAVE` )
    )->a( n = `enabled` b = is_savable )   " bound, so it can become true

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->tag( `Button` )->a( n = `press` v = client->_event( `SAVE` ) )
" … and no WHEN `SAVE` anywhere in the class
fixed
view->tag( `Button` )->a( n = `press` v = client->_event( `SAVE` ) )
" … CASE client->get_event( ).
"      WHEN `SAVE`.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

filter-groups-not-arrays error

a BINDING_CALL filter payload whose groups are objects — it clears the binding instead of filtering it

The compound filter form is an array of GROUPS, each an array of [path, operator, value1, value2?] ROWS. buildFilterGroups keeps only elements that pass Array.isArray, so an array of OBJECTS — [{"path":…,"operator":…,"value1":…}] — is dropped whole, the group list empties, and control falls into binding.filter([]): the filter is cleared, never applied. Nothing is logged on that path, because the root *is* an array so the malformed-JSON guard passes and the per-group drop is silent by construction. An intentional clear is [], which is empty to begin with and is not reported.

reported
view->tag( `List` )->a( n = `id` v = `list` ).
client->follow_up_action( val   = client->cs_event-binding_call
                          t_arg = VALUE #( ( `list` ) ( `items` ) ( `filter` )
                                           ( `[{"path":"NAME","operator":"Contains","value1":"x"}]` ) ) )   " cleared
fixed
view->tag( `List` )->a( n = `id` v = `list` ).
client->follow_up_action( val   = client->cs_event-binding_call
                          t_arg = VALUE #( ( `list` ) ( `items` ) ( `filter` )
                                           ( `NAME` ) ( `Contains` ) ( term ) ) )   " positional

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

frontend-action-unknown-id error --fix

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.

--fix: Where exactly one id a view of this class declares is the written id up to letter case (messageview for messageView), the finding names it and --fix rewrites the literal inside its own quotes. An id no declared id matches that way is reported without a suggestion.

reported
a( n = `id` v = `messageView` )
" … t_arg = VALUE #( ( `messageview` ) ( `navigateBack` ) )   " no control answers to it
fixed
a( n = `id` v = `messageView` )
" … t_arg = VALUE #( ( `messageView` ) ( `navigateBack` ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
t_arg = VALUE #( ( `table1` ) ( `setSticky` ) ( `ColumnHeaders` ) )   " not JSON -> {}
fixed
t_arg = VALUE #( ( `table1` ) ( `setSticky` ) ( `["ColumnHeaders"]` ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

invalid-aggregation-item error

a slashed control id that addresses no aggregation item

A cloned aggregation item has no id the backend can know — the rendered one carries the view prefix the framework assigns at runtime — so it is addressed positionally as <id>/<aggregation>/<index> (0-based) and resolved in the browser. Two ways to get that wrong, and the id rules around this one see neither, because they judge the head segment alone: a slashed value that does not MATCH the shape (a non-numeric index, a fourth segment) never enters the aggregation path at all and is looked up as one plain id, which resolves to nothing; and a shape that matches but names an aggregation the control does not declare makes getAggregation return nothing, so the wire is dropped with a log. The aggregation half is judged only when the head id resolves to a control the snapshot knows.

reported
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `carousel/pages/first` ) ( `setActivePage` ) ) )
fixed
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `carousel/pages/0` ) ( `setActivePage` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
client->_event_client( val   = client->cs_event-control_global
                       t_arg = VALUE #( ( `MESSAGE_TOASTER` ) ( `show` ) ( `hi` ) ) )
fixed
client->_event_client( val   = client->cs_event-control_global
                       t_arg = VALUE #( ( `MESSAGE_TOAST` ) ( `show` ) ( `hi` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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

reported
client->follow_up_action( val   = client->cs_event-keyboard_shortcut
                          t_arg = VALUE #( ( `Ctrl+Shift` ) ( `SAVE` ) ) )   " modifiers only — binds nothing
fixed
client->follow_up_action( val   = client->cs_event-keyboard_shortcut
                          t_arg = VALUE #( ( `Ctrl+Shift+S` ) ( `SAVE` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

literal-view-slot hint --fix

a view slot retyped as a string where cs_view has a constant for it

The mirror image of unknown-view-slot, and the reason that rule keeps finding work to do. view = MAIN` is correct — and nothing checks it: the value travels to the browser as an object key, so a rename, a typo or the wrong half of the pairing dispatches to no view at all, silently. client->cs_view-main cannot fail that way, because the ABAP compiler resolves the name: cs_view-mian does not activate. It also spells the one pairing nobody remembers — the constant is cs_view-nested and its VALUE is NEST, cs_view-nested2 is NEST2 — which is exactly the mistake unknown-view-slot reports once the literal is written out. A hint, because the wire works today; --fix` writes the constant in place.

--fix: replaces the literal with the client->cs_view-… constant carrying that value — an exact one-token substitution, nothing to guess.

reported
client->_event_client( val   = client->cs_event-control_by_id
                       view  = `NEST`   " works, but nothing checks it
                       t_arg = VALUE #( ( `table1` ) ( `focus` ) ) ).
fixed
client->_event_client( val   = client->cs_event-control_by_id
                       view  = client->cs_view-nested
                       t_arg = VALUE #( ( `table1` ) ( `focus` ) ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
)->a( n = `liveChange` v = client->_event( `SEARCH` ) )   " lossy under fast typing
fixed
)->a( n = `value`  v = client->_bind( search_term )
)->a( n = `change` v = client->_event( `SEARCH` ) )   " the final value, once

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

popover-anchor-unknown-id error --fix

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.

--fix: Where exactly one id a view of this class declares is the written by_id up to letter case, the finding names it and --fix rewrites the literal. Otherwise no suggestion is made.

reported
)->a( n = `id` v = `btnInfo` )
" …
client->popover_display( xml = popover->stringify( ) by_id = `btninfo` ).
fixed
)->a( n = `id` v = `btnInfo` )
" …
client->popover_display( xml = popover->stringify( ) by_id = `btnInfo` ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
client->popover_display( val = popover->stringify( ) ).   " does not compile
fixed
client->popover_display( xml = popover->stringify( ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
client->follow_up_action( val = `sap.ui.getCore().byId('x').focus()` ).   " raw JS
fixed
client->follow_up_action( val = client->cs_event-set_focus t_arg = VALUE #( ( `x` ) ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
view->tag( n = `DynamicSideContent` ns = `l` )->a( n = `id` v = `sideContent` ).
client->follow_up_action( val   = client->cs_event-control_by_id
                          t_arg = VALUE #( ( `sideContent` ) ( `setShowSideContent` ) ( `true` ) ) )
fixed
DATA show_side TYPE abap_bool.
view->tag( n = `DynamicSideContent` ns = `l` )->a( n = `showSideContent` v = client->_bind( show_side ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

trailing-empty-event-arg warning --fix

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.

--fix: The trailing empty ( \\ ) row is deleted from the VALUE #( ) constructor — it never arrives, so removing it changes nothing the handler can observe. A row with its line to itself takes the whole line; an inline row also takes the spaces in front of it. A comment is never removed with it.

reported
client->_event( val = `PICK` t_arg = VALUE #( ( `${/ID}` ) ( `` ) ) )   " get_event_arg( 2 ) reads initial
fixed
client->_event( val = `PICK` t_arg = VALUE #( ( `${/ID}` ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
client->follow_up_action( val   = client->cs_event-control_by_id
                          t_arg = VALUE #( ( `list` ) ( `mProperties` ) ) )
fixed
client->follow_up_action( val   = client->cs_event-control_by_id
                          t_arg = VALUE #( ( `list` ) ( `getVisible` ) ) )   " a public method

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
client->follow_up_action( val = `SET_TITEL` t_arg = VALUE #( ( `Hi` ) ) ).   " swallowed silently
fixed
client->follow_up_action( val = client->cs_event-set_title t_arg = VALUE #( ( `Hi` ) ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

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.

reported
client->_event_client( val   = client->cs_event-control_by_id
                       view  = `NESTED`   " the slot is NEST
                       t_arg = VALUE #( ( `table1` ) ( `focus` ) ) ).
fixed
client->_event_client( val   = client->cs_event-control_by_id
                       view  = client->cs_view-nested
                       t_arg = VALUE #( ( `table1` ) ( `focus` ) ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/frontend-wires.mjs

abap2UI5 — bindings

What a binding actually addresses once the framework has derived the client path from the ABAP name: whether the field exists, whether it survives the roundtrip, and whether the value arrives as the type the property expects.

abap-date-formatter-mismatch error

an ABAP TYPE d/TYPE t field converted with Formatter.DateCreateObject

The curated module ships three date helpers, and which one a field needs is decided by what its ABAP type SERIALIZES as. DateCreateObject is new Date( s ) and wants a string the JS Date constructor parses — an ISO one. A TYPE d reaches the model as 20240101 and a TYPE t as 120000, and the constructor parses neither: both come back as an Invalid Date. DateAbapDateToDateObject and DateAbapDateTimeToDateObject exist for exactly this and split the digits themselves. What makes it worth a rule is where it surfaces: an Invalid Date is TRUTHY, so every guard that merely checks for presence accepts it, and the throw happens much later and somewhere else — sap.ui.unified Month._checkDateEnabled calls CalendarDate.fromLocalJSDate, which throws for every rendered day and takes the whole view down, with nothing in the stack naming the binding. The EMPTY-value case this rule does not cover is closed upstream: DateCreateObject returns null for a falsy input and isNoAbapDate rejects anything that is not eight digits, so reporting it would now be a finding on correct code. The type mismatch is the half no guard in the formatter can see, because it is handed a string and cannot know which ABAP type produced it.

reported
DATA valid_from TYPE d.
" …
a( n = `dateValue` v = `{ path: 'VALID_FROM', formatter: 'Formatter.DateCreateObject' }` )
fixed
DATA valid_from TYPE timestamp.
" …
a( n = `dateValue` v = `{ path: 'VALID_FROM', formatter: 'Formatter.DateCreateObject' }` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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

reported
view->tag( `Button` )->a( n = `press` v = client->_bind( name ) )
fixed
view->tag( `Button` )->a( n = `press` v = client->_event( `SAVE` ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
a( n = `selectedSection` v = client->_bind( section ) )   " association -> the id "{/SECTION}"
fixed
follow_up_action( val   = client->cs_event-control_by_id
                  t_arg = VALUE #( ( `opl` ) ( `setSelectedSection` ) ( section ) ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
METHOD z2ui5_if_app~main.
  DATA lv_title TYPE string.
  view->tag( `Text` )->a( n = `text` v = client->_bind( lv_title ) ).   " gone after the roundtrip
ENDMETHOD.
fixed
PUBLIC SECTION.
  DATA title TYPE string.
" …
  view->tag( `Text` )->a( n = `text` v = client->_bind( title ) ).

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
PROTECTED SECTION.
  DATA expanded TYPE abap_bool.   " BINDING_ERROR on the first roundtrip
" … )->a( n = `expanded` v = client->_bind( expanded ) )
fixed
PUBLIC SECTION.
  DATA expanded TYPE abap_bool.
" … )->a( n = `expanded` v = client->_bind( expanded ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

binding-to-reference error --fix

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.

--fix: ->* is inserted after the attribute name in the _bind( )_bind( mt_data ) becomes _bind( mt_data->* ). Only where the attribute is declared TYPE REF TO data: a reference to a named type may be a class, on which ->* does not compile, so those stay findings without a fix.

reported
DATA mt_data TYPE REF TO data.
" …
)->a( n = `items` v = client->_bind( mt_data )   " throws
fixed
DATA mt_data TYPE REF TO data.
" …
)->a( n = `items` v = client->_bind( mt_data->* )   " binds the table

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

collapsed-brace-in-style error --fix

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.

--fix: A \\ is inserted in front of every \{ and \} inside the |…| template segments of the stylesheet — the doubled form a template needs to deliver the escaped brace — and one pass covers every segment of the sheet. The literal is not turned into a backtick literal: a template may carry other escapes and embedded expressions whose meaning would change with the delimiter, and rewriting those would be a guess.

reported
DATA(css) = |<style>.a \{color:red\}</style>|.   " the template collapses \{ to {
fixed
DATA(css) = `<style>.a \{color:red\}</style>`.   " a backtick literal survives

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

enum-field-unset-on-insert error

a row built at runtime without a field the view binds to an enum — the empty string takes the view down

The demo-kit originals push a JS object with the key absent, and UI5 falls back to the property default. ABAP has no absent: an unset field ships as "", which is not a member of any enum, so ManagedObject.validateProperty throws — and ManagedObjectBindingSupport re-throws anything that is not a FormatException, so the binding update dies and takes the view with it. Both repairs are already in the framework: seed the default (type = 'None'), or name the field in omit_initial_paths. Three construction sites are judged, because a port picks between them for reasons that have nothing to do with this defect: an INSERT/APPEND of a VALUE #( … ) literal, the t = VALUE #( ( … ) ( … ) ) seed a model_init writes (including the nested elements = VALUE #( … ) inside one), and a row assembled in a work area and inserted a few statements later. A row copied wholesale — VALUE #( FOR row IN t_all … ( row ) ) — has no field list to read and is not judged. Fields are keyed on the table each bound aggregation names, absolute ({/T_PAGES}) and relative ({path: 'T_APPOINTMENTS'}) alike, and each key gets only the fields of its OWN row template: a PlanningCalendar binds rows, specialDates and a nested appointments at once, and only the innermost carries ariaHasPopup. Not reported where the class fills the field afterwards — a LOOP … r->state = … completing a seed is how half this corpus moves an original's frontend formatter server-side — nor for a mixed-case path, which is not an ABAP component name at all (type="{Text}" is the demo kit's own quirk, ported verbatim: it resolves to nothing, so UI5 keeps the default). The widening came from a corpus sweep in 2026-08, where the INSERT-only reading had missed ten real defects across seven ports, every one of which took its view down.

reported
TYPES: BEGIN OF ty_appointment, title TYPE string, type TYPE string, END OF ty_appointment.
DATA t_appointments TYPE STANDARD TABLE OF ty_appointment WITH DEFAULT KEY.
INSERT VALUE #( title = `New` ) INTO TABLE t_appointments.   " type unset -> "" -> throws
view->ele( n = `CalendarRow` ns = `u` )->a( n = `appointments` v = client->_bind( t_appointments )
    )->ele( n = `appointments` ns = `u`
        )->ele( n = `CalendarAppointment` ns = `u`
            )->a( n = `title` v = `{TITLE}`
            )->a( n = `type` v = `{TYPE}` )
fixed
TYPES: BEGIN OF ty_appointment, title TYPE string, type TYPE string, END OF ty_appointment.
DATA t_appointments TYPE STANDARD TABLE OF ty_appointment WITH DEFAULT KEY.
INSERT VALUE #( title = `New` type = `Type01` ) INTO TABLE t_appointments.
view->ele( n = `CalendarRow` ns = `u` )->a( n = `appointments` v = client->_bind( t_appointments )
    )->ele( n = `appointments` ns = `u`
        )->ele( n = `CalendarAppointment` ns = `u`
            )->a( n = `title` v = `{TITLE}`
            )->a( n = `type` v = `{TYPE}` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

escaped-brace-in-backtick error --fix

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.

--fix: Every backslash in front of a brace inside the backtick literal is deleted, leaving the plain-brace form the literal should have carried. Nothing else in the literal is touched — a backtick literal has no escape processing, so the backslashes say nothing.

reported
a( n = `items` v = `\{ path: 'message>/' \}` )   " the backslash reaches the XML
fixed
a( n = `items` v = `{ path: 'message>/' }` )   " correct in a backtick literal

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

event-for-property error

_event( ) on a property

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

reported
view->tag( `Text` )->a( n = `tooltip` v = client->_event( `GO` ) )
fixed
view->tag( `Text` )->a( n = `tooltip` v = client->_bind( hint ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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. Only a literal that IS a value is judged — the v = … of an attribute call, a && chain of literals included, and the rows of a t_arg = VALUE …( ) event argument; a path quoted in a comment, a message text or any other literal binds nothing and is not reported.

reported
a( n = `title` v = `{/TITLE}` )   " breaks on rename
fixed
a( n = `title` v = client->_bind( title ) )   " moves with the variable

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

json-bind-on-scalar-property warning --fix

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.

--fix: The json = abap_true argument is deleted from the _bind( ), together with the whitespace in front of it — _bind( val = manifest json = abap_true ) becomes _bind( val = manifest ), the plain bind of the same attribute. Found from the reported attribute write: its argument list, the first _bind( ) in it, and that one argument; a call written any other way is reported without a fix.

reported
DATA manifest TYPE string.   " contains JSON
)->a( n = `value` v = client->_bind( val = manifest json = abap_true ) )
fixed
DATA manifest TYPE string.
)->a( n = `value` v = client->_bind( manifest ) )   " Input.value is string-typed

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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

reported
view->tag( n = `Card` ns = `card` )->a( n = `manifest` v = `{"sap.card":{"type":"List"}}` )
fixed
DATA manifest TYPE string.   " PUBLIC, holding the same JSON
" …
view->tag( n = `Card` ns = `card`
    )->a( n = `manifest` v = client->_bind( val = manifest json = abap_true ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

relative-aggregation-without-context error

a root-level aggregation bound with a relative path — it resolves against nothing and renders empty

Inside a bound row template a relative aggregation path is the normal and correct form. Outside one there is no context: Model.resolve returns undefined for a relative path with none (legacy syntax is off since 1.88), bindList never resolves, and the aggregation stays empty with no error anywhere. The complex form hides it well — {path: 'T_ITEMS'} looks deliberate, and the missing leading slash is the whole defect. It falls between two existing rules: hardcoded-binding-path only matches paths that START with /, and relative-binding-without-context deliberately skips aggregations. Reported only where the control has neither a binding context nor an enclosing template, which is what separates the broken cases from the dominant correct ones. Three narrowings came from running it on a 637-file corpus, where it first fired 81 times: the binding's OWN path only (a nested sorter: { path: … } is not it), a named model stripped before asking whether the path is absolute (message>/ IS absolute), and an aggregation whose value the reconstructor could not resolve — a _bind( ) handed in as a method parameter — still makes its children a row template, because a blind spot is not a defect. A fourth came from the abap2UI5/samples corpus after release: a class that issues cs_event-bind_element sets a binding context on a whole view slot at runtime, so every relative path under it resolves against a row the document never names — which is the entire point of that idiom, and invisible to a static walk. Which slot was bound is a second question, and a wrong second guess is worse than silence.

reported
)->a( n = `items` v = `{path: 'T_LEGEND'}` )   " no context: renders empty
fixed
)->a( n = `items` v = client->_bind( val = t_legend path = abap_true ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
DATA flag TYPE abap_bool.
view->tag( `Button` )->a( n = `visible` v = flag )   " 'X' / ' ' - both truthy
fixed
DATA flag TYPE abap_bool.
view->tag( `Button` )->a( n = `visible` b = flag )   " the builder renders true/false

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

unescaped-brace-in-style error --fix

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.

--fix: A backslash is inserted in front of every unescaped brace between <style> and </style> — but only where every one of them sits inside a backtick literal, which takes the backslash verbatim. A brace inside a |…| template is an embedded expression, and one outside any literal is code, so a stylesheet mixing them is reported without a fix rather than half-escaped.

reported
DATA(css) = `<style>.box {color:red}</style>`.
fixed
DATA(css) = `<style>.box \{color:red\}</style>`.

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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. A field read straight off a table ({/T/FIELD}) is reported as a missing row index: the JSONModel reads FIELD off the array itself. Write {/T/0/FIELD}, or bind the table.

reported
DATA name TYPE string.
…
view->tag( `Text` )->a( n = `text` v = `{/NAEM}` )   " the model has NAME
fixed
DATA name TYPE string.
…
view->tag( `Text` )->a( n = `text` v = client->_bind( name ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
a( n = `text` v = `{i18n>title}` )
fixed
a( n = `text` v = client->_bind( title ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

abap2UI5 — chain layout

How the builder chain is WRITTEN. The XML it emits is one line by construction, so the ABAP indentation is the only picture of the view tree that exists. All hints; the view renders identically either way.

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.

reported
)->tag( `Input` )->tag( `Button` )->tag( `Text` )   " three controls, one line
fixed
)->tag( `Input`
)->tag( `Button`
)->tag( `Text`

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/chain-layout.mjs

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.

reported
)->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
fixed
)->ele( `Shell`
    )->ele( `Page`
        )->tag( `Text`
            )->a( n = `text` v = `x` )

Defined in lib/chain-layout.mjs

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.

reported
)->ele( `Page`
    )->tag( `Input`
  )->tag( `Button`   " a sibling of Input, written a level out
fixed
)->ele( `Page`
    )->tag( `Input`
    )->tag( `Button`

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/chain-layout.mjs

Data and usability

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

absent-boolean-overrides-default error

a row that omits a boolean the neighbouring rows set, where the property defaults to true

abap_bool has no absent state, so a field a row leaves out still ships as a real JSON false — correct almost everywhere, and wrong exactly where the UI5 property's own default is true: the row then renders the OPPOSITE of what it means, with nothing failing anywhere. On its own that would report every boolean seed there is, so the rule asks for both signals: the property defaults to true AND the seed is INCONSISTENT, meaning some rows of this very table set the field and others do not. A table that never sets it is ordinary data (unread and active are false for every row on purpose); a table that always sets it has nothing missing. The gap between two rows of one literal is what is almost never deliberate — one port's notification items lost both close buttons that way, and with them its only backend wire. A field named in omit_initial_paths, carried as a constructor-level default, or filled by a later LOOP is never reported.

reported
TYPES: BEGIN OF ty_note, title TYPE string, icon_inset TYPE abap_bool, END OF ty_note.
DATA t_notes TYPE STANDARD TABLE OF ty_note WITH DEFAULT KEY.
t_notes = VALUE #( ( title = `a` icon_inset = abap_true )
                   ( title = `b` ) ).   " iconInset defaults to true and is now false
view->ele( `List` )->a( n = `items` v = client->_bind( t_notes )
    )->ele( `StandardListItem`
        )->a( n = `title` v = `{TITLE}`
        )->a( n = `iconInset` v = `{ICON_INSET}` )
fixed
TYPES: BEGIN OF ty_note, title TYPE string, icon_inset TYPE abap_bool, END OF ty_note.
DATA t_notes TYPE STANDARD TABLE OF ty_note WITH DEFAULT KEY.
t_notes = VALUE #( ( title = `a` icon_inset = abap_true )
                   ( title = `b` icon_inset = abap_true ) ).
view->ele( `List` )->a( n = `items` v = client->_bind( t_notes )
    )->ele( `StandardListItem`
        )->a( n = `title` v = `{TITLE}`
        )->a( n = `iconInset` v = `{ICON_INSET}` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
DATA percent TYPE string.
" … )->a( n = `percentValue` v = client->_bind( percent ) )
fixed
DATA percent TYPE p LENGTH 5 DECIMALS 2.
" … )->a( n = `percentValue` v = client->_bind( percent ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
DATA t_rows TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY.
…
view->ele( `Table` )->a( n = `headerText` v = client->_bind( t_rows ) )
fixed
DATA t_rows TYPE STANDARD TABLE OF ty_row WITH EMPTY KEY.
…
view->ele( `Table` )->a( n = `items` v = client->_bind( t_rows ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
DATA date TYPE d.
view->tag( `Text` )->a( n = `text` v = |\{ path: '{ client->_bind( val = date path = abap_true ) }', type: 'sap.ui.model.type.Date' \}| )
fixed
DATA date TYPE d.
view->tag( `Text` )->a( n = `text` v = |\{ path: '{ client->_bind( val = date path = abap_true ) }', type: 'sap.ui.model.type.Date', formatOptions: \{ source: \{ pattern: 'yyyyMMdd' \} \} \}| )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

escape-sequence-in-backtick warning --fix

a \n written in a backtick literal, which has no escapes

An ABAP ` backtick literal is RAW: \n inside it is a backslash followed by an n, and nothing further down the chain turns it into a line break — the client's formatTemplate substitutes {N} placeholders and passes everything else through. So a toast built as value - {0}, \n action - {1} renders the two characters on screen, where the original (a double-quoted JS string, shown through .sapMMessageToast's white-space: pre-line) breaks the line. The |…|` STRING TEMPLATE is the ABAP form that does process escapes, so the working spelling concatenates one. Scoped to text that reaches the user — an attribute value and the on-screen message helpers — rather than to every backtick literal: a backslash is legitimate in a regex pattern and in a Windows path, and nobody reads those. A doubled backslash is never reported; whoever wrote it meant a backslash.

--fix: The literal is split at every escape: the text between stays a backtick literal and each escape becomes its own |…| template, concatenated with && — `saved,\n and closed becomes saved, && |\n| && and closed`. Empty pieces are dropped, so a literal that starts with the escape opens with the template. Everything the rule reads is in expression position, where the chain is as valid as the literal it replaces.

reported
client->message_toast_display( `saved,\n and closed` )   " the two characters \ and n
fixed
client->message_toast_display( `saved,` && |\n| && ` and closed` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/abap-rules.mjs

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.

reported
view->tag( `Image` )->a( n = `src` v = `logo.png` )->a( n = `decorative` v = `false` )
fixed
view->tag( `Image`
    )->a( n = `src`        v = `logo.png`
    )->a( n = `decorative` v = `false`
    )->a( n = `alt`        v = `Company logo` )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

reported
view->ele( `Table` )->a( n = `items` v = client->_bind( t_rows ) )   " no columns: renders empty
fixed
view->ele( `Table`
    )->a( n = `items` v = client->_bind( t_rows )

    )->ele( `columns`
        )->tag( `Column`
    )->end( )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

picker-value-without-format warning

a date/time picker binding value with neither a binding type nor a valueFormat

A picker parses and formats its value with valueFormat, and when neither that nor a binding type says otherwise it falls back to the browser LOCALE for the string it writes BACK. client->_bind( ) is two-way and the write-back is a bare ABAP assignment, so the locale string lands in the field. Measured on OpenUI5 (en-US, seeded "2018-07-09T09:00:00"): a sap.m.DateTimePicker still READS the ISO string — DateFormat falls back to ISO — but writes back "Jul 12, 2018, 2:30:00 PM"; a sap.m.DatePicker does not read it at all (dateValue stays null, the raw text is shown) and writes back "7/12/18". Nothing raises, and en-US hides most of it — in de-DE the field comes back as "04.03.2025, 10:15:00", which new Date( ) parses month-first, so an appointment picked for 4 March is drawn on 3 April. Declare valueFormat with the pattern the ABAP field carries (yyyy-MM-dd'T'HH:mm:ss), or bind with a sap.ui.model.type.Date/DateTime and its formatOptions.source — a typed binding owns the pattern and is never reported. The picker family is read off the metadata (a control declaring both value and valueFormat), and the rule needs the ABAP class: it only fires where the CLASS also writes the field, because a field only the picker ever writes is self-consistent whatever the locale does, and one the class writes as digit-free text (N/A) is not a date at all.

reported
DATA start_at TYPE d.
start_at = sy-datum.
view->tag( `DatePicker` )->a( n = `value` v = client->_bind( start_at ) )   " no valueFormat: the picker cannot parse it
fixed
DATA start_at TYPE d.
start_at = sy-datum.
view->tag( `DatePicker` )->a( n = `valueFormat` v = `yyyyMMdd`
    )->a( n = `value`       v = client->_bind( start_at ) )

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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 )). All four shapes a property binding takes are judged, not only the bare {NAME} the rule started on: the composite {STREET} {HOUSENUMBER}, the complex { path: 'PRICE', type: … } and the expression {= ${STATUS} ? … } each resolve slashless paths against the same missing context and render just as blank. The composite is the shape that pays for the rest — samples-controls app 592 shipped 42 dead address bindings across 21 sections in it, over correctly declared root fields, past a green gate, because the matcher was anchored ^{NAME}$. A name the model root does NOT have is reported too: the verdict never depended on the name (a slashless path with no context resolves against nothing whatever it says), only the confidence that there is no context does — so that arm additionally stays silent under a bound aggregation whose row shape could not be resolved. Four things ARE a context and none of them is reported: an enclosing bound aggregation, a per-row template aggregation (template, rowActionTemplate, rowSettingsTemplate — UI5 clones them per row and the context comes from the parent's own rows binding in a sibling aggregation), a binding="{/PATH}" attribute (a ManagedObject special setting handed to bindObject( ) — the declarative form of the wire below), and a cs_event-bind_element wire, which is scoped to the one view slot it names: one popup wire used to disarm the check for every document of the class, main slot included.

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

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

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.

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

Open the reported code in the playground ↗ — the snippet in a class, with the linter's verdict beside the editor

Defined in lib/properties.mjs

validating-setter-out-of-range error

a property whose setter refuses low values, bound to a field that arrives as 0

The declared type is not the whole domain. UI5 lets a control write its own setXxx, and a few of them refuse a value the type allows — sap.ui.unified.RecurringCalendarAppointment declares recurrencePattern as int and throws for anything below 1. An unfilled ABAP TYPE i serializes as exactly that 0, and ManagedObject.updateProperty rethrows anything that is not a FormatException, so the WHOLE render dies rather than this one property being dropped. Reported by a user on UI5 1.150. What keeps the rule quiet is that the snapshot carries the setter's LOWER BOUND rather than the bare fact that it throws: 23 properties throw somewhere in their setter, and for most of them the initial 0 is perfectly legal (MonthPicker.month 0 is January, TimesRow.intervalMinutes guards >= 720). Only a guard of the shape if (v < N) throw is harvested — exactly what makes an unfilled ABAP field illegal — which is two properties in the whole snapshot. A field the seed fills with an accepted number is never reported, and an opaque row resolves to nothing at all.

reported
TYPES: BEGIN OF ty_appointment, title TYPE string, recurrence_pattern TYPE i, END OF ty_appointment.
DATA t_appointments TYPE STANDARD TABLE OF ty_appointment WITH DEFAULT KEY.
t_appointments = VALUE #( ( title = `Review` ) ).   " recurrence_pattern unseeded -> 0
view->ele( n = `CalendarRow` ns = `u` )->a( n = `appointments` v = client->_bind( t_appointments )
    )->ele( n = `appointments` ns = `u`
        )->ele( n = `RecurringCalendarAppointment` ns = `u`
            )->a( n = `title` v = `{TITLE}`
            )->a( n = `recurrencePattern` v = `{RECURRENCE_PATTERN}` )
fixed
TYPES: BEGIN OF ty_appointment, title TYPE string, recurrence_pattern TYPE i, END OF ty_appointment.
DATA t_appointments TYPE STANDARD TABLE OF ty_appointment WITH DEFAULT KEY.
t_appointments = VALUE #( ( title = `Review` recurrence_pattern = 1 ) ).
view->ele( n = `CalendarRow` ns = `u` )->a( n = `appointments` v = client->_bind( t_appointments )
    )->ele( n = `appointments` ns = `u`
        )->ele( n = `RecurringCalendarAppointment` ns = `u`
            )->a( n = `title` v = `{TITLE}`
            )->a( n = `recurrencePattern` v = `{RECURRENCE_PATTERN}` )

Defined in lib/properties.mjs