Skip to content

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 24 Sep 11:23
· 56 commits to main since this release
029826c
  • The linter judges against OpenUI5 1.152.0. The render runtime's
    @openui5/* pins, data/properties.json and data/icons.json move
    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 an unknown-property / unknown-control plus 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-id no 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 by master-title, the
    title of sap.m.Page master, exactly as the demo kit original does.
    column-cell-count-mismatch no longer counts a row template whose cells
    are filled in exclusive IF / CASE branches: 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: wrong labelFor targets copied from the
    originals, template text through v instead of t), and the Downstream
    job now runs samples-controls' view gates with VIEW_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-template no longer reports
    sap.ui.table.Table's rows.
    That aggregation is bound without a
    template by design: the table builds its rows from each column's
    template, and UI5 sets _doesNotRequireFactory on it so
    bindAggregation skips 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
    Downstream samples-controls corpus job 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
    TreeTable and AnalyticalTable. 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 in lib/properties.mjs.

  • The mirrors follow abap2UI5's 2026-09-22 removals
    (abap2UI5/abap2UI5#2776, #2777).
    check-upstream was red against
    abap2UI5 main, and with it abap2UI5's own check_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 (the inputmode action,
      replaced by z2ui5.cc.InputExt's bound property, so INPUT_MODES and
      its invalid-action-payload / ACTION_ID_SLOTS entries go with it),
      IMAGE_EDITOR_POPUP_CLOSE and Z2UI5 (removed with the z2ui5 global).
      A literal naming one is an unknown-frontend-action now.
    • FRONTEND_EVENT_ALIASES loses the five *_NAV_CONTAINER_TO names: the
      constants are gone from cs_event, and so is the server remap into a
      CONTROL_BY_ID to wire. POPUP_CLOSE / POPOVER_CLOSE stay.
    • lib/released-api.mjs loses the released structure z2ui5_t_02
      (nothing in the ecosystem named it; it falls into the z2ui5_t_ table
      family now) and the frozen z2ui5_cl_pop_js_loader.
    • obsolete-event-constant is withdrawn before it was ever released.
      It judged the run under the "obsolete label of cs_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.abap is re-synced with the interface.
  • The windows-latest leg of CI is green again. Both --watch sections
    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.
  • redundant-serializable (a hint, fixable): a class that declares
    INTERFACES if_serializable_object. beside INTERFACES z2ui5_if_app..
    z2ui5_if_app includes it - the framework persists the app instance with
    CALL TRANSFORMATION id between 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_app guard, which does not see the chained
    INTERFACES: if_serializable_object, z2ui5_if_app. - the shape that carries
    the redundancy most often. --fix deletes 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-roundtrip learns the framework's own
    remedy.
    second-root (an error: a second element at the document's top
    level — the split chain after a standalone factory( )., whose variable
    holds the root so the next statement adds a sibling of the mvc: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 off message_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 no cs_event-smart_variant_init wire in the class —
    saving a variant fails inside sap.ui.fl with nothing red). And
    live-event-roundtrip judges every live* event now, is silent for a
    wire carrying s_ctrl-check_queue_last, and carries a --fix that
    writes that flag with check_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 a LOOP 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 --fix for the ASSIGNING shape into the
    framework's own cell binding tab = … tab_index = sy-tabix, and only
    while nothing between the loop header and the call has moved sy-tabix),
    bound-aggregation-without-template (an error: bindAggregation throws
    "Missing template or factory function" and the whole view fails to load),
    unknown-source-property (the $source>/name twin of
    unknown-event-parameter — the reconstructor now records sourceParams
    beside eventParams; 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 through INPUT_FAMILIES in properties.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-capture inlines the call where the
    captured name is read exactly once as an attribute value, and deletes the
    capturing statement; missing-view-display-on-navigated copies the init
    branch's single display statement into the navigated branch (the same
    scanner missing-on-navigated-branch uses, shared now as
    singleDisplayStatement( )); unknown-binding-path carries 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-shut deletes the ->end( ) it sits on;
    insecure-asset-url rewrites a LOADED uri to https:// (the href hint
    keeps its finding); and loop-work-area-bound as 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's backlog/ABAPLINT.md carries
    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 of event-without-handler: a WHEN
    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: a labelFor / 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 (a ColumnListItem
    with a different number of cells than its sap.m.Table has 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-branch writes the ELSEIF 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-ifs folds ADJACENT blocks into one ELSEIF chain (a
    block with an ELSE, or statements between two blocks, keeps the finding);
    binding-to-nonpublic and private-app-attribute MOVE the single-line
    declaration into the section the remedy names (private-app-attribute
    renames PRIVATE SECTION. outright where the class has no PROTECTED one),
    a chained DATA: 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-target writes the
    target call 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.mjs applies them like any other non-overlapping
    pair.

  • Did-you-mean on the five closed sets that had none. unknown-model
    (Device> for device>), uncurated-formatter, unknown-view-slot
    (main for MAIN), unknown-frontend-action (set_title for
    SET_TITLE) and the filter-row operator of invalid-frontend-action
    (contains for Contains — the set is case-sensitive upstream, and the
    miss leaves the binding unfiltered) now carry written/suggestion where
    the written name is a member of the set up to letter case, and
    attachSuggestionFixes turns that into the fix. No edit distance, as
    before: a case miss is not a guess. unknown-view-slot and
    invalid-frontend-action carry the fix without joining FIXABLE: 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 — a client->_event( ) whose
    t_arg is a VALUE #( ( x ) ) with ONE row, where arg = x says the same
    thing. The client folds arg into the same string_table, so the wire and
    the handler's get_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: the t_arg = VALUE #( ( x ) ) span becomes
    arg = x in place, so a two-line call collapses onto its first line and the
    continuation keeps its column. From two rows on t_arg is the right
    parameter and the rule is silent, as it is when both spellings are passed
    (the documented composition appends arg behind the rows, so the call sends
    one more argument, not the same one) and on a row containing a #: arg is
    generic (TYPE clike), so a CONV #( ) or nested VALUE #( ) has no type
    to derive # from and the remedy would not compile. Only
    client->_event( ) is judged — _event_client( ) and follow_up_action( )
    are frontend actions and have no arg parameter 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_STATE and WIZARD_SET_NEXT_STEP are 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 three ACTION_ID_SLOTS entries
    go with it; the multi-slot path they covered is asserted through
    SMART_VARIANT_INIT now, and a literal WIZARD_SET_NEXT_STEP is an
    unknown-frontend-action like any other retired name. Migration, which the
    framework's own deprecations page carries: the wizard pair is two
    CONTROL_BY_ID calls (discardProgress then setNextStep, and goToStep
    is reachable that way too, which the bundled event could not express), and
    the share link is app_state_get_href( ) + CLIPBOARD_COPY, which hands
    the string to ABAP instead of only to the clipboard.
    test/fixtures/cs_event.intf.abap is re-synced with the interface, which
    also drops the three obsolete URL-API alias constants; SERVER_EVENTS is
    unchanged, since SET_NAV_ROUTING / SET_PUSH_STATE /
    SET_APP_STATE_ACTIVE are still live wire values — the hash_* /
    app_state_* constants that replaced those names kept them.

  • An XML view writes the enum KEY, and invalid-property-value judged 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: XMLTemplateProcessor puts an
    attribute through parseValue( ) (key → value) before isValid( ), while
    a setter or a binding goes to validateProperty and isValid( ) alone —
    and isValid( ) 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 to undefined, 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.announce mode, a setSticky payload) 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
    --fix that rewrites it into the key. The snapshot gained an additive
    enumKeys section for it ({ "<value>": "<key>" }, only where they differ);
    a snapshot without it judges every enum as before. The eight:
    CalendarIntervalType and PlanningCalendarBuiltInView (OneMonth =
    "One Month"), sap.m/sap.tnt IllustratedMessageType (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-controls z2ui5_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/*.js controls, and it held MultiInputExt and MessageManager — 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 carry base — the module the
    mirror extends, default sap/ui/core/Control — because CameraSelector
    extends sap.m.ComboBox and the aggregation rules judge it by that type.
    check-upstream compares 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 on v = is a finding.
    abap2UI5's view builder takes text through a third parameter now: t applies
    escape_literal( ) to the whole value, so a brace or backslash in it is
    shown instead of parsed as a binding. The reconstructor read t as an
    "unparsed attribute call" and dropped the attribute with nothing but a note;
    it resolves like v now and is escaped the way the class escapes it
    (escapeLiteral( ) from ./reconstruct), and an id written through t
    counts as the class's id for the wire rules. The new rule
    unescaped-text-in-attribute (warning, fixable) reports a v = 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 an escape_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 the i
    flag now, identifiers are folded where they are compared, and npm 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 ./builders is the one place that decides
    whether a class calls the builder; it is spelling- and space-tolerant.

  • Chained declarations are read in full. TYPES: and DATA: chains,
    TABLE OF without a category, SORTED/HASHED tables, READ-ONLY,
    CLASS-DATA, LIKE and 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 a VALUE #( ) row used to end the row at
    its brace. declarationElements( ) from ./reconstruct is 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 --cache entry stores what a
    replay reads and leaves the documents and the model out (a third of the
    cache on the samples corpus). abap2ui5lint-disable counts 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-classes judges 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.abap in the tree was dropped at
    collection, and the AGENTS.md sentence that justified source-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 (config allClasses)
    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-unresolved and
    trailing-empty-event-arg reported 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 a disable-next-line directive
    then hit nothing. Both sit on the argument they name now. The
    obsolete-model-update fix cut from client-> and left li_ or me-> on
    a torn line for every handle not spelled exactly client; it removes the
    whole statement. The trailing-empty-event-arg fix 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-boolean reads the
    arguments by name, so a( 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-path judges 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 — the v = … of an attribute call, && chains
    included, and the rows of a t_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-display recognises two identical helper arms
    (view_display( ) twice, the usual corpus spelling), not only two identical
    client->…display( ) calls. missing-on-navigated-branch follows a
    main( ) that only delegates into the method it delegates to.
    missing-view-display-on-navigated stays silent where the branch hands
    client to another object — that object may display, and the linter does
    not see it.

  • too-many-children read an absent multiple as 0..1; UI5 reads it as 0..n.
    ManagedObjectMetadata defaults multiple to true when the declaration
    does not say, and the UI5 sources leave it out on 0..n aggregations often
    enough — sap.m.table.columnmenu.Menu.items and .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: false is 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 aggregation label twice, 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 regenerated data/properties.json does.

  • invalid-aggregation-child now honours the harvested widensAggregation.
    The generator has recorded for a while which classes override
    addAggregation( ), and nothing read it. sap.uxap.ObjectPageSubSection
    declares blocks as sap.ui.core.Control and 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 a property_gate.skip for it).
    A flagged owner, or one whose ancestor is flagged, is no longer judged by
    its declared child types. The flag on sap.ui.base.ManagedObject itself,
    where the method is defined rather than overridden, is ignored.

  • A dotted namespace prefix is a prefix. nsMapOf had learned that
    xmlns:viz.data is a declaration; the XML tag parser had not, so
    <viz.data:FlattenedDataset> parsed as an aggregation tag NAMED viz.data
    with the real name swallowed into the attribute text, and every VizFrame
    view reported an aggregation-in-aggregation at its root.

  • undeclared-namespace sees the prefix inside the name. The builder can
    write tag( \core:Icon` )instead ofns = `core`, and resolve( )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 with ns` 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/FIELD as the property FIELD of
    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; length is 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
    was type|control|member|value, so a second <Button type="Wrong"/> was
    folded into the first: the count understated and --fix corrected 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-new and its siblings
    are counted per occurrence from now on, so a baseline's counts move;
    --update-baseline settles them.

  • member-too-new no 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.52 FeedListItemAction,
    sap.f.cards.BaseHeader (@1.86) out of the @1.64 Header,
    sap.tnt.NavigationListItemBase (@1.121) out of NavigationListItem — and
    the members that move up arrive untagged, so the scan dated
    FeedListItemAction.text at 1.137 and Header.press at 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.
    XMLTemplateProcessor recognizes an aggregation only where
    childNode.namespaceURI === ns, the namespace of the control tag it sits in;
    <f:content> under a sap.m.Page is loaded as the control sap.f.content
    and the 404 takes the view down. Reported as unknown-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: items on an
    f:GridList is inherited from sap.m.ListBase and 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-aggregation with 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 the sap.ui.define dependency list with the
    first function ( of the FILE, split on commas. Every sap.m.p13n module
    and integration's Paginator are written with an arrow factory, four
    calendar controls carry a comment inside the parameter list, DragDropBase
    comments two dependencies out, MockServer writes sap.ui and .define(
    on two lines — and all of them came out with parent: null, which the
    property gate reads as "a root class": every inherited property an
    unknown-property, and sap.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.integration ships 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 of Control, Element,
    UIComponent and friends carries @example code — 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.js quotes. 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/base module exports. 973 controls become 959, all of them real.

  • An aggregation that says nothing is 0..n, not 0..1. The snapshot wrote
    multiple only when a declaration said multiple: 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.items and
    quickActions among them, so a menu with two items was too-many-children.
    Every aggregation and association now carries multiple as
    ManagedObjectMetadata resolves it (false written out where it used to be
    implied), and Page.subHeader with 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.115 and Since 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)
    and sap.ui.integration.ActionDefinition.buttonType (Since 1.130) were
    reported to a 1.71 app. Every spelling is read now (sap.m.TablePersoController
    and sap.ui.core.search.SearchProvider included), and a version that ended
    a sentence no longer keeps the full stop — 45 entries read 1.88..

  • An event parameter is not the aggregation of the same name. The flat
    members map topped itself up from every @since block in a class, so
    SinglePlanningCalendar.appointments — an aggregation with no version —
    carried 1.67.0 from the appointments PARAMETER of appointmentSelect,
    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
    declares key and sortOrder on change and 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,
    flagged fired: true so 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 an unknown-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-icons runs again. It read the @openui5 pin from the
    root manifest's optionalDependencies, 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.json byte for byte.

  • HASH_REPLACE and HASH_ATTACH_CHANGED are released constants. Both
    are in z2ui5_if_client=>cs_event and 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-upstream reads the cs_event block of the
    released interface and demands that every value in it is accepted by
    FRONTEND_EVENTS, FRONTEND_EVENT_ALIASES or SERVER_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, a Text and a buttons aggregation onto two lines, and
    chain-element-per-line said so. It is one call per line now, and npm 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| became n and |\t|
    became t in every value it resolved. The transpiler settles what they
    actually mean: @abaplint/transpiler's StringTemplateTranspiler emits 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.md teaches 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, \r and \t keeps 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:HTML stylesheet, a CodeEditor showing JSON and one more — and each
    drops one from its bindings count, 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( ) and parseNamedArgs( ) in lib/abap.mjs, classify( )
    in the chain-layout reader and the row scanner in reconstruct.mjs.

    The quieter half is the value resolver, which knew a backtick literal and a
    template: an attribute written v = '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`` and n = '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-reassigned and 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 test now 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-visibility advertised a fix over an example the fix
    declines: the correction moves the declaration into the PUBLIC 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
    --fix writes - and npm test runs 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 to client->view_display( )", which is how the sample corpora
    teach — was judged on its own sentence. It reported obsolete-binder,
    obsolete-model-update, obsolete-frontend-event, obsolete-bind-argument
    and binding-to-local (four of them warnings, so CI went red), raised
    phantom events for event-without-handler to call dead, and counted a
    quoted display as the first of two for double-display-in-branch — that
    last one on the framework's own Hello World sample. Worse, three of those
    rules carry a fix: --fix silently 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, and npm test now 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-slot and raw-javascript-to-frontend. Seven rules read a
    literal ON PURPOSE - an icon name, a t_arg string, 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_constructor or INTO 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( ) sets url on every finding
    — the rule's card on the rules page — so it is in --format json and 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( ) in scripts/generate-rules-page.mjs: a chain
    fragment under a namespaced View root in a displayed main( ), 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 test decodes 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( ) in abap-source.mjs — what runs whenever the call runs,
    nested blocks blanked): unconditional-popup-display (a popup_display( )
    at the top level of main( ), 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) and double-display-in-branch
    (the same slot twice in one sequence, the first dead; a hint).
    popup-without-close-wire reports a Dialog built in a method that wires
    no event at all (a hint, per method). event-name-case-mismatch is the rule
    event-without-handler was deliberately blind to: a raise spelled save
    against a handler reading SAVE — the runtime compares letter for letter,
    so the handler never runs; a warning, with a --fix that 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 --fix that 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: Buttom gets no suggestion, because Button would be a guess
    and Test for Text a wrong one. lib/suggest.mjs holds the one
    function; attachSuggestionFixes( ) in findings.mjs finds 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-visibility reads 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 the check_on_init( ) call from the OR, or the whole init arm of the
    fork; binding-to-reference inserts the ->* (for TYPE REF TO data only,
    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-style doubles the backslash inside the
    template, across every segment of the sheet in one pass; class-constructor-visibility
    moves the declaration line under PUBLIC SECTION. of the same class;
    escape-sequence-in-backtick splits 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-builder says it in a friendlier, shorter way. The message
    led with DEPRECATED and NOTHING about the view was checked, and read like
    a class on z2ui5_cl_xml_view was 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" } (or false) for a repo that stays on the old builder on purpose.
    RULE_DOCS and the rules page carry the same rewrite.

  • Every rule documents a before/after pair, and every report links at it.
    RULE_DOCS gains remedy — the same source as example, fixed — for all
    120 rules, gated like example is (missing one fails npm 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, so example was 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 in lib/ 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 SARIF helpUri now 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, like redundant-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 writes xsdbool( )
    for you (a warning - and no fix, because boolc's FALSE is a blank while
    xsdbool's is initial, which is behaviour, not spelling). And DELETE itab INDEX sy-tabix inside a LOOP AT over 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 (deleteIndex decrements 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.
    The source-line-too-long
    precedent again - a consumer whose only gate is npx abap2ui5lint must 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 its source-line-too-long neighbour. Measured over abap2UI5's
    own src (200 .abap files, which gate these themselves): 0 findings.

  • stats.ruleHits: per-rule fired counts, before suppression. The --json
    stats gain a ruleHits map (rule id -> findings the gate produced), counted
    BEFORE the rules block, 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 extends and maxWarnings (+ --max-warnings). A config can
    name another abap2ui5lint.jsonc/.json as its base: the extending file wins
    per key, the two rules blocks 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, whatever failOn says -
    the way to fail on errors only while still capping the warning debt.

  • --format checkstyle and --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 our hint to its info; 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.xml name (or content starting with <) goes down the raw-view
    path, anything else is read as an ABAP class. Exit codes and every
    --format behave exactly as for a file. The render gate stays off for a
    pipe, and --fix, --render and --screenshot are 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-i unwraps the CONV (the rule already
    guarantees it is the entire right-hand side of an assignment into a
    TYPE i target declared in this file); lifecycle-is-initial rewrites
    IS NOT INITIAL on a lifecycle call to the predicative form and
    IS INITIAL to = abap_false — on a plain abap_bool variable only the
    IS INITIAL half is rewritten, because = abap_true vs <> abap_false
    for the NOT form is a choice, not a mechanical rewrite, and that one stays
    a finding; trailing-empty-event-arg deletes 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": true in the config turns it on for a repo,
    and a corrupt or foreign cache file reads as empty rather than as an error.
    --fix needs no special handling: it rewrites the file, so the stale entry
    never matches again.

  • checkFiles and screenshotFiles accept a caller-owned renderer. An
    already-open session from openRenderer( ) (the ./render export) is used
    as-is and never closed - the caller owns its lifecycle - so a long-lived
    consumer (mcp-server's validate_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 at openRenderer time.

  • 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": true does), renderPages on checkFiles. 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.

  • checkNodes stops 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. statementAt in the
    reconstructor binary-searches the offset-sorted statement list instead of
    scanning it backwards. No verdict changes.

  • scrub( ) and blankLiterals( ) 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),
    and blankLiterals' 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 the ifBranchEnd/ifBlockEnd loops hit deterministically. Signatures
    and outputs are unchanged.

  • applyRules compiles a rule's config once per run, not once per finding.
    Every finding used to recompile its rule's exclude regexes 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.