Repository navigation
v0.7.0
-
The linter judges against OpenUI5 1.152.0. The render runtime's
@openui5/*pins,data/properties.jsonanddata/icons.jsonmove
together from 1.151.0 (RELEASING.md step 1c). A member released in 1.152 -
sap.ui.unified.DateTypeRange.ariaHasPopup,sap.f.HeroBanner- exists
now, where it was anunknown-property/unknown-controlplus a failed
render; a consumer that pins its runtime to the linter's metadata version
(samples-controls' check-pins policy 5) can move to 1.152 with this
release. -
Two more false positives the samples-controls corpus turned up.
association-unknown-idno longer reports an id of the form
<known id>-<suffix>: a control renders its internal parts under its own
id plus a suffix, and port 565 labels its Popover bymaster-title, the
title ofsap.m.Pagemaster, exactly as the demo kit original does.
column-cell-count-mismatchno longer counts a row template whose cells
are filled in exclusiveIF/CASEbranches: the reconstructor replays
every branch, so port 570's edit and display templates (four cells each)
came out as one template with eight. The replay now marks a held
container filled inside a branch (branched), and the rule stands down on
it. The other findings on that corpus were real and are fixed in the
ports (samples-controls: wronglabelFortargets copied from the
originals, template text throughvinstead oft), and the Downstream
job now runs samples-controls' view gates withVIEW_GATES_LINTER=next, so
that repository judges this unreleased linter against a budget recorded for
it rather than against the one its pinned release keeps. -
bound-aggregation-without-templateno longer reports
sap.ui.table.Table'srows. That aggregation is bound without a
template by design: the table builds its rows from each column's
template, and UI5 sets_doesNotRequireFactoryon it so
bindAggregationskips the "Missing template or factory function" check.
The rule did not know the flag, and reported four samples-controls ports
(115, 137, 164, 174) that load and render fine — which is what kept the
Downstreamsamples-controls corpusjob red on main. The same exemption
covers every aggregation UI5 marks that way (sap.ui.core.util.Export.rows,
sap.gantt.GanttChartBase.rows/relationships,
sap.suite.ui.microchart.LineMicroChartLine.points,
sap.viz.ui5.data.FlattenedDataset.data), and through inheritance
TreeTableandAnalyticalTable. The flag is set in code after the class
is defined, so the metadata snapshot does not carry it; the list sits next
to the rule inlib/properties.mjs. -
The mirrors follow abap2UI5's 2026-09-22 removals
(abap2UI5/abap2UI5#2776, #2777).check-upstreamwas red against
abap2UI5 main, and with it abap2UI5's owncheck_gates. Every entry here
was a name that no longer exists upstream, which is the worse direction of
drift: the linter passed a wire or a call that now does nothing, or does
not compile.- The dispatch table loses
KEYBOARD_SET_MODE(theinputmodeaction,
replaced byz2ui5.cc.InputExt's bound property, soINPUT_MODESand
itsinvalid-action-payload/ACTION_ID_SLOTSentries go with it),
IMAGE_EDITOR_POPUP_CLOSEandZ2UI5(removed with thez2ui5global).
A literal naming one is anunknown-frontend-actionnow. FRONTEND_EVENT_ALIASESloses the five*_NAV_CONTAINER_TOnames: the
constants are gone fromcs_event, and so is the server remap into a
CONTROL_BY_IDtowire.POPUP_CLOSE/POPOVER_CLOSEstay.lib/released-api.mjsloses the released structurez2ui5_t_02
(nothing in the ecosystem named it; it falls into thez2ui5_t_table
family now) and the frozenz2ui5_cl_pop_js_loader.obsolete-event-constantis withdrawn before it was ever released.
It judged the run under the"obsoletelabel ofcs_event, and
abap2UI5 removed every constant of that run, and the label, three days
after the rule landed. A class naming one no longer compiles, which
abaplint reports, and a rule that cannot fire is one this suite rejects
(every rule has to fire somewhere, every example has to fire its rule).
No released version carried it, so no configuration can name it.test/fixtures/cs_event.intf.abapis re-synced with the interface.
- The dispatch table loses
-
The windows-latest leg of CI is green again. Both
--watchsections
failed there from the day the flag landed, on their last assertion each:
"Ctrl+C ends the watch with exit 0 (got SIGINT)". Not a defect in the loop- every other assertion in those sections passes on Windows, so the first
run, the re-run on a new file, the re-read config, the survived broken
config and the reused browser are all covered there. It is that a PARENT
PROCESS cannot ask for a Ctrl+C on Windows:subprocess.kill()ends the
target with TerminateProcess whatever signal name it is given, so the
child never reaches its handler and the exit event carries
{ code: null, signal: 'SIGINT' }. (A real Ctrl+C in a console does reach
a Node process there - the runtime synthesizes SIGINT from the console
control handler - which no test can generate from outside.) The exit code
is asserted where it can be asked for, and the half that holds everywhere -
the loop ENDS rather than hanging on the watcher handles it keeps open - on
every platform. Nothing about the product changed.
- every other assertion in those sections passes on Windows, so the first
-
redundant-serializable(a hint, fixable): a class that declares
INTERFACES if_serializable_object.besideINTERFACES z2ui5_if_app..
z2ui5_if_appincludes it - the framework persists the app instance with
CALL TRANSFORMATION idbetween roundtrips, so an app that cannot be
serialized is not an app - and the second declaration compiles and changes
nothing. What it costs is the reading: an app class is the most copied piece
of ABAP in this ecosystem, and the extra line says that serialization is
something an app has to ask for, so the next class carries it too. The names
are read out of the INTERFACES STATEMENTS rather than with the usual
INTERFACES z2ui5_if_appguard, which does not see the chained
INTERFACES: if_serializable_object, z2ui5_if_app.- the shape that carries
the redundancy most often.--fixdeletes the line where it is a statement
of its own; the chained form is reported without one, since deleting a name
out of it would leave a dangling comma. -
Three more rules, and
live-event-roundtriplearns the framework's own
remedy.second-root(an error: a second element at the document's top
level — the split chain after a standalonefactory( )., whose variable
holds the root so the next statement adds a sibling of themvc:View;
the render gate rejected the document as native HTML, the property gate
never said why),message-box-removed-parameter(an error: the pure UI5
options abap2UI5#2748 took offmessage_box_display( )and
message_toast_display( )— a call naming one does not compile on a
current pin; no fix, the value moves into the CONTROL_GLOBAL option
object),smart-variant-without-init(a warning: a SmartVariantManagement
in the view and nocs_event-smart_variant_initwire in the class —
saving a variant fails inside sap.ui.fl with nothing red). And
live-event-roundtripjudges everylive*event now, is silent for a
wire carryings_ctrl-check_queue_last, and carries a--fixthat
writes that flag withcheck_no_busy(a positional event name becomes
val = …to make room). -
Four more abap2UI5-specific rules.
loop-work-area-bound(a
_bind( )on the work area of aLOOP AT— an attribute work area binds
ONE path for every row, a local or a field symbol a path the model never
carries; a warning, with a--fixfor the ASSIGNING shape into the
framework's own cell bindingtab = … tab_index = sy-tabix, and only
while nothing between the loop header and the call has movedsy-tabix),
bound-aggregation-without-template(an error:bindAggregationthrows
"Missing template or factory function" and the whole view fails to load),
unknown-source-property(the$source>/nametwin of
unknown-event-parameter— the reconstructor now recordssourceParams
besideeventParams; a hint with a did-you-mean), and
editable-control-without-binding(an Input, CheckBox, Switch, Select …
whose value property is neither bound nor read back by an event — a hint,
judged per family throughINPUT_FAMILIESinproperties.mjs; a control
disabled or read-only by a literal stays out). Measured on abap2UI5's own
app classes and on app-template: 0 findings for all four. -
Six more fixes.
client-handle-captureinlines the call where the
captured name is read exactly once as an attribute value, and deletes the
capturing statement;missing-view-display-on-navigatedcopies the init
branch's single display statement into the navigated branch (the same
scannermissing-on-navigated-branchuses, shared now as
singleDisplayStatement( ));unknown-binding-pathcarries a did-you-mean
and a fix where the missing segment is a field of that level up to letter
case (pathProblem( )names the segment and the keys it was judged
against;pathFinding( )turns that into the pair, and the finding itself
carries no key list);excess-shutdeletes the->end( )it sits on;
insecure-asset-urlrewrites a LOADED uri tohttps://(thehrefhint
keeps its finding); andloop-work-area-boundas above. -
Four abap2UI5-specific rules, and the split that produced them. The
linter keeps the checks about an abap2UI5 app and its view; rules about
ABAP as a language go to abaplint (abap2UI5'sbacklog/ABAPLINT.mdcarries
the stock — nine items filed there in the same round, each measured on
abaplint 2.120.52 first). What was left for here:
handler-without-event(the inverse ofevent-without-handler: aWHEN
nothing raises — a hint, judged only when every raise and every handler is
a literal, the class raises at least one event itself and never hands its
client to another object),binding-to-expression(a_bind( )on a
literal, a method call or a constructor expression — the framework derives
the path from the ATTRIBUTE it is handed, and a temporary is found nowhere;
an error),association-unknown-id(the other half of
binding-on-association: alabelFor/initialFocus/ariaLabelledBy
naming an id no control of the document declares, judged only when every
id in it is a literal),column-cell-count-mismatch(aColumnListItem
with a different number of cells than itssap.m.Tablehas columns —
mapped by index, rendered shifted, nothing logged). Measured on abap2UI5's
own classes and on app-template before shipping: 0 findings for all four.
A fifth,obsolete-event-constant, was withdrawn before release — see the
entry on the 2026-09-22 removals above. -
Six fixes on rules that had none, each mechanical or absent.
missing-on-navigated-branchwrites theELSEIF client->check_on_navigated( ).
branch with a copy of the init branch's own display statement — only when
that branch has exactly one statement at its level that displays;
separate-lifecycle-ifsfolds ADJACENT blocks into oneELSEIFchain (a
block with anELSE, or statements between two blocks, keeps the finding);
binding-to-nonpublicandprivate-app-attributeMOVE the single-line
declaration into the section the remedy names (private-app-attribute
renamesPRIVATE SECTION.outright where the class has no PROTECTED one),
a chainedDATA:element keeping the finding;hardcoded-binding-path
rewrites a value that IS the literal{/NAME}of a public attribute to
client->_bind( name );external-link-without-targetwrites the
targetcall behind the href's own, on a line of its own in the house
layout. Moving a declaration is the first fix made of two spans (a delete
and an insert) —fix.mjsapplies them like any other non-overlapping
pair. -
Did-you-mean on the five closed sets that had none.
unknown-model
(Device>fordevice>),uncurated-formatter,unknown-view-slot
(mainforMAIN),unknown-frontend-action(set_titlefor
SET_TITLE) and the filter-row operator ofinvalid-frontend-action
(containsforContains— the set is case-sensitive upstream, and the
miss leaves the binding unfiltered) now carrywritten/suggestionwhere
the written name is a member of the set up to letter case, and
attachSuggestionFixesturns that into the fix. No edit distance, as
before: a case miss is not a guess.unknown-view-slotand
invalid-frontend-actioncarry the fix without joiningFIXABLE: their
card examples (NESTED, a wrong global target) are the lesson and are not
case misses, and a card must not promise a fix its own example declines. -
New rule
event-arg-single-row-table— aclient->_event( )whose
t_argis aVALUE #( ( x ) )with ONE row, wherearg = xsays the same
thing. The client foldsarginto the samestring_table, so the wire and
the handler'sget_event_arg( )are byte for byte unchanged; the single
argument is what most wires carry (a row key, a${$source>/…}, one event
parameter) and there the table constructor is longer than the value inside
it. A hint with a--fix: thet_arg = VALUE #( ( x ) )span becomes
arg = xin place, so a two-line call collapses onto its first line and the
continuation keeps its column. From two rows ont_argis the right
parameter and the rule is silent, as it is when both spellings are passed
(the documented composition appendsargbehind the rows, so the call sends
one more argument, not the same one) and on a row containing a#:argis
generic (TYPE clike), so aCONV #( )or nestedVALUE #( )has no type
to derive#from and the remedy would not compile. Only
client->_event( )is judged —_event_client( )andfollow_up_action( )
are frontend actions and have noargparameter at all. Measured on the
corpora: 17 findings in abap2UI5/samples, 9 in samples-stack, 1 in
samples-controls, all of them hints, so no gate changes colour. -
CLIPBOARD_APP_STATEandWIZARD_SET_NEXT_STEPare gone from the
dispatch table, because they are gone from abap2UI5. Both constants and
both frontend handlers were removed upstream
(abap2UI5/abap2UI5#2752), so a wire naming either reaches
handlers[args[0]], finds nothing and does nothing — not even a console
line, which is what makes an unknown action the quietest miss there is. The
mirror kept accepting them, so the linter was telling app authors that a
dead wire was fine.WIZARD_SET_NEXT_STEP's threeACTION_ID_SLOTSentries
go with it; the multi-slot path they covered is asserted through
SMART_VARIANT_INITnow, and a literalWIZARD_SET_NEXT_STEPis an
unknown-frontend-actionlike any other retired name. Migration, which the
framework's own deprecations page carries: the wizard pair is two
CONTROL_BY_IDcalls (discardProgressthensetNextStep, andgoToStep
is reachable that way too, which the bundled event could not express), and
the share link isapp_state_get_href( )+CLIPBOARD_COPY, which hands
the string to ABAP instead of only to the clipboard.
test/fixtures/cs_event.intf.abapis re-synced with the interface, which
also drops the three obsolete URL-API alias constants;SERVER_EVENTSis
unchanged, sinceSET_NAV_ROUTING/SET_PUSH_STATE/
SET_APP_STATE_ACTIVEare still live wire values — thehash_*/
app_state_*constants that replaced those names kept them. -
An XML view writes the enum KEY, and
invalid-property-valuejudged the
VALUE — which is the spelling that breaks. A UI5 enum is a
{ Key: "Value" }map, and for 227 of the snapshot's 235 the two are the
same string, which is why this stood for a year. For the eight where they
differ the spellings are not interchangeable, and which one is right depends
on how the value reaches the control:XMLTemplateProcessorputs an
attribute throughparseValue( )(key → value) beforeisValid( ), while
a setter or a binding goes tovalidatePropertyandisValid( )alone —
andisValid( )is keyed by VALUE (DataType.createEnumType:
mValues[sValue] = sName). So the gate had it exactly backwards: it rejected
intervalType="OneMonth", which the demo kit has shipped for years, and
accepted"One Month", which parses toundefined, logs an error and leaves
the property at its default. The property gate judges views, so it judges
keys now; the two ABAP-side checks that read the enum table (an
INVISIBLE_MESSAGE.announcemode, asetStickypayload) judge arguments
that reach a setter and stay on the values. Writing the value form is
reported with its own message — the reason, not just "not a valid value",
because the API reference is where its author read that string — and with a
--fixthat rewrites it into the key. The snapshot gained an additive
enumKeyssection for it ({ "<value>": "<key>" }, only where they differ);
a snapshot without it judges every enum as before. The eight:
CalendarIntervalTypeandPlanningCalendarBuiltInView(OneMonth=
"One Month"),sap.m/sap.tntIllustratedMessageType(NoData=
"sapIllus-NoData"),FileUploaderHttpRequestMethod(Post= "POST"),
LightBoxLoadingStates,SharedDomRef,BackgroundHelper. Measured: 0
findings across the samples-controls (642 files) and samples (159) corpora,
before and after — this direction only removes false positives, and neither
corpus writes a value form. Found downstream rebuilding the Team Calendar
demo app (abap2UI5/samples-controlsz2ui5_cl_smpc_demo_003), where the
correct spelling had to be waived with a disable comment. -
The companion-control mirror carries all eleven controls a view can name,
not two. The render harness boots metadata mirrors of abap2UI5's bundled
cc/*.jscontrols, and it heldMultiInputExtandMessageManager— so a
view naming any of the other nine failed view CREATION on a control that
exists in every real installation. Found downstream, on the shape it is
easiest to find it: abap2UI5/samples-controls' Shopping Cart demo app keeps
its cart in browser storage the way abap2UI5/samples app 327 documents, with
<z2ui5:Storage>reading the key back, and the render gate answered
failed to load 'z2ui5/cc/Storage.js'. Added:CameraPicture,
CameraSelector,Dirty,FileUploader,Geolocation,Storage,Tree,
UITableExt,Websocket. An entry may now carrybase— the module the
mirror extends, defaultsap/ui/core/Control— becauseCameraSelector
extendssap.m.ComboBoxand the aggregation rules judge it by that type.
check-upstreamcompares the properties of the names it finds in the file,
so a control that was never mirrored is the one drift it cannot see; the
file says so now, next to the list. -
a( t = … )is read, and a value carrying data onv =is a finding.
abap2UI5's view builder takes text through a third parameter now:tapplies
escape_literal( )to the whole value, so a brace or backslash in it is
shown instead of parsed as a binding. The reconstructor readtas an
"unparsed attribute call" and dropped the attribute with nothing but a note;
it resolves likevnow and is escaped the way the class escapes it
(escapeLiteral( )from./reconstruct), and an id written throught
counts as the class's id for the wire rules. The new rule
unescaped-text-in-attribute(warning, fixable) reports av =whose
ORIGIN is data — a|…|template that interpolates, a name whose
assignments in the class show no binding vocabulary,CONV string( ), a
name && \literal`chain — and leaves a literal, a method call, aCOND/SWITCH, an ABAP boolean and anything that shows a bind, an event, a wire, an escaped brace or anescape_literal( )alone: assembling a binding in a variable and passing it throughvis whatv` is for. The
fix renames the parameter. This is the ui5-check entry that stood "blocked
on origin"; the origin is read out of the class. -
ABAP is case-insensitive, and now the linter is too. Outside its
literals and comments ABAP does not care about case, and a pretty printer set
to "uppercase" or "lowercase" produces exactly the source the linter used to
misread: an all-uppercase class was never collected at all (no checkable app
classes, exit 0), and a lowercase-keyword corpus lost 166 of its 173 views
in the reconstructor. Every keyword and identifier regex carries thei
flag now, identifiers are folded where they are compared, andnpm test
recases every fixture to upper and to lower case and demands the same
findings on the same lines and columns and the same reconstructed documents.
usesBuilderFactory( )from./buildersis the one place that decides
whether a class calls the builder; it is spelling- and space-tolerant. -
Chained declarations are read in full.
TYPES:andDATA:chains,
TABLE OFwithout a category,SORTED/HASHEDtables,READ-ONLY,
CLASS-DATA,LIKEand type aliases all reach the model now. A table
declared in a chain used to become a scalar, which silenced every row rule
for it; a|…|template inside aVALUE #( )row used to end the row at
its brace.declarationElements( )from./reconstructis the shared
reader. -
parseNamedArgs( )no longer reads the=of=>as an argument
boundary, so a static access inside a value (v = zcl_x=>c_flag) keeps
its value.namedArgMarks( )exposes the same read with positions, for a
rule that has to rewrite one argument wherever it stands. -
Smaller corrections in the same round. The verdict badge no longer
counts an opt-in rule nobody switched on. The--cacheentry stores what a
replay reads and leaves the documents and the model out (a third of the
cache on the samples corpus).abap2ui5lint-disablecounts only in a
comment, never inside a string literal. The literal map reads assignments
(x = \open`.), no longer comparisons (IF x = `open`.), so a template no longer inherits a compared value as its static text.client->cs_view-…written asz2ui5_if_client=>cs_view-…orme->client->cs_view-…is the same slot. The GitHub-app spike readsminUi5the wayparseConfig( )`
returns it. -
--all-classesjudges the classes that build no view. The hygiene rules
— the 256-column line abapGit cannot import, the round-trip family, the
activation rules, the released-API check — ran only over a class that calls
the view builder; every other.clas.abapin the tree was dropped at
collection, and the AGENTS.md sentence that justifiedsource-line-too-long
("a class that cannot be imported is the most severe thing this tool can
find") held for app classes only. With--all-classes(configallClasses)
every class is collected, and one that builds no view is judged by the
source-side rules alone — no view, wire or lifecycle rule runs over it, and
an app class gets the verdict it always got. Off by default: a repository
scan must not start judging includes and generated classes unasked. -
Findings stand where the code is.
event-arg-unresolvedand
trailing-empty-event-argreported an offset from two coordinate systems
and landed up to sixteen columns early — on the previous line where the row
was indented less than that, which is where adisable-next-linedirective
then hit nothing. Both sit on the argument they name now. The
obsolete-model-updatefix cut fromclient->and leftli_orme->on
a torn line for every handle not spelled exactlyclient; it removes the
whole statement. Thetrailing-empty-event-argfix keeps the row's
indentation. -
Fewer false errors on correct code. A
WHEN \A`opens the handler scope of event A only inside the CASE over the event, and only at that CASE's own level — a status switch nested in B's handler is a status switch, not A's handler; the same depth-aware read finds aWHEN OTHERSbehind a nested CASE. An id writtenn = 'id'is an id the wire rules know: the ids come from the reconstructed tree, the text scan is the fallback. A class name in a'…'literal is text, like one in backticks.delete-index-in-loopkeeps reporting an ended innerLOOPand aDO`
between the loop header and the DELETE, and its card now says why: the ABAP
kernel restores the cursor there, the transpiler runtime the same source
runs on does not, and both incidents were measured on the latter. -
Rules read every legal spelling.
unconverted-abap-booleanreads the
arguments by name, soa( v = mv_flag n = \visible` )andv =
me->mv_flagare the flags they are (one of three was reported before), and its fix renames the parameter wherever it stands.binding-to-referenceandbinding-to-localread chainedDATA:declarations, where only the first name used to be known.live-event-roundtripseesme->client->_event( )and either argument order. A wire whose action name is not the first argument, and at_arg = VALUE string_table( )(or any typed table) are read like the#form —frontend-action-unknown-id` and
its relatives never ran on them. -
hardcoded-binding-pathjudges values, not prose. All 18 corpus
findings were sentences in a documentation class explaining a two-way
binding, and that corpus had switched the rule off. Only a literal that IS
a value counts now — thev = …of an attribute call,&&chains
included, and the rows of at_arg = VALUE …( )event argument — and the
finding names the attribute it stands in, so two attributes with the same
path are two findings (their baseline keys change accordingly). -
The lifecycle heuristics follow the class's own methods.
redundant-init-displayrecognises two identical helper arms
(view_display( )twice, the usual corpus spelling), not only two identical
client->…display( )calls.missing-on-navigated-branchfollows a
main( )that only delegates into the method it delegates to.
missing-view-display-on-navigatedstays silent where the branch hands
clientto another object — that object may display, and the linter does
not see it. -
too-many-childrenread an absentmultipleas 0..1; UI5 reads it as 0..n.
ManagedObjectMetadatadefaultsmultipletotruewhen the declaration
does not say, and the UI5 sources leave it out on 0..n aggregations often
enough —sap.m.table.columnmenu.Menu.itemsand.quickActions, their two
containers — that a column menu with two actions was an error, and one
samples-controls port was rebuilt around the false finding. Only an explicit
multiple: falseis 0..1 now, for the explicit aggregation tag and for the
children written straight under a control:<table:Column><Label/><Text/>
fills the 0..1 default aggregationlabeltwice, UI5 logs "multiple
aggregates defined for aggregation label with cardinality 0..1" and keeps
the last one, and the gate said nothing. Both halves need the snapshot to
write the flag explicitly, which the regenerateddata/properties.jsondoes. -
invalid-aggregation-childnow honours the harvestedwidensAggregation.
The generator has recorded for a while which classes override
addAggregation( ), and nothing read it.sap.uxap.ObjectPageSubSection
declaresblocksassap.ui.core.Controland then stashes or unwraps an
ObjectPageLazyLoader— an Element — in exactly that method, so the sample
that is ABOUT lazy loading was invalid by the declaration and fine in the
browser (samples-controls app 592 carried aproperty_gate.skipfor it).
A flagged owner, or one whose ancestor is flagged, is no longer judged by
its declared child types. The flag onsap.ui.base.ManagedObjectitself,
where the method is defined rather than overridden, is ignored. -
A dotted namespace prefix is a prefix.
nsMapOfhad learned that
xmlns:viz.datais a declaration; the XML tag parser had not, so
<viz.data:FlattenedDataset>parsed as an aggregation tag NAMEDviz.data
with the real name swallowed into the attribute text, and every VizFrame
view reported anaggregation-in-aggregationat its root. -
undeclared-namespacesees the prefix inside the name. The builder can
writetag( \core:Icon` )instead ofns = `core`, andresolve( )reads both — the undeclared check read onlyns, so the name form resolved to nothing and the node was silently unjudged while the same view written withns` was reported (the corpus writes the name form a dozen times in one
class). Both spellings are reported now, both carry the fix, and an
aggregation tag with an undeclared prefix is reported as well. -
unknown-binding-path: a field read straight off a table. The path walk
stepped into row 0 of every array it met, so{/T/FIELD}passed whenever the
row had the field. The JSONModel reads/T/FIELDas the propertyFIELDof
the array, which is undefined, and the control renders blank. A table segment
now has to be followed by a row index (/T/0/FIELD), or the path has to end
at the table ({/T}is what an aggregation binds;lengthis the one
property an array has); the message says a row index is missing rather than
calling the field a typo. -
Two controls with the same wrong value are two findings. The dedupe key
wastype|control|member|value, so a second<Button type="Wrong"/>was
folded into the first: the count understated and--fixcorrected one
Button per pass. The node's offset is part of the key now. What the dedupe
exists for still holds — a helper method replayed at several call sites
builds its nodes at the helper body's offsets and its one defect stays one
finding, corrected everywhere by one fix. On a corpus that repeats a
post-floor member across many controls,member-too-newand its siblings
are counted per occurrence from now on, so a baseline's counts move;
--update-baselinesettles them. -
member-too-newno longer dates a member by a base class younger than the
control. UI5 extracts base classes out of existing controls —
sap.m.ListItemActionBase(@1.137) out of the @1.52FeedListItemAction,
sap.f.cards.BaseHeader(@1.86) out of the @1.64Header,
sap.tnt.NavigationListItemBase(@1.121) out ofNavigationListItem— and
the members that move up arrive untagged, so the scan dated
FeedListItemAction.textat 1.137 andHeader.pressat 1.86 although both
shipped with their control (checked in the tagged 1.64.0 source). A member
that shipped with a base class younger than the control is now dated at the
control's own release; a member the base class gained after its own release
keeps its date, because it is new for the subclass too. Sixteen findings on
the samples-controls corpus go with it, all of them documented in sidecars
as a blind spot no gate could see. -
An aggregation tag has to be in its parent tag's namespace.
XMLTemplateProcessorrecognizes an aggregation only where
childNode.namespaceURI === ns, the namespace of the control tag it sits in;
<f:content>under asap.m.Pageis loaded as the controlsap.f.content
and the 404 takes the view down. Reported asunknown-aggregation, with a
message that names the rule and what UI5 makes of the tag. The comparison
is against the parent TAG's library, not the declaring class:itemson an
f:GridListis inherited fromsap.m.ListBaseand is still<f:items>. -
An association written as a child tag is reported.
<Button><ariaLabelledBy>lbl</ariaLabelledBy></Button>was accepted because
the tag check looked associations up alongside aggregations. UI5 knows only
aggregations there and resolves the tag as a control. Reported as
unknown-aggregationwith the attribute form spelled out, deliberately
without a did-you-mean or a fix; a tag that is an association up to letter
case is no longer offered the association's spelling either. -
48 controls were roots that inherit nothing. The generator read a
module's parent by pairing thesap.ui.definedependency list with the
firstfunction (of the FILE, split on commas. Everysap.m.p13nmodule
and integration'sPaginatorare written with an arrow factory, four
calendar controls carry a comment inside the parameter list,DragDropBase
comments two dependencies out,MockServerwritessap.uiand.define(
on two lines — and all of them came out withparent: null, which the
property gate reads as "a root class": every inherited property an
unknown-property, andsap.m.p13n.SelectionPanel is not allowed in sap.m.Page content. The header is read with its comments stripped, the
factory is matched right after the dependency array in every shape
(function,(…) =>, a bare arrow parameter), and a class extending a
class of the same file (CustomLocaleData) finds it. No class in the
snapshot is parentless now; the one real root,sap.ui.base.Object, has no
entry and the gate knows it by name.Two more things the same walk got wrong, and both papered over the first.
sap.ui.integrationships a minified thirdparty bundle carrying copies of a
dozen core classes (Locale,CustomData, the calendars) with no header to
read a parent from, and because the walk is last-writer-wins those copies
REPLACED the real entries. And the JSDoc ofControl,Element,
UIComponentand friends carries@examplecode —sap.mylib.MyControl,
my.Component,myapp.views.MainView, thirteen names in all — which the
snapshot listed as controls, next to a...that an error message in
mvc/Controller.jsquotes. Thirdparty trees are skipped, example code is
not a class, and the two enums only the bundle had contributed
(sap.ui.core.CalendarType,sap.ui.core.date.CalendarWeekNumbering) are
read from where the core really registers them: an alias for an object a
sap/basemodule exports. 973 controls become 959, all of them real. -
An aggregation that says nothing is 0..n, not 0..1. The snapshot wrote
multipleonly when a declaration saidmultiple: true, and the reader
took its absence as "single" — the opposite of UI5's default. Eight
aggregations omit the flag,sap.m.table.columnmenu.Menu.itemsand
quickActionsamong them, so a menu with two items wastoo-many-children.
Every aggregation and association now carriesmultipleas
ManagedObjectMetadataresolves it (falsewritten out where it used to be
implied), andPage.subHeaderwith two bars is still one too many. -
84 deprecations lost their release. The sources spell it
As of version 1.20,as of 1.20,Since version 1.20,since 1.115andSince 1.130;
the reader knew the first three, and the gate treats a deprecation without a
version as older than every floor.sap.ui.core.mvc.JSView(Since 1.90)
andsap.ui.integration.ActionDefinition.buttonType(Since 1.130) were
reported to a 1.71 app. Every spelling is read now (sap.m.TablePersoController
andsap.ui.core.search.SearchProviderincluded), and a version that ended
a sentence no longer keeps the full stop — 45 entries read1.88.. -
An event parameter is not the aggregation of the same name. The flat
membersmap topped itself up from every@sinceblock in a class, so
SinglePlanningCalendar.appointments— an aggregation with no version —
carried1.67.0from theappointmentsPARAMETER ofappointmentSelect,
and 75 further entries named no member of their class at all. The map
covers declared members only; a parameter's version lives under its event. -
What a control fires counts as declared.
sap.m.table.columnmenu.QuickSort
declareskeyandsortOrderonchangeand fires{ item }, so an app
reading$parameters>/item— the only thing the event carries — was an
unknown-event-parameter. The generator reads the object literal of every
fire<Event>({ … })call and adds its keys to the declared parameters,
flaggedfired: trueso a reader can tell the two apart; 48 parameters
across 29 events, and two of the three findings the rule produced on the
samples-controls corpus are gone (the third,ColorPickerPopover change colorString, forwards another control's parameters as a variable, which no
literal reader can see). -
An XML comment is not a view, and
sap-icon://status-{ code }is not a
name. The icon scan read raw view XML through, so
<!-- <Button icon="sap-icon://old-typo"/> -->was anunknown-icon, and an
interpolated template reported its prefix (status-) as a glyph. Comments
are blanked before the scan (character for character, so every offset still
points where it did), and a name that ends at{is what the reconstructor
already calls it: composed at runtime, not judged. -
npm run generate-iconsruns again. It read the@openui5pin from the
root manifest'soptionalDependencies, where the pins had not been since the
render-runtime split, and threw before touching the network — the
dependabot-openui5 workflow, whose one job is to run it, could not go green
on any bump. It reads the workspace manifest now (the root stays as the
fallback for an older layout), and a full scan reproduces the committed
data/icons.jsonbyte for byte. -
HASH_REPLACEandHASH_ATTACH_CHANGEDare released constants. Both
are inz2ui5_if_client=>cs_eventand consumed by
follow_up_action( )like the three server events already listed, but the
linter's mirror did not have them, so the wire the documentation shows was an
unknown-frontend-action. They are listed, and the gap that let them arrive
unnoticed is closed:check-upstreamreads thecs_eventblock of the
released interface and demands that every value in it is accepted by
FRONTEND_EVENTS,FRONTEND_EVENT_ALIASESorSERVER_EVENTS— the two
server-side lists used to be the one part of the mirror nothing gated, on
the grounds that they "live in the server". The server publishes them. -
One rule card taught a chain this project rejects. The layout rules are
the house style every sample corpus is held to, and the half of a card
labelled "the same code, fixed" is the last place a reader should meet a
chain that fails them.popup-without-close-wire's remedy squeezed a
Dialog, aTextand abuttonsaggregation onto two lines, and
chain-element-per-linesaid so. It is one call per line now, andnpm test
asks all 125 cards — both halves — whether any of them still shows a chain
the layout rules reject. It was the only one. -
|\n|is a newline, and the view now says so. The template resolver
dropped the backslash and kept the letter, so|\n|becamenand|\t|
becametin every value it resolved. The transpiler settles what they
actually mean:@abaplint/transpiler'sStringTemplateTranspileremits the
template body VERBATIM into a JavaScript backtick literal (it escapes only
the backtick), so the browser reads the escape with JavaScript's rules — and
the corpus writes… && |\n| && …between the lines of a message box for
exactly that reason.What it cost: the two-line UI5 type binding that
cookbook/model/expression_binding.mdteaches reconstructed as
{ type : "sap.ui.model.type.Integer",n path:"/INPUT32" }, and UI5 answers
SyntaxError: Expected ':' instead of 'p' — a render error against an
example that runs correctly in a playground. Found by running the render gate
over the documentation's own 69 examples, which its CI cannot do.Everything outside
\n,\rand\tkeeps the old behaviour, which is
also JavaScript's:\|,\{,\}and\\are the character itself.Over the three sample corpora (819 results) no finding moves. Three
reconstructions gain the line breaks and tabs the running app has — a
core:HTMLstylesheet, aCodeEditorshowing JSON and one more — and each
drops one from itsbindingscount, because a{ … }that a newline
interrupts is not one binding, which is what UI5 sees too. -
ABAP has three literal forms and the readers knew two.
'…'— the text
field literal, the oldest form and the one most ABAP outside this project's
house style is written in — was not tracked as a literal at all, so a
structural character inside one read as the real thing. A(in a title made
the view unreconstructible (no view reconstructed from builder calls, an
ERROR) on ABAP a system compiles without a murmur; a.ended the statement
early, a"started a comment that ran to the end of the line, a)closed
the call it sat in and an=opened an argument. Every shared scanner now
tracks it beside`and|:scrub( ),parenRegion( ),topSplit( ),
splitStatements( )andparseNamedArgs( )inlib/abap.mjs,classify( )
in the chain-layout reader and the row scanner inreconstruct.mjs.The quieter half is the value resolver, which knew a backtick literal and a
template: an attribute writtenv = 'Hello'resolved to nothing and was
DROPPED from the reconstructed view, so the property rules and the render
gate judged a view the app does not build. It resolves now, as ABAP does —
''inside is one quote, and the trailing blanks of a type-C literal go.The same blindness ran through the readers.
n =Page`` andn = 'Page'
name the same control, but the chain reader matched only backticks for
`n`/`ns`, so an element call written the second way came back "unparsed
element call" and nothing was reconstructed from it; and the seed readers
behind `VALUE #( )`, the `DATA … VALUE` default and a scalar assignment
matched only backticks too, so a table seeded the way most ABAP is written
derived an EMPTY model. `npm test` now asks it of the whole fixture set at
once: every fixture rewritten from one literal form into the other has to
reconstruct to the same view and derive the same model. 45 of the 49
differed; none do now.No finding moves on the three sample corpora (831 classes; they are backtick
throughout) or on the documentation's 44 runnable app classes, which is what
makes this a gap rather than a regression: it was only ever wrong on code
nobody here writes. -
A third off a full run.
topRows( )- the reader behind
value-header-default-reassignedand the enum/boolean row rules - found the
character before a candidate(by copying the whole prefix and running
/\s+$/over it, once per paren. Quadratic in the constructor body, and
pathological on a literal-blanked source, which is mostly runs of spaces:
41% of a 637-class run was that one regex. Walking back to the last
non-space character instead takes the same run from 10.4s to 6.9s, measured
over abap2UI5/samples-controls with the property gate off. Nothing about
what it decides changes; a fixture dry run reports the same 187 findings in
52 files. -
A half-written class is reported, not thrown at.
npm testnow runs
every reader in the tool over ~450 deliberately broken sources - each
fixture truncated at twelve points, with characters deleted, doubled, and
unbalanced(/)/`/|/'/.spliced in - and fails if any of them
raises instead of reporting. Nothing did, which is the point: the VS Code
extension checks on every keystroke pause, so a class mid-edit is the
normal input, and a throw there takes the findings for every other file in
the run with it. The fix path is included, under the strict-span mode the
suite already runs with. -
The Autofix badge now means the fix works on the card's own example.
class-constructor-visibilityadvertised a fix over an example the fix
declines: the correction moves the declaration into thePUBLIC SECTION,
and the example had none to move it into, so a reader pressing Autofix on
exactly what the page showed them got nothing. The pair is the real one
now - the example is a class shell with both sections, the remedy is what
--fixwrites - andnpm testruns every fixable rule's example to a
fixpoint and fails if the rule survives. -
A
client->call written inside a string is prose, not a call. Every
scan that locates a call read the source with its literals intact, so a
class that DOCUMENTS the API it uses — a MessageStrip reading "the view is
handed toclient->view_display( )", which is how the sample corpora
teach — was judged on its own sentence. It reportedobsolete-binder,
obsolete-model-update,obsolete-frontend-event,obsolete-bind-argument
andbinding-to-local(four of them warnings, so CI went red), raised
phantom events forevent-without-handlerto call dead, and counted a
quoted display as the first of two fordouble-display-in-branch— that
last one on the framework's own Hello World sample. Worse, three of those
rules carry a fix:--fixsilently rewrote the sentence inside the string.
Fifteen more rules had the same defect and were found the way the first
eight should have been - by asking all 124 at once.RULE_DOCS[id].example
is, by the rules page's own gate, the shortest source that makes that rule
fire; quoted inside a literal in an otherwise clean class, none of them may
fire, andnpm testnow asserts exactly that. It caught the whole
lifecycle module (missing-on-navigated-branch,separate-lifecycle-ifs,
lifecycle-is-initial,redundant-init-display,duplicate-for-iterator),
four more hygiene and client-API scans (value-header-default-reassigned,
get-viewname-removed,popover-display-val, the DECLARATION half of
abap-date-formatter-mismatch) and the frontend-wire locator behind
unknown-view-slotandraw-javascript-to-frontend. Seven rules read a
literal ON PURPOSE - an icon name, at_argstring, a URL, a brace, a
path - and are listed with their reason; the test also fails if one of
those stops needing the exception.The same held for the statement-shaped rules: a sentence naming
CLASS-METHODS class_constructororINTO CORRESPONDING FIELDS OF TABLE @DATA(…)was reported as an ERROR, and the first of those carries a fix
too. Every scan that asks where a call or a statement IS now reads the
literal-blanked copy and reads the arguments out of the source, which is
offset-for-offset the same text; the rules that read a literal deliberately
(an event name, a bound path) are unchanged. -
Every finding links its rule.
annotate( )setsurlon every finding
— the rule's card on the rules page — so it is in--format jsonand on
every object a consumer gets; the stylish line carries it after the rule
id (line:col severity message rule-id url), because the reference block
at the end of the run is too far from the line in a log of any length.
Markdown, SARIF, checkstyle and JUnit already linked it and are unchanged. -
The rules page opens the reported code in the playground. Every card
whose reported snippet the linter reproduces inside a class carries a link
that opens that class at abap2ui5.github.io/playground — the snippet in a
frame (playgroundSource( )inscripts/generate-rules-page.mjs: a chain
fragment under a namespaced View root in a displayedmain( ), a method
in a class, a section in a class), in the playground's share-link format,
written at generation time so the page stays self-contained. The frame is
guessed, so every link is verified before it is written: the wrapped
source goes through the linter and only a card whose own rule fires on it
gets the link — 103 of 124 today; the rest are data-shaped or file-level
(a BOM, CRLF, a 255-character line) that no class frame can carry.npm testdecodes every fragment and holds the count above 100. -
Five new rules, from the flow of a method and from the one comparison
get_event( )makes. Three read the statement sequence a call sits in
(branchTail( )inabap-source.mjs— what runs whenever the call runs,
nested blocks blanked):unconditional-popup-display(apopup_display( )
at the top level ofmain( ), behind no guard — the frontend loads and
opens the fragment anew on every call, so every roundtrip rebuilds the
dialog; a warning),display-after-nav-app-call(a display behind the
hand-over, usually a missing RETURN; a hint) anddouble-display-in-branch
(the same slot twice in one sequence, the first dead; a hint).
popup-without-close-wirereports aDialogbuilt in a method that wires
no event at all (a hint, per method).event-name-case-mismatchis the rule
event-without-handlerwas deliberately blind to: a raise spelledsave
against a handler readingSAVE— the runtime compares letter for letter,
so the handler never runs; a warning, with a--fixthat writes the
handler's spelling into the raise. -
Did you mean. Eight closed-set rules name the one candidate a written
name can only have meant — the same name up to letter case and-/_—
and carry a--fixthat writes it:unknown-control,unknown-property,
unknown-aggregation,invalid-property-value,unknown-event-parameter,
unknown-icon,frontend-action-unknown-id,popover-anchor-unknown-id.
The control/aggregation pair suggests across the line UI5 XML draws with
the first letter (Content→content,page→Page). No edit distance,
on purpose:Buttomgets no suggestion, becauseButtonwould be a guess
andTestforTexta wrong one.lib/suggest.mjsholds the one
function;attachSuggestionFixes( )infindings.mjsfinds the span for
the view-side five (ABAP and raw XML alike), the others compute it where
they report. 34 rules carry a fix now. -
class-constructor-visibilityreads the chained form.CLASS-METHODS: other, class_constructor.declares it just the same and was never
reported; it is now, at the name, without a fix (the line is shared). -
Seven more rules carry a
--fix— 25 of the rule set now do, up from 18.
Each one is a correction with nothing to decide:redundant-init-display
drops thecheck_on_init( )call from the OR, or the whole init arm of the
fork;binding-to-referenceinserts the->*(forTYPE REF TO dataonly,
because->*on an object reference does not compile);unescaped-brace-in-style
escapes every brace of the stylesheet where all of them sit in backtick
literals;collapsed-brace-in-styledoubles the backslash inside the
template, across every segment of the sheet in one pass;class-constructor-visibility
moves the declaration line underPUBLIC SECTION.of the same class;
escape-sequence-in-backticksplits the literal into the\left` && |\n| &&
`right`chain its own message recommends;json-bind-on-scalar-propertydeletes thejson = abap_trueargument. Every one declines the shape where a fix would be a guess — a comment inside the span, a chainedCLASS-METHODS:, a brace inside a template — and says so on the rules page (fixNote). Findings and severities are unchanged;--fixsimply settles more of them. Considered and left without one:boolc-instead-of-xsdbool(the two are not aliases — a blank false against an initial one),separate-lifecycle-ifs(IF init … ENDIF. IF navigated …may rely on BOTH running on the first start, so an ELSEIF would blank the app),empty-catch-block(the pragma asserts an intent the linter cannot know) andcommercial-ui5-host(a/resources/` path may not exist on the open host). -
frozen-view-buildersays it in a friendlier, shorter way. The message
led withDEPRECATEDandNOTHING about the view was checked, and read like
a class onz2ui5_cl_xml_viewwas a defect on the way to breaking. It is
not: that builder keeps working, and the plan is for it to move into a
separate addon rather than to disappear, so staying on it is a legitimate
choice. What the finding is actually about is what such a class does not get
— no control, property, binding or render check can read the old API — so
the text now names that, names the one thing to switch to, and recommends it
instead of demanding it. Roughly a third shorter. Severity is unchanged
(warning), and so is everything else about the rule: same id, same
position on the factory call, same"rules": { "frozen-view-builder": "hint" }(orfalse) for a repo that stays on the old builder on purpose.
RULE_DOCSand the rules page carry the same rewrite. -
Every rule documents a before/after pair, and every report links at it.
RULE_DOCSgainsremedy— the same source asexample, fixed — for all
120 rules, gated likeexampleis (missing one failsnpm test, and so
does one identical to its example). It is a split as much as an addition:
around a third of the examples used to carry the fix as a trailing line of
their own snippet, soexamplewas not "the source that triggers it" and a
reader could not tell which of the two lines was the reported one. The rules
page renders the pair as two labelled blocks under its own#<rule>-example
anchor, and each card now says which module inlib/decides the finding,
linked at the line (scanned at generation time, so the existing staleness
gate keeps it honest). Reports carry the addresses instead of only the id:
the stylish output ends in a reference block listing each rule it reported,
the markdown job summary links the rule cell and adds a Rules reported
table with a link to the card and one to the pair, and a GitHub annotation
ends in the rule id and its deep link — the annotation renders no rule id of
its own, so a reader had the message and no way on from it. New exports:
ruleUrl( ),ruleExampleUrl( ),RULES_PAGE(./rule-docs) and
rulesReported( )(./report); the SARIFhelpUrinow comes from the same
helper it used to restate. -
Three structural rules from abap-check:
empty-catch-block,
boolc-instead-of-xsdbool,delete-index-in-loop. An empty CATCH wants
##NO_HANDLER(a hint, likeredundant-conv-i- the code is correct,
the extended check on a real system is what speaks; a comment does not fill
the block).boolc( )where the ecosystem's downport writesxsdbool( )
for you (a warning - and no fix, because boolc's FALSE is a blank while
xsdbool's is initial, which is behaviour, not spelling). AndDELETE itab INDEX sy-tabixinside aLOOP ATover the same table (an error),
reported only where sy-tabix is provably not the loop's own cursor any
more: a READ TABLE, a completed inner LOOP or a DO between the loop header
and the DELETE (the TABLE_INVALID_INDEX shape a live app 500ed with), or a
DELETE naming an ENCLOSING loop's table while an inner one is open (the
index is then another table's row number). The plain current-row delete is
legal ABAP - the kernel adjusts the loop cursor for a delete on the loop
table, and so does @abaplint/runtime (deleteIndexdecrements every
registered loop controller) - and the rule's first cut reporting it named a
reviewed, working samples-controls port (558), which is the corpus doctrine
firing: measured over the 637-file samples-controls corpus the narrowed
rule reports 0, while every incident shape from abap-check §5 (app 352's
DO, filter_itab's inner loop, the clobbering READ TABLE) stays covered by
fixtures. -
Four abapGit round-trip rules:
byte-order-mark,crlf-line-ending,
trailing-whitespace,missing-final-newline. Thesource-line-too-long
precedent again - a consumer whose only gate isnpx abap2ui5lintmust see
the round-trip family too. abapGit writes every file one specific way (LF
only, no BOM on.abap, no trailing blanks, exactly one terminating
newline); a file written otherwise comes back DIFFERENT from what the system
serializes, on every pull, for everyone. All four are warnings - per
abap-check §1 only the 255-character line actually kills the import, the
rest never stop diffing - and all four carry a mechanical--fix(delete
the BOM, delete every CR, strip the blanks, append the newline). CRLF is
one finding per file with a fix span per CR; trailing whitespace is one per
line, like itssource-line-too-longneighbour. Measured over abap2UI5's
ownsrc(200 .abap files, which gate these themselves): 0 findings. -
stats.ruleHits: per-rule fired counts, before suppression. The--json
stats gain aruleHitsmap (rule id -> findings the gate produced), counted
BEFORE therulesblock, source directives and any baseline had their say -
so a fully baselined corpus still shows which rules fired on it, instead of
that number being an anecdote. Additive key; every count comes from the walk
the gate already did. -
Config
extendsandmaxWarnings(+--max-warnings). A config can
name another abap2ui5lint.jsonc/.json as its base: the extending file wins
per key, the tworulesblocks merge per rule id, relative paths resolve
against the file that wrote them, chains follow and a cycle is refused
loudly.maxWarnings(ui5lint's flag as a config key, plus--max-warnings)
fails the run when the warning count exceeds it, whateverfailOnsays -
the way to fail on errors only while still capping the warning debt. -
--format checkstyleand--format junit. The two XML shapes most CI
systems ingest natively (Jenkins, GitLab, Azure DevOps test tabs) - thin
renderers over the same problem walk every other formatter reads, properly
XML-escaped. checkstyle maps ourhintto itsinfo; junit renders a
clean file as one passing testcase, so a test tab shows the file was seen. -
--stdin: lint piped source. The property gate over standard input -
the editor and pre-commit case - reported under--stdin-filename <name>
(default<stdin>), which also decides the handling: a.view.xml/
.fragment.xmlname (or content starting with<) goes down the raw-view
path, anything else is read as an ABAP class. Exit codes and every
--formatbehave exactly as for a file. The render gate stays off for a
pipe, and--fix,--renderand--screenshotare refused rather than
silently ignored. -
Four existing rules gained a
--fix.escaped-brace-in-backtick
deletes the backslashes (a backtick literal has no escape processing, so
they say nothing);redundant-conv-iunwraps the CONV (the rule already
guarantees it is the entire right-hand side of an assignment into a
TYPE itarget declared in this file);lifecycle-is-initialrewrites
IS NOT INITIALon a lifecycle call to the predicative form and
IS INITIALto= abap_false— on a plainabap_boolvariable only the
IS INITIALhalf is rewritten, because= abap_truevs<> abap_false
for the NOT form is a choice, not a mechanical rewrite, and that one stays
a finding;trailing-empty-event-argdeletes the trailing empty
() `` row (it never arrives, so nothing observable changes), whole
line included when the row has it to itself, comments never. -
--cache: an opt-in cross-run result cache, eslint-style. Each file's
full result (the findings, not just pass/fail - a baselined corpus needs
the findings again on replay) is stored keyed by its content hash, under one
context hash over the linter version, the metadata snapshot's ui5Version and
every resolved setting that changes a verdict (floor, distribution, allow,
gates, rules block). A hit skips both gates for that file; any relevant
change is a miss.--cache-location <file>names the store (default
.abap2ui5lintcache),"cache": truein the config turns it on for a repo,
and a corrupt or foreign cache file reads as empty rather than as an error.
--fixneeds no special handling: it rewrites the file, so the stale entry
never matches again. -
checkFilesandscreenshotFilesaccept a caller-ownedrenderer. An
already-open session fromopenRenderer( )(the./renderexport) is used
as-is and never closed - the caller owns its lifecycle - so a long-lived
consumer (mcp-server'svalidate_view) can keep one warm Chromium across
many calls instead of paying a browser start per call. Absent the option,
behaviour is exactly the old open/close. A passed renderer's pool size and
theme were decided atopenRenderertime. -
The render gate's page pool is a dial now.
--render-pages <n>on the
CLI,"render": { "pages": n }in the config (which also ASKS for the gate,
the way"render": truedoes),renderPagesoncheckFiles. The default
stays 4, precedence stays CLI flag > config > default, and an unknown key or
a non-positive count inside the render object fails loudly like every other
config mistake. -
checkNodesstops rebuilding its constants per document. The
known-library set (a key walk over ~1000 snapshot controls) is cached per
snapshot object, and the built-in-roots set, the framework-attribute set and
the relative-asset regex moved to module scope.statementAtin the
reconstructor binary-searches the offset-sorted statement list instead of
scanning it backwards. No verdict changes. -
scrub( )andblankLiterals( )share a small bounded memo. The
comment scrub used to be recomputed about six times over the same source per
file (reconstructor, ABAP rules, chain layout, source readers, directives),
andblankLiterals' single-entry memo thrashed whenever two source views
interleaved - while pinning two whole source copies for the life of the
process. Both now keep their last four inputs in a recency-refreshing Map,
so theifBranchEnd/ifBlockEndloops hit deterministically. Signatures
and outputs are unchanged. -
applyRulescompiles a rule's config once per run, not once per finding.
Every finding used to recompile its rule'sexcluderegexes and re-spread
the path-form set; the compiled config is now memoized per (rules object,
rule id) and the form list is built once per file. No verdict changes - the
exclude-semantics tests now also pin that one rules object walked over many
files still decides per file.