Repository navigation
Read before upgrading
-
This release is 0.9.0, not 0.8.6. In
0.xthe minor is the number
that says "a consumer has to act" (npm version minor, RELEASING.md), and
several things below need action rather than a lockfile bump: the
baseline key format (a baseline written here is misread by 0.8.5 and
older), the refused numericui5, the baselines a downstream repo has to
refresh, the baseline judged on an empty walk and read from its default
file, and the refused flag pairs.
Nothing removed or renamed an export or a rule id - a renamed id is the
one change that would have been a breaking change on its own - and the
--jsonshape only grew.@abap2ui5/linter-rendermoves to 0.9.0 with
it (one tag, both packages) andnpm run sync-peer-rangewrites the peer
range; its FLOOR stays 0.8.0, since nothing here needs a newer runtime. -
Refresh every downstream baseline with this release - and move every
tool that reads it at the same time. Fixed false positives leave their
baseline entries behind, and a stale entry fails a run (exit 1) just as a
new finding does. abap2UI5-addons/popups carries one now:
src/99/z2ui5_cl_pop_search_help.clas.abap|missing-view-display-on-navigated,
the negated guardIF client->check_on_navigated( ) = abap_false. RETURN.
that 9c630cc (#134) stopped misreading - measured, popups' configured run
exits 1 on that entry alone.abap2ui5lint --update-baselinedrops it.
The rewritten file uses this release's keys (the baseline-key bullet
below), which
0.8.5 and older do not read: bump the Action pin, anynpxpin and the
VS Code extension's linter pin in the same change. -
abap2UI5/samples needs one waiver. A void helper's parameter now
resolves from the call being replayed (Reconstruction), so app 507's
threerender_list( … items = client->_bind( … ) )calls reconstruct
theiritemsbindings - andomit-initial-drops-falsesees the
omit_initial = abap_truelist whoseenabledcolumn the sample drops on
purpose ("Archived is enabled now, its abap_false was initial too"). A true
positive on a sample that demonstrates the defect: waive it there
(" abap2ui5lint-disable-next-line omit-initial-drops-false -- the sample shows exactly this). Nothing else changes on the seven corpora. -
Baseline keys carry no source position any more - and an older linter
cannot read the new ones. Ten rules kept two occurrences in one class
apart by writing a position into a key field: the source offset into
value(default-key-table,abapdoc-html-tag,
event-arg-default-index,display-after-nav-app-call,
double-display-in-branch,empty-catch-block,
boolc-instead-of-xsdbool,delete-index-in-loop) or the line number
intomember(source-line-too-long,trailing-whitespace). Both fields
are part of the baseline key, so a line inserted above a baselined finding
made it a stale entry AND a new finding. The position now travels as
dedupe, which only the per-class collapse reads and which is dropped
before a finding leaves the rules (thelinesays where it is). A
baseline written before keeps working:loadBaseline( ),
applyBaseline( )andupdateBaseline( )read an old key of one of the
ten with a number in that field as today's key (migrateKey( )on
./baseline), so it matches wherever the code moved, and the next
--update-baselinewrites the new form. The other direction does not
hold: 0.8.5 and older compute the old key for those ten rules, so a
baseline rewritten by this release waives nothing of theirs - the findings
come back and every such entry is reported stale (measured: a baseline
written by--update-baselinehere fails a 0.8.5 run with exit 1). That
reaches whatever judges the same baseline with an older linter - an Action
ornpxpinned to an earlier release, and the VS Code extension until its
pin moves, whose Add to Baseline then writes an old key beside the new
one, which this release reads as one key counted twice and reports stale.
Move every tool that reads one baseline to this release together. -
A numeric
ui5/minUi5in the config is refused (exit 2) with a
message that says why: JSON reads1.120as the number 1.12, so the floor
moved eight minors down without a word. Write it quoted,"1.120". A
config that wrote"ui5": 1.71unquoted worked before and has to be
quoted now; the schema always said string. -
A run whose walk collects nothing now has its baseline judged. The
"no checkable app classes" return ran BEFORE the baseline block, so the one
run in which every entry is stale by construction - the classes were
deleted, or moved underignore- was green: no stale entry was reported
(exit 0 instead of 1),--update-baselinekept the entries instead of
pruning them, and a--baselinefile that did not exist was no error. The
empty run applies the baseline (stale entries print under the sentence in
the stylish report, ride in thejsondocument'sbaselineblock and
decide the exit code in every format;--advisorystill exits 0),
--update-baselineprunes the entries of the files that are gone, and a
named baseline that is missing is exit 2. A consumer whose corpus was
emptied and whose baseline still has entries sees exit 1 where it saw 0 -
abap2ui5lint --update-baselinesettles it. -
abap2ui5lint-baseline.jsonis read where--update-baselinewrote
it. Without--baselineand without"baseline"in the config the
update wrote that file and no later run looked for it, so every run kept
reporting the debt it had just frozen. A run that names no baseline reads
abap2ui5lint-baseline.jsonnext to the config (or, without one, in the
current directory) when it exists,--update-baselinewrites the default
to the same place (next to the config, where it used to be the cwd), and
the update prints a second line saying so and how to pin it (--baseline <file>or"baseline"inabap2ui5lint.jsonc). A repository that happens
to carry a file of that name next to its config, from an earlier update it
never wired up, is baselined from now on - the run summary'sbaselined
row and the stale-entry rule say so. -
Contradictory or ineffective flag pairs are refused (exit 2) instead of
silently doing something else. A value flag no longer takes the next
option as its value:--allow --no-renderallowed a control called
--no-renderand ran the render gate the user had just switched off,
--sarif-out --jsonwrote a file literally named--json. Also refused:
--update-baselinebeside--json-out,--sarif-out,--badgeor
--badge-corpus(the sidecars were never written, and the Action's
outputs stayed empty);--stdin --update-baselinewithout
--stdin-filename(<stdin>|...entries no run could match);
--deprecated-atequal to--ui5(an empty horizon that reported
nothing);--no-configbeside--config;--badgetwice;
--cache-locationwithout--cache;--stdin-filenamewithout--stdin.
A workflow carrying one of these inflagsgoes red with the message
where it was silently green. The machine--formats beside
--update-baselinekeep their contract (the "baseline: wrote" line
on stderr, stdout empty), now spelled out in--help.
Rules
-
New opt-in rule
bound-aggregation-over-size-limit(warning, off
until arulesentry asks for it). A list bound to a table the class
seeds past the JSONModel's 100 entries - theitemsof a non-growing
sap.m.ListBase, aSelector aComboBoxBase, bound without a
length, in a class that raises no size limit - stops at 100 without a
word. The count is read where the source decides it, as a lower bound:
the table's one literalVALUE #( ( … ) ( … ) )seed, or the APPENDs
directly inside literalDO n TIMESloops (on the start path's
check_on_init( )/check_on_navigated( )branch too). A table the
class may also shorten or replace - DELETE, CLEAR, a second seed, a
SELECT into it, a method it is handed to - is not judged, nor an APPEND
under any other condition. The general rule was built and dropped in 0.3
for noise on the samples-controls ports, whose originals cap at 100 too;
opt-in, it is offered to app authors, for whom 123 rows showing 100 is the
defect. -
A deprecation horizon:
deprecatedAt/--deprecated-at, and the
ruledeprecated-after-target(hint).ui5answered two questions with
one number - what exists on the target, and what counts as deprecated -
so an app held to 1.71 heard nothing about the 60 controls and 58
members UI5 deprecated between 1.72 and 1.136, the legacy-free baseline
SAP measures best-practice code against (#148). With
"deprecatedAt": "1.136"beside"ui5": "1.71", what took effect after
the target and up to the horizon is reported, naming the replacement the
deprecation links and whether the target already has it
(sap.m.MessagePage→IllustratedMessage, @SInCE 1.98: keep it until
the target moves). Every other rule still judges againstui5; a horizon
below the target is refused. Unset, nothing changes - no consumer sees a
new finding until it asks for the horizon. -
native-html-in-view(hint): native XHTML/SVG in a view. UI5 1.120
deprecated markup in the XHTML and SVG namespaces inside an XML view; the
XMLView logs a[FUTURE FATAL]for it and the next major release refuses
the view. Reported once per island, on its outermost element, and only
on a target of 1.120 or later - below that it is a
deprecated-after-targetunder a horizon and silent without one. A hint
for its first release: abap2UI5 views have long carried<html:style>
blocks, and a corpus on a 1.120+ target should see its count before the
rule can fail a build. -
chain-unbalanced-parensends an unclosed call at its statement's
period. Its scan read an unclosed(as open to the end of the file:
every statement behind it ran into one, so a SECOND broken chain was
never reported, and a stray)further down was counted off against the
open one and vanished. A.before a blank ends the statement at any
depth now -parenRegion( )'s rule, since no ABAP call reaches past one -
so each broken chain is reported on its own statement. No corpus carries
one. -
unbound-public-attributeandunused-public-attributesee a reader in
the same run. A popup hands its result back through a PUBLIC attribute
its caller reads (CAST zcl_pop( client->get_app( … ) )->ms_result), and
a caller sets a value on the app it calls the same way - unbound by
construction, and both rules said no single source can see that caller.
The class index (classIndexOf( )) now records per classoutsideReads:
the names another class of the run reaches asref->namethrough a
reference it types as this class (TYPE REF TO, an inline declaration
fromCAST/NEW/a static factory, or that expression itself before the
arrow); a reference whose class is not written down is nobody's. Both
rules stand down for those names. Because that stand-down depends on what
the RUN holds, a waiver of either rule in such a class is unjudged rather
thanunused-directive(publicReadFromOutside( )on./abap-rules,
fed to the directives asstoodDown): the same class linted alone - an
editor, a pre-commit hook - still needs it, and the unused-directive fix
would have deleted it.classIndexDeps( )carries the set, so--cache
re-judges the popup when a caller starts or stops reading it, and--fix
re-checks it between passes.ClassIndexintypes.d.tsgains the
optional field. Corpora: nothing is reported differently -
abap2UI5/samples app 024's and abap2UI5'snode/srv/zcl_tst_stack_a
waivers for the attribute their callers set stay (unjudged in a corpus
run, used when the class is linted alone); the other outside readers in
the corpora are ABAP Unit test classes, which no run collects, or call the
popup'sresult( )method. -
A
COND #( )orSWITCH #( )of literals is judged branch by branch.
COND #( WHEN n = 1 THEN \Emphasized` ELSE `Default` )on a Buttontypehas a closed set of values, and it was reported asunresolved-attribute-valuewhile a wrong value in a branch went unjudged. The reconstructor records the values of a conditional with anELSEwhose every result resolves (a literal, a&&chain, a nested conditional of the same shape; aTHROWbranch yields none) as the node'sbranchValues, and the property gate judges each like a literal:invalid-property-value(with its case-only--fixlanding on the branch) and the other value rules, silent when all of them pass. NoELSE, aLET, or a result that does not resolve keeps the hint. abap-cloud-gui: its twounresolved-attribute-value` hints are gone. -
uncurated-formatterreads more of the formatters a view names. A
CONTROLLER formatter in a class (formatter: '.weightState', the demo
kit's own form) names a function on the view's controller, which in
abap2UI5 is the framework's and defines none: the view fails to load with
formatter function .weightState not found, and only the render gate said
so. A raw view is left alone (it may be a freestyle app's, with a
controller of its own). And every alias acore:requirepoints at
z2ui5/model/formatteris read ({ Fmt: 'z2ui5/model/formatter' }with
formatter: 'Fmt.round2DP'), the did-you-mean keeping the written alias;
the expression form no longer matches a name that merely ends in
Formatter(myFormatter.round2DP(). -
int,floatandbooleanvalues are judged the way DataType parses
them. The numeric check was^[+-]?\d+(\.\d+)?$:.5,5.and1e3-
numbers to DataType'sNumber( )- were reported as invalid, while10.5
on anintproperty (whichNumber.isIntegerrefuses) passed. An empty
value is valid for all three (NaNfor a number,falsefor a boolean),
and a value the reconstruction guessed from a LOOP variable is not judged
as if the author had written it. -
An
xmlnson an inner element is scoped to it. Every declaration of
a document went into one map, the last one winning, so<VBox xmlns="sap.ui.layout.form">inside a sap.m Page turned the Page above it
into anunknown-control, and a prefix declared in one subtree passed as
declared in another, which the browser's parser refuses. The property
gate, the control-id map, the enum-bound-field reader and the run
summary's profile resolve each element against the declarations in scope
at it (namespaceScopes( )inlib/properties.mjs), and
undeclared-namespacereports a prefix used outside the element that
declares it. The top level stays one scope (a second root is judged with
what the first declares). -
popup-display-xml,popover-display-valand
popover-anchor-unknown-idread the CLIENT'spopup_display( )/
popover_display( )call and find the argument among the named ones
wherever it stands: an app's ownmy_popup_display( xml = … )was
reported and "fixed" into a parameter the method does not have, and
popover_display( by_id = … val = … )went unseen. -
view-never-displayedno longer reports a helper class that hands its
stringified view to the caller -result = view->stringify( )where
resultis a RETURNING, EXPORTING or CHANGING parameter of the method
(abap-cloud-gui'sz2ui5_cl_cgui_listandz2ui5_cl_cgui_selscreen,
which its config excluded by name). -
unescaped-text-in-attributeleavesidandclassalone: the
XMLTemplateProcessor takes both verbatim and never parses a brace in them
as a binding, and the fix,t =, wrote a backslash into the id or the
class token. -
undefined-css-classleaves aclassvalue the reconstruction guessed:
|state-{ ls-row-state }|in a LOOP came out asstate-, the literal
part of a class completed at runtime. -
unknown-iconreads`sap-icon://status-` && lv_codeas the prefix
it is - the backtick spelling of|sap-icon://status-{ lv_code }|, which
was already not judged. -
editable-control-without-bindingsays the control "takes user input"
rather than that it "lets the user type": 152 of samples-controls's 343
findings are on controls nobody types into (RadioButton, CheckBox, Switch,
Slider, Select, …). -
"rules": { "<id>": true }switches an opt-in rule (chain-house-layout,
portable-app) on. It passed the config check and ran nothing;truenow
means "on, as it is". -
unused-directiveleaves a waiver alone where the rule it names never
ran: in an app class whose view another class builds, a helper class under
--all-classesand a class on the frozen builder, only some rules judge
the class (SOURCE_RULES, new on./abap-rules, plus the class-reading
rules for an app class), so a waiver ofunbound-public-attributeor a
property-walk rule there had nothing it could suppress - and its fix
deleted the waiver the class needs the day its view moves back in. It is
unjudged now, as under--no-properties. No corpus carries one. -
unescaped-brace-in-styleandcollapsed-brace-in-styleread a<style>
tag with attributes, in any case. Both rules bounded a stylesheet with the
literal<style>…</style>, so a sheet opened as<style type="text/css">- the form a
core:HTMLcontent copied from an HTML page carries - was
never judged, and its unescaped braces reached the XMLView parser as a
binding. The opening tag is read aslib/css-classes.mjsreads it
(<style\b[^>]*>, case-insensitive), the closing tag the same way.
- the form a
-
hardcoded-binding-pathstands down inside a<style>value again. The
exemption the rule documents -{/between<style>and</style>is an
escaped CSS brace meeting a CSS comment, not a path - had gone out of
judge( )when the rule was narrowed to attribute values, and
v =+'<style>.a \{/* brand */ color: red; \}</style>'+rwas reported. A literal that opens a<styletag is not judged, and a{/written behind a backslash is never a path, in any literal. The test that asserted the exemption did so on aDATA(css) = …assignment the rule no longer judges at all, which is why it kept passing; it asks on acontent` value now. -
binding-to-localjudges the ROOT of the bound name. The scan required
the name to end at a blank or), soclient->_bind( ls_local-field )-
a component of a method-local structure, lost with it after the roundtrip -
was never seen while_bind( lv_plain )beside it was. The root decides, as
it does forbinding-to-staticandbinding-to-nonpublic. Two shapes the
old scan did not judge stay unjudged: a->dereference (a reference into
data that lives elsewhere) and a call carryingtab =, the framework's
cell binding whose path is derived from the table. -
duplicate-for-iteratorfolds case.FOR iandFOR Iin one method
are one variable to the 7.02 downport that materializes them as
DATA i TYPE i, and were two names to the rule. The member is reported as
written. -
One list of boolean types.
abap-rules.mjsdeclared its own
(abap_bool|abap_boolean|boolean|xfeld|flag) andguide-rules.mjsanother
withxsdbooleanandboole_don top, so aDATA mv_flag TYPE boole_d
written throughv =was not anunconverted-abap-boolean, not a
manual-init-flag, not alifecycle-is-initialcandidate - while
omit-initial-drops-falsesaw it.BOOLEAN_TYPESis exported from
guide-rules.mjsand read bycheckAbapBooleans( ), whoseboolVarsthe
two lifecycle rules and the text rule'sisBoolshare. -
Measured on abap2UI5
src+node/srv(103 findings in 39 files) and this
repo's fixtures with--all-classes(327 in 80): no change per type.
Reconstruction
-
A void helper's parameter is the value its call passes. A helper the
replay enters (add_row( page = page text = \Save` )) that writes its parameter where the view needs a value (a( n = `text` v = text )) left that value unresolved - a parameter is the caller's - so the attribute was dropped with every rule that would judge it. During the replay of one call the caller is known: the parameter is the value THAT call passes, resolved where the call stands (a parameter handed on to a second helper through the first one's call). Only when every call of the helper in the class passes a value that resolves, and the helper never writes the parameter itself: ame->call or aCALL METHOD(which the replay does not enter) or one argument computed at runtime keeps it a variable for every call. Eleven documents on the seven corpora gain their attributes this way (samples-controls 585, demo_001 and app 000; abap2UI5/samples 000, 502, 507 and 512; samples-stack 000; abap2UI5'sz2ui5_cl_ui5_app_start` and its downport; abap-cloud-gui's range
popup) - no new render error, and one new finding (samples 507, see Read
before upgrading). -
A quote in a comment no longer silences the class. The comment and
literal readers (scrub( ),blankLiterals( ),splitStatements( ))
lost their place in three shapes - a string template whose embedded
expression runs over two lines, a|in a literal inside an embedded
expression (sep = `|`), and a UTF-8 BOM in front of a*comment
line. The comment behind it was then kept as code, a'in its text
(" don't care) opened a literal that ran to the end of the file, and
most ABAP-side rules reported nothing for the rest of the class.scrub( )
now carries an open embedded expression over the line break and reads a
"inside one as a comment, a BOM is blanked, and every reader ends a
quote or backtick literal - and a template's text - at its line, as ABAP
does, so a reader that is ever wrong again is wrong for one line. The one
literalEnd( )is shared byparenRegion,topSplit,namedArgMarks,
the reconstructor's paren scan andchain-house-layout, whose own scanner
closed a template at the first|and so lost the)of a crammed chain
call behind|{ concat_lines_of( table = mt sep =|) }|. -
A
CONSTANTSvalue resolves.CONSTANTS c_base_url TYPE string VALUE \https://…`andt = c_base_url && `sample1.jpg`, the chainedCONSTANTS:form and one level ofBEGIN OF cs … END OF csread ascs-name` were dropped as values the gate cannot follow, so a URL, a
colour or an enum value kept in a constant was judged by nothing and
rendered as absent. -
A name written once with a string template resolves to it.
DATA(expr) = |\{= ${ client->_bind( flag ) } ? 'A' : 'B' \}|.and then
v = expron five controls - how a port hands one expression binding
around - was dropped (samples-controls apps 445 and 452 carried fifteen
unresolved-attribute-valuehints for it). So was a name written once with
cl_abap_char_utilities=>newline. A second write of the name in its scope
keeps it a variable, unresolved as before, and so does a template with a
piece the scan cannot compute:|{ tenths DIV 10 }.{ tenths MOD 10 }em|
would stand as the guess.em, which the render gate refuses as a
CSSSize. -
A name is read in its scope. A literal or a template the class gives
a name was read class-wide - the last literal assignment anywhere - so a
helper'sv = typecame out as main'sDATA(type) = \Bogus`, reported asinvalid-property-valueand rendered with a value nobody passed, and two methods' locals of one name both read the later one. A name that is a variable of the method it is used in - an inline or aDATA/STATICS/CONSTANTSdeclaration there, or one of its parameters - now reads that method's own writes, the last one before the use; any other name is the class's attribute and reads the class's writes to it, as before. A parameter's value is the caller's, so a helper'sv = typeis unresolved rather than borrowed. On the seven corpora no document and no finding changes from the scopes; abap-cloud-gui'sz2ui5_cl_cgui_rangeno longer carries the guessvalue="{}"for|{{ part }}|`. -
A pragma behind an argument value (
v = `Bogus` ##NO_TEXT) is no part
of it: the attribute was dropped as unresolved, and every check of its
value with it. -
A builder handle kept in an attribute and stringified in another method
is skipped, not failed. abap2UI5'snode/srv/zcl_tst_hostbuilds its
page in one method, keeps the handle inmo_main_page, lets a sub-app
created by name build into it and stringifies it in another; the replay
enters only the methods that open a factory, so the render gate failed the
class withno view reconstructed from builder calls. When the replay
yields no document, the builder calls in the methods it never entered are
counted ashelperTokens, so the class is skipped with the usual "built in
helper methods" note (abap2UI5'srender-errorwaiver for it now waives
nothing; sapgui'szcl_sapgui_frameandzcl_sapgui_se91are the same
shape). A class whose replay yields a document is unchanged. -
me->is read as the attribute it names, everywhere the reconstructor
reads a target or a handle.me->mv_type =+'Error'+rseeded nothing: the bound enum was modelled as"", and the render gate reported a falserender-erroron a correct class.me->mo_view = …factory( ),me->mo_page = me->mo_view->ele( … )andme->mo_page->tag( … )were no handle, so a view held in attributes produced no document at all. One fragment (OWNinlib/reconstruct.mjs) now stands in front of every such name - the literal and template writes of the resolver, the scalar and table seeds, the derived seeds, the factory, capture and handle chains, and the capture scan that chooses the handle-aware reconstruction - and the bare name is the key. A write spelledme->xis the ATTRIBUTE's write even where the method declares a localx.FINAL(view) = …is an inline declaration likeDATA(view) = …in the same places (INLINE`). -
The client under another name binds.
bindingOf( )knew
client->_bind( )alone;mo_client->_bind_edit( mv_name )and
me->client->_bind( mv_name )were "unresolved value expression dropped",
the attribute gone from every gate's view of the class. It reads any word
ending inclient, with or withoutme->- the reading the event rules
already had (clientArrowsinlib/abap.mjs). -
Helper declarations are read per METHODS element. Three things hid a
helper: the keyword was required in front of EVERY name, so a
colon-chainedMETHODS: render_toolbar …, add_button … RETURNING …declared
no helper; the lazyIMPORTING[\s\S]*?RETURNINGran from a void method's
declaration across its period into the next one, so a void method declared
ahead of a RETURNING helper swallowed it; and the returning parameter had
to be calledresult.parseHelpers( )reads each METHODS statement
throughsplitStatements+topSplit(',')(asparamsOfdoes), judges
every element on its own, and takes the returning parameter's name from
the signature (ro_view = io_view->tag( … )is the chain). -
A RETURNING helper's parameter is substituted in code only. The
inlined body had its parameter names replaced textually, literals
included: a parameter namedtextturnedn =+'text'+rinton =+'``Save``'+r, which parsed as no attribute at all (the Button lost
its text, and every rule that judges it with it).codeOnly( )masks the
literals - the embedded expressions of a|…{ text }…|template stay code
and are still substituted - and an argument NAME in front of=is never
the parameter. -
An element or attribute named by a constant resolves.
ele( n = c_page )
anda( n = c_attr v = … )were "unparsed": the element was dropped, its
children reparented into the element above (a falseunknown-propertyon
their attributes) and itsend( )closed that one instead (a false
excess-shut).n =is read through the resolver now (nameOf( )),
which knew the constant all along. A name that does not resolve - a
parameter, a computed string - still OPENS an element: a nameless
placeholder node (unresolvedName) keeps the tree's shape and theend( )
count right;toXml( )and the walk look through it as they do through the
document wrapper, and its attributes stay off the element above. -
A pragma after
VALUEno longer hides the declaration.DATA mv_state TYPE string VALUE 'Success' ##NO_TEXT.read as no declaration ("bound
variable has no DATA declaration - mocked as string", value lost), and
CONSTANTS c_title … VALUE 'My Title' ##NO_TEXT.as no constant. Pragmas
are blanked (blankPragmas) beforeDATA_DECL,LIKE_DECL, the
constants reader, the literal writes and the scalar seeds. -
A conditional inside a
VALUE #( )row leaks nothing. The row reader
took a seed pair by pair, sostate = COND #( WHEN mv_ok = abap_true THEN … )madeMV_OKa FIELD of the row and seededSTATEas""- a false
render error on the enum ("" is not a ValueState). A parenthesised value
(COND #( ),SWITCH #( ),CONV string( ), a method call, a bare
( … )) is skipped as one region (SEED_CALL); a conditional whose every
branch is a literal seeds its ELSE value (literalBrancheswith a
literal-only resolver), and anything else leaves the field out of the
render model - the control keeps its default, as for an unseeded scalar -
while the shape keeps the declared default so the path is still judged.
The leak hid a trueunknown-binding-pathon{MV_OK}, which the probe
class now reports. -
An
IMPORTING … DEFAULTis the value a call that omits the parameter
passes, soadd_button( io_page = page )resolvestextto the
declared+'Save'+rin the replay (defaultsOfbesideparamsOf). And a
numeric literal (v = 20) is its digits, not a name to look up: it used to
be dropped as unresolvable. -
Measured (abap2UI5
src+node/srv, 39 files, and the 100 fixtures with
--all-classes): finding counts per type, per-file stats and reconstructor
notes identical before and after; the render gate's errors identical. The
probe classes that reproduce each item are in
test/review/round-2026-10-12-reconstruct.mjs.
--fix
-
--fixruns to a fixed point. One pass applies every fix whose span
overlaps none applied before it, and a fix can leave a shape another rule
fixes (a deleted trailingt_argrow leaves the one-row table
event-arg-single-row-tablerewrites;chain-house-layoutre-lays a
chain another fix sat in) - so a run ended with "N deferred to the next
run" and the reader ran--fixtwo or three times. The CLI now repeats
the pass in memory, re-checking the files the previous pass changed (and
any whose class-index facts - superclass chain, outside reads - moved),
and writes each file once: the text the old--fixreached when it was
run until it changed nothing. Bounded atMAX_FIX_PASSES(10, ESLint's
bound, exported on./fix); a run that stops there says so. The summary
names the passes (fixed 6 problem(s) in 1 file(s) in 2 passes), and
--fix-dry-runlists a later pass's fixes with(pass N). -
--fixkeeps a CRLF file CRLF. The fixes that insert or re-lay lines
(chain-house-layout,missing-on-navigated-branch, the inserted
class_constructor, …) wrote LF, so withcrlf-line-endingswitched off a
fixed file came back with mixed line endings. Every fix now writes the
source's line ending (matchLineEndings( )on./fix, applied by the
entry points to what survives the rules and directives) - LF while
crlf-line-endingis fixing the file to LF in the same pass. And
chain-house-layoutcompared a CRLF line break as if it were wrong, so it
reported (and rewrote) every multi-line chain of a CRLF class whatever its
layout. -
chain-house-layout --fixindented a*comment inside the chain -
and a*is a comment in column 1 only. The fix moved every
continuation line of a re-laid segment by the segment's delta, a comment
line among them, so the rewritten class did not activate. A continuation
line whose first non-blank character is a comment keeps its column now - a
"comment too, whose indent is the author's and not the chain's
(lib/chain-layout.mjs). -
redundant-serializableno longer deletes a comment with the line. The
fix was anchored on the scrubbed copy, where a trailing comment is blanked
to spaces that the pattern's[ \t]*before the line break swallowed, so
INTERFACES if_serializable_object. " keep: the draft needs itwent
whole. A line the original spells differently from the scrubbed match is
reported without a fix - thesame( )rulelifecycle-rules.mjsapplies
to its own deletions - and the bare line keeps it. -
A moved declaration lands directly under its new section's last line.
moveDeclarationFix( )(binding-to-nonpublic,private-app-attribute)
inserted at the line start of the NEXT section header, below the blank line
that closes the target section, so the movedDATAstood alone under the
gap, detached from its siblings. The blank lines are walked back over and
the declaration is appended under the section's last declaration (under
its header when the section is empty); the gap keeps closing the section. -
Four rules that rewrote under
--fixare listed as fixable and say what
the fix does.unknown-view-slot(main→MAIN),
invalid-frontend-action(a filter-row operator up to letter case,
contains→Contains),association-unknown-idand
unknown-source-property(the id or$source>segment up to letter case
and separators) carried fixes without being inFIXABLE, so the rules page
showed no--fixbadge and nofixNote, and--fixrewrote what the page
said it would not touch. The filter-operator fix rides on the finding now
(asunknown-view-slot's does) instead of only on the pipeline's
attachSuggestionFixes( ), so a caller ofcheckAbapRules( )alone sees
it too. And the missing direction is gated:
test/review/round-2026-10-12-rules.mjsobserves every finding type
produced with a fix over the cards and the four shapes, and fails on one
thatFIXABLEdoes not list.
CLI, output and config
-
The class index covers every class under the configured
paths. A
class's verdict reads two facts out of OTHER classes: acs_eventit
inherits, and a public attribute another class reads back
(outsideReads). The index was built over the files the run checked, so
it changed with what the run was handed: without--all-classesthe
superclass or caller that builds no view was never in it, and a one-file
run (a pre-commit hook) saw no other class at all - both reported what the
full run, and the editor, which indexes its whole workspace, silence. The
CLI now indexes every ABAP class under the config'spaths(the run's own
paths without a config), minusignore, whatever it checks ---stdin
included, with the piped source in place of the file it names - and
--watchre-runs when one of them is saved. Cost, measured on
samples-controls (642 classes, 12 MB): a one-file run 0.25 s -> 0.58 s
(the walk 40 ms, the reads 25 ms,classIndexOf( )275 ms); a full run
unchanged within noise, since its own files are most of the index.
Nothing changes on the seven corpora. -
--stdinrefuses a path beside it (exit 2). The path was neither read
nor linted, and the run reported on the piped source as if the file had
been checked:cat a.clas.abap | abap2ui5lint --stdin b.view.xmlwas
green whateverb.view.xmlheld. The message names--stdin-filename
and--config. -
The stylish report no longer prints control characters out of the
source. A message quotes the checked file, and an ESC in a literal
reached the reviewer's terminal raw - an escape sequence that clears,
retitles or recolours it, or hides the line it sits on. The stylish
report, the annotations, the--verbosenotes, the stale-baseline lines
and the--fixlisting print every C0/C1 control, DEL and the bidi
override/isolate characters as a visible\xNN/\uNNNN(a tab or line
break as a blank);terminalSafe( )on./report. JSON and SARIF carry
the text as written. -
--format checkstyleand--format junitstay well-formed whatever a
message quotes: a control character becomes U+FFFD, and a line break or
tab is written as a character reference, which an attribute keeps. Both,
and the GitHub annotations, write/-separated paths on Windows too, as
SARIF always did - an annotation with a\path never reached its line. -
A run with nothing to check prints the EMPTY document of a machine format
(checkstyle,junit,sarif, asjsonalready did) with the sentence
on stderr, and still writes--sarif-outand--json-out: a workflow's
upload-sarif step behind the Action'ssarifinput failed on a file that
was never written. -
--update-baselinebeside a machine format writes its "baseline: wrote"
line to stderr; stdout is the document a caller parses. -
--advisory/--fail-on neverexit 0 under--max-warnings(or a
configmaxWarnings) too, as they do for stale baseline entries; the
excess is still said on stderr. -
--max-warnings,--jobsand--render-pagestake digits only.''
and' 'were read as 0 and0x2as 2, so--max-warnings "$MAX"with
the variable unset failed the build on the first warning. -
A baseline file that is valid JSON but no object (
null, a number, an
array, a string) is refused with the file's name and the shape it should
have, instead ofCannot read properties of null. -
The config's
ignorecount ("N files ignored by config") is taken in the
same directory walk; the CLI walked the tree a second time and read every
class twice.collectFiles( )takes anonIgnoredcallback for it. -
A config writes only inside its own directory. The
badgefiles (on
every run) and thebaseline(--update-baseline) a config names are
repository content, and a cloned repository's"badge": {"file": "../../.bashrc", "label": "..."}had a plainnpx @abap2ui5/linterin it
overwrite that file with JSON whose label the config chose. A path that is
absolute or leaves the config's directory is now refused when the config
is loaded (exit 2, the message names the key), and one that leaves it
through a symbolic link the repository ships is refused when it would be
written.--badge,--badge-corpusand--baselineon the command line
still write wherever they point. Every downstream config names files
under its own directory and is unaffected. -
A UTF-8 BOM no longer shifts the columns of line 1 by one.
-
isXmlSource( )on the main export: whethercheckFiles( )reads a
source as a raw view or as a class - the one predicate the library, the
--fixpasses and the cache's class index share. -
--update-baselinedoes not write the default baseline through a
symbolic link out of its directory. Without--baselinethe file now
lands next to a discovered config, in the repository rather than the
cwd, so a cloned repository that shippedabap2ui5lint-baseline.jsonas
a dangling link (nothing to parse first) had the update write the
baseline JSON - whose keys carry the repository's own file names - to
wherever the link pointed, a shell start-up file included. That is
refused now (exit 2), as for a config-named baseline (below); a file
named with--baselineis still the user's and is written where it
points. -
cli.mjshousekeeping, no behaviour: oneownPackage( )/ownVersion( )
reads package.json (it was parsed three times: the schema URL,--version,
the cache key), oneshownPath( )prints a path relative to the cwd where
that is shorter (configShownin the report andnicein the watch loop
were the same function), and the baseline block is onebaselineStep( )
shared by the normal run and the empty walk. -
lib/config.mjs's key list documentsbaseline, which the loader has
accepted since 0.8.x. -
@abap2ui5/linter/schemais typed. The exports map has served
data/abap2ui5lint.schema.jsonas./schemaandtypes.d.tsdeclared
every subpath but that one, so a TypeScript consumer importing it got an
implicitanyunder--strict.declare module "@abap2ui5/linter/schema"
exports it asRecord<string, unknown>, and the typings gate innpm test
now requires adeclare moduleblock for EVERY key of the exports map -
the JSON ones too, which the previous filter (typeof v === 'object')
never asked about.
--cache
-
The cache file is never written through a symbolic link. It sits in
the working directory - the repository - so a repository that shipped
.abap2ui5lintcacheas a link to a file of the user's, beside
"cache": true, had every run overwrite that file. A link there is now
refused with one line on stderr (the run itself is unaffected, as for any
cache that cannot be written). -
--cachefollows the class index and the linter's own code. A class
is judged against what its superclass declares (cs_event) and what the
other classes read of it (outsideReads), and a cached run built that
index out of the cache MISSES only: a subclass re-checked without its
cached superclass read the inherited constant as unknown, and an edited
superclass left its subclasses' stale entries in place. The index is built
over every file of the run, and each entry is also keyed on the index
facts its check read (deps,classIndexDeps( )). The context hash takes
a fingerprint ofcli.mjs,lib/*.mjsanddata/*.jsonbeside the
version, so a checkout between releases or a regenerated snapshot does not
replay the verdicts of the code before it. A run over part of a tree keeps
the entries of every other file (and drops those whose file is gone), and
the per-document models (docModels) are no longer stored. The cache file
format moves to version 2; an old file is simply a cold cache.
The index thedepsare read from is the wider one of the CLI entry
above, so editing a class the run only indexes - a helper that stops
reading a popup's result - re-judges the popup too.
Render gate
-
An answer that arrives after a document's render window is charged to
that document. A control that sends a request (a Card fetching its
manifest) reports what comes back whenever it comes back, and the page
meanwhile rendered the next document, whose window collected the error -
samples-controls apps 118 and 168 failed app 180, 183, 184 or 185
depending on the run. The harness counts the fetch/XHR requests a page has
in flight and lets them settle into the window of the document that sent
them; a page still waiting aftersettleTimeout(openRenderer( ),
defaultRENDER_SETTLE_TIMEOUT_MS, 2 s) is reloaded before the next
document, so a late answer can be lost but never misfiled. Not covered: an
error a control raises from a bare timer long after its view is gone. -
The browser's own network error is waived. A Card whose manifest lives
on sdk.openui5.org fetches it when it renders; where that host does not
answer (a sandbox, an offline runner)TypeError: Failed to fetchis
environment noise like the other waived messages - whether a remote URL
answers is not a property of the view. What the control says about the
missing resource is still reported. -
A raw
.fragment.xmlrenders as a fragment. The gate decided view or
fragment by^<core:FragmentDefinitionon the text, so a fragment file
that opened with an XML declaration or a comment, wrote
FragmentDefinitionin the default namespace, or had a bare control as
its root failed with "XMLView's root node must be 'View'". The file name
decides now (.fragment.xml/.view.xml, on the result'sdocKindsand
for--screenshot), and the renderer's own fallback reads past a BOM, the
XML declaration, comments and a doctype and accepts any prefix. A leading
BOM is dropped before the page parses the document, as the decoder drops
it when the runtime loads the file: Chrome 153 (Playwright's chromium
1243) refuses U+FEFF ahead of<?xml, and UI5 reported the parser's
error document as "Using native HTML content in XMLViews is deprecated".
Performance
-
Linear on long input. Measured quadratic and now linear:
default-key-table(a class of 10,000TYPE TABLE OFlines took a
minute, also without a terminator behind the declarations), the PUBLIC
attribute reader andunbound-public-attribute(three regex scans of the
class per attribute),client-handle-capture(a scan per captured
handle),commercial-ui5-host(a regex tried after every-of one long
hyphenated token) and ten regexes anchored on^\s*under the multiline
flag (the model's literal seeds and table seeds, the section DATA blocks,
the event dispatch,declaresApp( ), …), which let every blank line -
and every blanked comment line - start a scan over all the ones behind
it. That last one is what the VS Code extension met:prepareAbap( )runs
on every edit, and 2,000 commented-out lines took a second in 0.8.5, 8,000
sixteen. The findings are unchanged on the corpora;test/timing.mjs
judges each case by n against 4n, so a loaded runner cannot fail it. -
The class index (
outsideReads) finds typed references by theirTYPE REF TOkeyword instead of trying a name at every word of every class. -
Every rule is swept for growth.
test/review/timing-sweep.mjstakes
each rule's card - the example the rules page wraps into a class and
verifies - and grows it around the reported line in nine shapes (a long
run of blanks, a long literal, a long comment; a block of comment lines,
of declarations, the statement repeated, the method copied; nested IFs and
a nested view), every case judged n against 4n (screenedLinearly( )in
test/timing.mjs: one run each, the best of three only where that does
not pass), plus four raw-view shapes. What it and the shapes behind it
found, all three linear now: a class of many views with unused namespace
declarations counted each prefix over the whole source once per finding
(200 views, 5.7 s); an unclosed call handed the rule reading it the rest of
the file as its argument list, so 2,000 of them took 11 s (parenRegion( )
ends such a list at the statement's period now, and reads a"or*
comment as a comment); andunescaped-text-in-attribute's write reader
split a long blank run after a statement's first word every possible way
(CLASS-METHODSand 32,000 blanks, a second). No finding changes on the
seven corpora. -
Three per-item RegExp rescans read once.
navigation-lost-on-rebuild
rebuilt two patterns and rescanned the class for every bound field of
every navigation wire (literalsOf( ), memoized per field);
omit-initial-drops-falsebuilt one per boolean column peromit_initial
bind (the declared boolean names are read once per class);
manual-init-flagbuilt the= abap_truepattern perIF(cached per
flag). Same verdicts, measured on both corpora.
Action
-
render: Falsemeans false. GitHub compares strings in anif:
case-insensitively and the shell does not, sorender: Falseskipped the
runtime install and then ran the gate anyway, andannotations: False
annotated. Both inputs are lower-cased in the shell, in the lint and the
screenshot step. -
The Chromium cache works on Windows and macOS runners. The cache step
saved~/.cache/ms-playwright, Playwright's browser directory on Linux
only, so elsewhere every job downloaded Chromium again. The install, lint
and screenshot steps setPLAYWRIGHT_BROWSERS_PATHto one directory under
runner.temp, and that is what is cached (a Linux runner misses the cache
once). -
The Action's
exit-codeoutput describes exit 2 too: the run did not
happen (bad usage, a bad config, a missing render runtime), and the count
outputs are empty then.
Metadata
-
The snapshot is OpenUI5 1.153.0 (from 1.152.0):
data/properties.json,
data/icons.jsonand the@openui5/*pins of@abap2ui5/linter-render
moved together (RELEASING.md step 1c). One control is new
(sap.ui.core.tooltip.TooltipFocusGuard) and three enums
(sap.m.InputDescriptionAlign,sap.ui.core.popover.PopoverPlacement,
sap.ui.table.ShowScrollHandle); the icon font is unchanged (707 names).
A member new in 1.153 was anunknown-*"typo?" finding and a render
failure; it is now*-too-newagainst a lowerminUi5, which a consumer
can accept. A@abap2ui5/linter-renderof 0.8.x serves 1.152 and is named
on stderr as a version gap (runtimeSnapshotMismatch): install the 0.9.0
runtime with this release. -
The generator gives a class the methods it owns wherever the file
writes them. sap/ui/unified/ColorPicker.js defines the private
_ColorPickerBoxbetween the picker's extend call and the picker's own
methods, and a file was read "from one extend call to the next": the
picker'sfireChange({ …, colorString })was harvested for the box, so
$parameters>/colorStringon a ColorPicker - what the demo kit sample
reads - was anunknown-event-parameter.data/properties.jsonnow
carriescolorStringandformatHSLon the picker'schangeand
liveChange; no other class moved. -
data/compat.jsonsays the mirrors are synced against abap2UI5 1.147.0
(framework.mirrored, was 1.146.0 - the mirrors had been synced, the record
had not;npm run generate-compat -- --local <abap2UI5 checkout>is the
step that was skipped). app-template'scheck-pinand the VS Code extension
read this field, so a 1.147.0 app stops reading as "newer than the linter
knows". The weeklyupstream-syncworkflow now compares the field against
upstream'spackage.jsonversion and prints a warning line on lag - a
warning, not a failure or an issue: a version bump upstream that touches
none of the four mirrored files is not drift, and that workflow files issues
for drift only.
Also in this release
-
./check: the entry point without the renderer.checkAbapSource( )
andcheckXmlSource( )move tolib/check.mjs, exported as
@abap2ui5/linter/checktogether with what they decide with -
declaresApp,VIEWLESS_APP_RULE,sizeLimitRaised,
standDownUnusedNamespaces(theunused-namespace-declarationstand-down,
a function now instead of a block insidecheckAbapSource( )),
frozenBuilderOf/FROZEN_BUILDERS,isXmlSource,isAbapGitXml,
xmlFileKind. The main entry re-exports all of it unchanged; what it adds
ischeckFiles( )with the worker pool and the renderer (http,os,
module), which a browser bundle cannot resolve - so the VS Code
extension's web build re-implemented each of those decisions and pinned
every copy to this source with a test of its own. Both entry points take
data(the snapshot itself, for a host without a path - from the new
snapshotFromJson( )on./properties, which reads data/properties.json's
text or object the wayloadSnapshot( )reads the file) andprep(a
prepareAbap( )of the same source the caller already holds;
checkFiles( )ignores it).npm testimports./checkin a fresh process
with a resolve hook and fails on the renderer, the entry point, a
worker_threads/http/os/moduleimport or anything of the render
runtime. The pool's threads load./checktoo, not the renderer. -
A view nested 5,000 levels deep is judged. Every walk over a tree
recursed once per level, and the property walk ran out of stack at about
a thousand:RangeError: Maximum call stack size exceededout of the
middle of a run - from the namespace scopes, the main walk,toXml( ),
the portable walk and the namespace stand-down in turn. They go through
lib/tree.mjsnow (walkTree( ),nodesOf( )), which keeps the
recursion's order without the recursion, so every finding comes out where
it did. What the browser makes of such a document is the render gate's to
report, as a render error: UI5 itself runs out of stack between 400 and
1,000 levels, and the browser's XML parser refuses 5,000. -
The first display is modelled. The model a view is judged and rendered
against took its scalar seeds from the whole class, so a value only an
event handler assigns was rendered as if it were there from the start.
samples-stack app 013 bound a MessageStriptypeto an attribute seeded
Errorin aCATCHblock: both gates renderedError, every real start
shipped""and terminated the app. A document built on the start path -
main( )ahead of its dispatch, thecheck_on_init( )arm (or the
check_on_navigated( )arm of a chain without one), the constructor and
what they call - is now judged and rendered against a model seeded from
that path only (initModel,initialFields,docOnIniton
prepareAbap( ),docModelson the result). A popup built in a handler,
and every class with no such dispatch, keeps the class-wide model.
Measured on six corpora (samples-controls, samples-stack, abap2UI5, popups,
sapgui, abap-cloud-gui): no new finding, no new render error. -
New rule
enum-bound-to-initial-field(error), the root-field twin of
enum-field-unset-on-insert: an enum-typed property bound to an attribute
the start path gives no value - no literal seed, no declaredVALUE, no
other write. The first display ships"",validatePropertythrows and the
app terminates, hidden control or not. A field the start path writes
non-literally, one bound underomit_initial, and a document the first
display does not show are not judged. -
New rule
rows-hidden-by-visible(warning). Theitemsof a
non-growing list (sap.m.ListBase: List, Table, GridList, Tree) bound
without alengthand with a binding-valuedvisibleon the row template,
in a class that raises no size limit: a JSONModel hands such a binding at
most 100 entries and the hidden rows are among them, so the list stops
short of its data without a word. samples-controls' overview app listed a
few dozen of its 622 ports that way. The narrow successor of the
bound-aggregation-over-size-limitrule dropped in 0.3. Where the limit
does not apply it is silent: a growing list pages with lengths of its own,
a binding info'slengthis passed through, and a raw view has no class
to ask. Neither is a MultiInput'stokensor a layout container's
repeateditemsjudged - a value set or a repeater, not rows of data; the
first version reported the hidden tokens of abap2UI5/samples app 078, of
popups'z2ui5_cl_popup_get_range_mand sample 19 and of abap2UI5's
frozen copy, its only hits on seven corpora. -
New rule
invalid-css-value(error, fixable). A size, colour or
percentage literal its UI5 type refuses -width="100",100 px,
calc(100%-2rem),fit-content, a capitalised colour name. UI5 checks
sap.ui.core.CSSSize,AbsoluteCSSSize,CSSSizeShortHand,CSSColor
andPercentagewith one regex each; the XML parser only logs a refused
value, then the control's setter throws and the whole view fails to load.
The regexes are harvested into the snapshot (typePatterns, every
string-based DataType whoseisValid( )is one regex test) and are the
same from 1.71 to the pinned release.--fixrepairs only the mechanical
cases: surrounding blanks, a blank between number and unit, a colour name's
case. A bare number keeps the finding without a suggestion. -
New rule
unknown-binding-type(error, fixable). A binding info's
type:(by global name) or a core:require module path in
sap.ui.model.type/sap.ui.model.odata.typethat names no model type.
By global name the binding runs without its type (the raw value is shown,
input is never parsed back); through core:require the module 404s and the
view is never created.sap.ui.model.type.Decimaland.Numberare the
plausible names that do not exist. The snapshot lists both namespaces
(modelTypes); a type of the app's own, a relative.MyTypeand an alias
are not judged (the alias at its core:require). A name that matches a type
up to letter case is rewritten by--fix. -
New rule
binding-type-too-new(warning), the version half of the
same check: a model type newer than the floor
(sap.ui.model.odata.type.DateTimeWithTimezone, @SInCE 1.99), read from the
class's own JSDoc intomodelTypes. -
The metadata snapshot gains two sections, both additive:
typePatternsandmodelTypes(loadSnapshot( )attaches them as
__typePatterns/__modelTypes). A snapshot without them - one a
consumer generated with an older generator - leaves the three rules above
silent. A consumer that builds the snapshot object itself rather than
throughloadSnapshot( )(the VS Code extension's web host) has to attach
the two the same way to get them. -
portable-app: the vendored profile follows protocol revision 0.3
(604d267) - the client-API names are read fromactions.api, the names a
raw view's.eF( )may use fromactions.wire.custom. -
New opt-in rule
portable-app(warning once asked for). Reports
everything in an app outside the abap2UI5 protocol's portable profile v1
(abap2UI5/protocolprofiles/portable.md) - what keeps the app from
running unchanged on a non-UI5 frontend such as the UI5 Web Components
frontend: controls and members the profile does not list,z2ui5.cc
custom controls, named models and other binding forms, expression
constructs outside its grammar, event wires and arguments ($event,
prevent_default_expr), frontend actions (CONTROL_BY_ID, …), the nested
view slots (decision Q1) andview_display( switch_default_model_path ).
One id, areasonper finding. Off until"rules": { "portable-app": "error" }. The profile is vendored verbatim asdata/portable-v1.json
(source commit indata/portable-v1.source.json, refreshed with
npm run sync-portable-profile, drift reported by upstream-sync), and
the rule reads both the current and the Q10 shape of its frontend actions.
New export./portable(checkPortable), new optionportableProfile. -
z2ui5_if_ui5_monitoris released API. abap2UI5 adds a roundtrip
monitor seam to its released packagesrc/02(an addon such as
abap2UI5-addons/admin-cockpit implements it to log usage, timing and
errors). Unlisted,non-released-apiwould have reported every
implementing class, since the name falls under the internal
z2ui5_if_ui5_*prefix. Merge together with the abap2UI5 change, or
check-upstreamreports the extra name as drift until it lands. -
ExcelBridgeis mirrored. abap2UI5 ships a new companion control,
z2ui5.cc.ExcelBridge(the workbook bridge of an Excel add-in,
abap2UI5/office-addin). Unmirrored, a view naming it would fail view
CREATION in the render gate with a module 404 and the property walk would
look away from it;check-upstreamreports the drift until both land. -
NativeBridgeScanis mirrored. abap2UI5 ships a new companion
control,z2ui5.cc.NativeBridgeScan(the scan button of the native mobile
shell, abap2UI5/mobile-shell). Unmirrored, a view naming it failed view
CREATION in the render gate with a module 404 and the property walk looked
away from it;check-upstreamreported the drift. -
The property gate is more than twice as fast on a corpus. The
samples-controls corpus (642 classes,--no-render) took 15.2 s and takes
6.6 s, with byte-identical results on it, on abap2UI5/samples and on
abap2UI5. The source readers no longer rebuild strings one character at a
time, a call's argument list is parsed once instead of a dozen times, two
whole-class scans per attribute or per match are gone, and
checkFiles( { jobs } )spreads the files over worker threads (one per 64
files) and returns the results in file order. The CLI's new--jobs <n>
defaults to the machine's cores, at most 4;--jobs 1keeps one thread.
The library default is 1. -
New rule
unknown-message-box-type(warning, fixable).
message_box_display( type = … )takes the sap.m.MessageBox display
methods -information,warning,error,success,confirm,alert,
show- and the server silently falls back to a plainshow( )box for
anything else: an error writtentype = \E`opens with no icon and no title. The four message-type letters with a box of their own (E, W, S, I) are rewritten by--fix. The list is mirrored fromct_box_typeand gated bycheck-upstream`. -
New rule
bind-path-as-value(error, fixable)._bind_path( x )and
_bind( val = x path = abap_true )return the bare path for a binding the
app assembles itself; as the wholev =of an attribute they make the
attribute the TEXT/X.--fixturns the call into_bind( x ). -
New rule
html-content-not-markup(error, fixable). A
sap.ui.core.HTMLcontentthat does not open with a tag is read by
jQuery as a selector: nothing renders, or rendering throws.--fixwraps
plain text in a<span>. -
New rule
navigation-lost-on-rebuild(hint), the navigation twin of
control-state-lost-on-rebuild: a NavContainer, SplitApp or
FlexibleColumnLayout moved withto/backToPage/toDetail/toMaster
only from a handler, while a bound field keeps the page - after the next
view_display( )the container is back on its initial page and the field
names another. Silent when a display-path method navigates the same
container (abap2UI5 backlog itemnavcontainer-position-not-reissued). -
raw-javascript-to-frontendsees two more shapes: an inlineon…=
handler in markup (core:HTMLcontent) and ajavascript:URL in a
URI-typed property. The default CSP refuses both, so neither ever ran. -
Three rules gained a
--fix.abapdoc-html-tagescapes the tag's
brackets as</>;control-call-arg-kindrewrites abool
argument spelledabap_true,x,TRUE,abap_falseorFALSEto the
spelling the frontend reads;duplicate-propertydeletes a second
attribute write that repeats the first word for word (two different values
keep the finding). -
frontend-action-as-backend-eventreads an inheritedcs_eventas the
class's own. A class declaring its backend events asCONSTANTS: BEGIN OF cs_eventwas already exempt, but a SUBCLASS reaching the same
constant was not: abap2UI5-addons/rap-ext's worklist,INHERITING FROM z2ui5_cl_rap_list_report, was warned onclient->_event( cs_event-back )
and even onclient->_event( z2ui5_cl_rap_list_report=>cs_event-back ),
failed the gate, and had to copy the constants into a local variable. A
class-qualified<class>=>cs_event-…is now that class's constant, the
client's only forz2ui5_if_client; and the superclass chain is resolved
through the other files of the run (checkFilesbuilds the index,
classIndexOf( )from./abap-rulesfor a caller judging one source at a
time, passed asclassIndex). A superclass the run does not have might
declare one, so the bare spelling in such a class is not reported; a
chain known to the end, orINHERITING FROM object, is judged as before. -
A binding assembled in place is no longer "fixed" into text.
unescaped-text-in-attributeread a name's origin from its=
assignments only, so a binding info built with
CONCATENATE `{path:'` lv_path `'}` INTO lv_bind(around a
client->_bind( … path = … )result) looked like a name nothing writes -
data by definition - and--fixmoveda( v = lv_bind )ontot, whose
escape_literal( )turned the Tree'sitemsbinding into text: a silent
runtime break made by the fixer (oblomov-dev/cloudy-sapgui). The rule now
also readsCONCATENATE … INTO x,x &&= …,REPLACE … IN x WITH …and
me->x = …/ls_row-f = …as writes; in a statement that assembles a
string (those, or an operand of&&) a literal holding a brace counts as
binding vocabulary, and a name the write reads carries its own writes'
vocabulary along (lv_bind = lv_path && …). A brace literal assigned
whole (label = `{/X}`) is still data. -
missing-on-navigated-branch --fixno longer swallows a popup's
result. A called app that leaves with an event (nav_app_leave( event = … ), how the confirm and select popups hand back their answer) raises
check_on_navigated( )AND that event on the same roundtrip. The fix wrote
itsELSEIF client->check_on_navigated( ).right behind the init branch,
ahead of the event arms, so the fixed class took the navigated arm and
never sawPOPUP_TRUE(found migrating abap2UI5-addons/popups onto 0.8.5).
In a class that calls other apps the arm is now written LAST in the chain,
where it only catches roundtrips the chain ignored before; a chain ending
inELSEthere is reported without a fix. A class that calls no app keeps
the arm right behind init, as before. -
Neither navigated fix copies a display of a branch-local variable.
client->view_display( view->stringify( ) )next toDATA(view) = …in the
init branch was copied into the new arm, where it compiles (an inline
declaration is method-wide) and dereferences an initial reference on the
first hop back. Such a statement is now reported without a fix, in
missing-on-navigated-branchandmissing-view-display-on-navigatedalike. -
missing-view-display-on-navigatedreads a negated guard as a guard.
IF client->check_on_navigated( ) = abap_false. RETURN. ENDIF.was judged
as a navigated branch that never re-displays - its branch is the roundtrip
that did NOT navigate, and the re-display stood right below it. A negated
condition whose branch leaves the method is now judged on the rest of the
method (reported without a fix when nothing there displays); one that does
not leave is not a navigated branch. -
@abap2ui5/linter-renderasks forplaywright ^1.64.0. The workspace
manifest still said^1.61.1while@abap2ui5/mcp-server, samples-controls
and the playground had moved on, so a project installing the render
runtime next to one of them resolved TWO Playwright versions - two
packages in the tree and two Chromium downloads for one browser. The range
now follows the sibling repositories (^1.64.0, as@abap2ui5/mcp-server
asks), and one install shares the one Playwright. The@openui5pins moved
with the metadata snapshot (see Metadata), not as a dependency. -
The package size the docs quote is measured, not remembered. README,
AGENTS.md, RELEASING.md and release.yml said ~340 kB (release.yml ~240 kB);
npm pack --dry-runproduces a ~600 kB tarball (44 files, 2.2 MB unpacked -
data/properties.jsongrew with every snapshot and nothing counted). All
four say ~600 kB, and a new gate (test/review/round-2026-10-12-hygiene.mjs)
reads the README figure, requires the other three to agree with it, and
fails when it is more than 25% off the tarballnpm packproduces. -
Workflows:
downstream.ymlno longer cancels an in-progress run on
main - the samecancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
asci.yml, so main's runs stay the record of what landed while a
superseded branch push still frees its runner. Thepackagejob's
npm audit --omit=devcomment now says what it audits: the production set
of BOTH published packages (the linter has none; the linter-render
workspace's @openui5, playwright and less-openui5 are in the root lockfile),
not "the linter minus an optional peer" ---workspaces=falsewas measured
and does not narrownpm audit, so the flag was not added. -
Docs: the README's "Working on this repository" block no longer runs
node cli.mjs src(a directory this checkout does not have - it answered
no such file or directory); it lintstest/fixtures --no-renderand shows
the sibling-app form. CONTRIBUTING.md namesnpm run checkas what a pull
request must pass (npm testplus the github-app dry run) and lists
generate-compatwith the other gated generators;test/review/README.md
describes what the directory holds today (review rounds, audits, corpus
sweeps, topic files) instead of "the 2026-09 review round".