Skip to content

v0.9.0

Latest

Choose a tag to compare

@github-actions github-actions released this 14 Oct 09:07
· 12 commits to main since this release
369f4af

Read before upgrading

  • This release is 0.9.0, not 0.8.6. In 0.x the minor is the number
    that says "a consumer has to act" (npm version minor, RELEASING.md), and
    several things below need action rather than a lockfile bump: the
    baseline key format (a baseline written here is misread by 0.8.5 and
    older), the refused numeric ui5, the baselines a downstream repo has to
    refresh, the baseline judged on an empty walk and read from its default
    file, and the refused flag pairs.
    Nothing removed or renamed an export or a rule id - a renamed id is the
    one change that would have been a breaking change on its own - and the
    --json shape only grew. @abap2ui5/linter-render moves to 0.9.0 with
    it (one tag, both packages) and npm run sync-peer-range writes the peer
    range; its FLOOR stays 0.8.0, since nothing here needs a newer runtime.

  • Refresh every downstream baseline with this release - and move every
    tool that reads it at the same time.
    Fixed false positives leave their
    baseline entries behind, and a stale entry fails a run (exit 1) just as a
    new finding does. abap2UI5-addons/popups carries one now:
    src/99/z2ui5_cl_pop_search_help.clas.abap|missing-view-display-on-navigated,
    the negated guard IF client->check_on_navigated( ) = abap_false. RETURN.
    that 9c630cc (#134) stopped misreading - measured, popups' configured run
    exits 1 on that entry alone. abap2ui5lint --update-baseline drops it.
    The rewritten file uses this release's keys (the baseline-key bullet
    below), which
    0.8.5 and older do not read: bump the Action pin, any npx pin and the
    VS Code extension's linter pin in the same change.

  • abap2UI5/samples needs one waiver. A void helper's parameter now
    resolves from the call being replayed (Reconstruction), so app 507's
    three render_list( … items = client->_bind( … ) ) calls reconstruct
    their items bindings - and omit-initial-drops-false sees the
    omit_initial = abap_true list whose enabled column the sample drops on
    purpose ("Archived is enabled now, its abap_false was initial too"). A true
    positive on a sample that demonstrates the defect: waive it there
    (" abap2ui5lint-disable-next-line omit-initial-drops-false -- the sample shows exactly this). Nothing else changes on the seven corpora.

  • Baseline keys carry no source position any more - and an older linter
    cannot read the new ones.
    Ten rules kept two occurrences in one class
    apart by writing a position into a key field: the source offset into
    value (default-key-table, abapdoc-html-tag,
    event-arg-default-index, display-after-nav-app-call,
    double-display-in-branch, empty-catch-block,
    boolc-instead-of-xsdbool, delete-index-in-loop) or the line number
    into member (source-line-too-long, trailing-whitespace). Both fields
    are part of the baseline key, so a line inserted above a baselined finding
    made it a stale entry AND a new finding. The position now travels as
    dedupe, which only the per-class collapse reads and which is dropped
    before a finding leaves the rules (the line says where it is). A
    baseline written before keeps working: loadBaseline( ),
    applyBaseline( ) and updateBaseline( ) read an old key of one of the
    ten with a number in that field as today's key (migrateKey( ) on
    ./baseline), so it matches wherever the code moved, and the next
    --update-baseline writes the new form. The other direction does not
    hold: 0.8.5 and older compute the old key for those ten rules, so a
    baseline rewritten by this release waives nothing of theirs - the findings
    come back and every such entry is reported stale (measured: a baseline
    written by --update-baseline here fails a 0.8.5 run with exit 1). That
    reaches whatever judges the same baseline with an older linter - an Action
    or npx pinned to an earlier release, and the VS Code extension until its
    pin moves, whose Add to Baseline then writes an old key beside the new
    one, which this release reads as one key counted twice and reports stale.
    Move every tool that reads one baseline to this release together.

  • A numeric ui5 / minUi5 in the config is refused (exit 2) with a
    message that says why: JSON reads 1.120 as the number 1.12, so the floor
    moved eight minors down without a word. Write it quoted, "1.120". A
    config that wrote "ui5": 1.71 unquoted worked before and has to be
    quoted now; the schema always said string.

  • A run whose walk collects nothing now has its baseline judged. The
    "no checkable app classes" return ran BEFORE the baseline block, so the one
    run in which every entry is stale by construction - the classes were
    deleted, or moved under ignore - was green: no stale entry was reported
    (exit 0 instead of 1), --update-baseline kept the entries instead of
    pruning them, and a --baseline file that did not exist was no error. The
    empty run applies the baseline (stale entries print under the sentence in
    the stylish report, ride in the json document's baseline block and
    decide the exit code in every format; --advisory still exits 0),
    --update-baseline prunes the entries of the files that are gone, and a
    named baseline that is missing is exit 2. A consumer whose corpus was
    emptied and whose baseline still has entries sees exit 1 where it saw 0 -
    abap2ui5lint --update-baseline settles it.

  • abap2ui5lint-baseline.json is read where --update-baseline wrote
    it.
    Without --baseline and without "baseline" in the config the
    update wrote that file and no later run looked for it, so every run kept
    reporting the debt it had just frozen. A run that names no baseline reads
    abap2ui5lint-baseline.json next to the config (or, without one, in the
    current directory) when it exists, --update-baseline writes the default
    to the same place (next to the config, where it used to be the cwd), and
    the update prints a second line saying so and how to pin it (--baseline <file> or "baseline" in abap2ui5lint.jsonc). A repository that happens
    to carry a file of that name next to its config, from an earlier update it
    never wired up, is baselined from now on - the run summary's baselined
    row and the stale-entry rule say so.

  • Contradictory or ineffective flag pairs are refused (exit 2) instead of
    silently doing something else.
    A value flag no longer takes the next
    option as its value: --allow --no-render allowed a control called
    --no-render and ran the render gate the user had just switched off,
    --sarif-out --json wrote a file literally named --json. Also refused:
    --update-baseline beside --json-out, --sarif-out, --badge or
    --badge-corpus (the sidecars were never written, and the Action's
    outputs stayed empty); --stdin --update-baseline without
    --stdin-filename (<stdin>|... entries no run could match);
    --deprecated-at equal to --ui5 (an empty horizon that reported
    nothing); --no-config beside --config; --badge twice;
    --cache-location without --cache; --stdin-filename without --stdin.
    A workflow carrying one of these in flags goes red with the message
    where it was silently green. The machine --formats beside
    --update-baseline keep their contract (the "baseline: wrote" line
    on stderr, stdout empty), now spelled out in --help.

Rules

  • New opt-in rule bound-aggregation-over-size-limit (warning, off
    until a rules entry asks for it). A list bound to a table the class
    seeds past the JSONModel's 100 entries - the items of a non-growing
    sap.m.ListBase, a Select or a ComboBoxBase, bound without a
    length, in a class that raises no size limit - stops at 100 without a
    word. The count is read where the source decides it, as a lower bound:
    the table's one literal VALUE #( ( … ) ( … ) ) seed, or the APPENDs
    directly inside literal DO n TIMES loops (on the start path's
    check_on_init( ) / check_on_navigated( ) branch too). A table the
    class may also shorten or replace - DELETE, CLEAR, a second seed, a
    SELECT into it, a method it is handed to - is not judged, nor an APPEND
    under any other condition. The general rule was built and dropped in 0.3
    for noise on the samples-controls ports, whose originals cap at 100 too;
    opt-in, it is offered to app authors, for whom 123 rows showing 100 is the
    defect.

  • A deprecation horizon: deprecatedAt / --deprecated-at, and the
    rule deprecated-after-target (hint).
    ui5 answered two questions with
    one number - what exists on the target, and what counts as deprecated -
    so an app held to 1.71 heard nothing about the 60 controls and 58
    members UI5 deprecated between 1.72 and 1.136, the legacy-free baseline
    SAP measures best-practice code against (#148). With
    "deprecatedAt": "1.136" beside "ui5": "1.71", what took effect after
    the target and up to the horizon is reported, naming the replacement the
    deprecation links and whether the target already has it
    (sap.m.MessagePage → IllustratedMessage, @SInCE 1.98: keep it until
    the target moves). Every other rule still judges against ui5; a horizon
    below the target is refused. Unset, nothing changes - no consumer sees a
    new finding until it asks for the horizon.

  • native-html-in-view (hint): native XHTML/SVG in a view. UI5 1.120
    deprecated markup in the XHTML and SVG namespaces inside an XML view; the
    XMLView logs a [FUTURE FATAL] for it and the next major release refuses
    the view. Reported once per island, on its outermost element, and only
    on a target of 1.120 or later - below that it is a
    deprecated-after-target under a horizon and silent without one. A hint
    for its first release: abap2UI5 views have long carried <html:style>
    blocks, and a corpus on a 1.120+ target should see its count before the
    rule can fail a build.

  • chain-unbalanced-parens ends an unclosed call at its statement's
    period.
    Its scan read an unclosed ( as open to the end of the file:
    every statement behind it ran into one, so a SECOND broken chain was
    never reported, and a stray ) further down was counted off against the
    open one and vanished. A . before a blank ends the statement at any
    depth now - parenRegion( )'s rule, since no ABAP call reaches past one -
    so each broken chain is reported on its own statement. No corpus carries
    one.

  • unbound-public-attribute and unused-public-attribute see a reader in
    the same run.
    A popup hands its result back through a PUBLIC attribute
    its caller reads (CAST zcl_pop( client->get_app( … ) )->ms_result), and
    a caller sets a value on the app it calls the same way - unbound by
    construction, and both rules said no single source can see that caller.
    The class index (classIndexOf( )) now records per class outsideReads:
    the names another class of the run reaches as ref->name through a
    reference it types as this class (TYPE REF TO, an inline declaration
    from CAST/NEW/a static factory, or that expression itself before the
    arrow); a reference whose class is not written down is nobody's. Both
    rules stand down for those names. Because that stand-down depends on what
    the RUN holds, a waiver of either rule in such a class is unjudged rather
    than unused-directive (publicReadFromOutside( ) on ./abap-rules,
    fed to the directives as stoodDown): the same class linted alone - an
    editor, a pre-commit hook - still needs it, and the unused-directive fix
    would have deleted it. classIndexDeps( ) carries the set, so --cache
    re-judges the popup when a caller starts or stops reading it, and --fix
    re-checks it between passes. ClassIndex in types.d.ts gains the
    optional field. Corpora: nothing is reported differently -
    abap2UI5/samples app 024's and abap2UI5's node/srv/zcl_tst_stack_a
    waivers for the attribute their callers set stay (unjudged in a corpus
    run, used when the class is linted alone); the other outside readers in
    the corpora are ABAP Unit test classes, which no run collects, or call the
    popup's result( ) method.

  • A COND #( ) or SWITCH #( ) of literals is judged branch by branch.
    COND #( WHEN n = 1 THEN \Emphasized` ELSE `Default` )on a Buttontypehas a closed set of values, and it was reported asunresolved-attribute-valuewhile a wrong value in a branch went unjudged. The reconstructor records the values of a conditional with anELSEwhose every result resolves (a literal, a&&chain, a nested conditional of the same shape; aTHROWbranch yields none) as the node'sbranchValues, and the property gate judges each like a literal: invalid-property-value(with its case-only--fixlanding on the branch) and the other value rules, silent when all of them pass. NoELSE, a LET, or a result that does not resolve keeps the hint. abap-cloud-gui: its two unresolved-attribute-value` hints are gone.

  • uncurated-formatter reads more of the formatters a view names. A
    CONTROLLER formatter in a class (formatter: '.weightState', the demo
    kit's own form) names a function on the view's controller, which in
    abap2UI5 is the framework's and defines none: the view fails to load with
    formatter function .weightState not found, and only the render gate said
    so. A raw view is left alone (it may be a freestyle app's, with a
    controller of its own). And every alias a core:require points at
    z2ui5/model/formatter is read ({ Fmt: 'z2ui5/model/formatter' } with
    formatter: 'Fmt.round2DP'), the did-you-mean keeping the written alias;
    the expression form no longer matches a name that merely ends in
    Formatter (myFormatter.round2DP().

  • int, float and boolean values are judged the way DataType parses
    them.
    The numeric check was ^[+-]?\d+(\.\d+)?$: .5, 5. and 1e3 -
    numbers to DataType's Number( ) - were reported as invalid, while 10.5
    on an int property (which Number.isInteger refuses) passed. An empty
    value is valid for all three (NaN for a number, false for a boolean),
    and a value the reconstruction guessed from a LOOP variable is not judged
    as if the author had written it.

  • An xmlns on an inner element is scoped to it. Every declaration of
    a document went into one map, the last one winning, so <VBox xmlns="sap.ui.layout.form"> inside a sap.m Page turned the Page above it
    into an unknown-control, and a prefix declared in one subtree passed as
    declared in another, which the browser's parser refuses. The property
    gate, the control-id map, the enum-bound-field reader and the run
    summary's profile resolve each element against the declarations in scope
    at it (namespaceScopes( ) in lib/properties.mjs), and
    undeclared-namespace reports a prefix used outside the element that
    declares it. The top level stays one scope (a second root is judged with
    what the first declares).

  • popup-display-xml, popover-display-val and
    popover-anchor-unknown-id read the CLIENT's popup_display( ) /
    popover_display( ) call and find the argument among the named ones
    wherever it stands: an app's own my_popup_display( xml = … ) was
    reported and "fixed" into a parameter the method does not have, and
    popover_display( by_id = … val = … ) went unseen.

  • view-never-displayed no longer reports a helper class that hands its
    stringified view to the caller - result = view->stringify( ) where
    result is a RETURNING, EXPORTING or CHANGING parameter of the method
    (abap-cloud-gui's z2ui5_cl_cgui_list and z2ui5_cl_cgui_selscreen,
    which its config excluded by name).

  • unescaped-text-in-attribute leaves id and class alone: the
    XMLTemplateProcessor takes both verbatim and never parses a brace in them
    as a binding, and the fix, t =, wrote a backslash into the id or the
    class token.

  • undefined-css-class leaves a class value the reconstruction guessed:
    |state-{ ls-row-state }| in a LOOP came out as state-, the literal
    part of a class completed at runtime.

  • unknown-icon reads `sap-icon://status-` && lv_code as the prefix
    it is - the backtick spelling of |sap-icon://status-{ lv_code }|, which
    was already not judged.

  • editable-control-without-binding says the control "takes user input"
    rather than that it "lets the user type": 152 of samples-controls's 343
    findings are on controls nobody types into (RadioButton, CheckBox, Switch,
    Slider, Select, …).

  • "rules": { "<id>": true } switches an opt-in rule (chain-house-layout,
    portable-app) on. It passed the config check and ran nothing; true now
    means "on, as it is".

  • unused-directive leaves a waiver alone where the rule it names never
    ran: in an app class whose view another class builds, a helper class under
    --all-classes and a class on the frozen builder, only some rules judge
    the class (SOURCE_RULES, new on ./abap-rules, plus the class-reading
    rules for an app class), so a waiver of unbound-public-attribute or a
    property-walk rule there had nothing it could suppress - and its fix
    deleted the waiver the class needs the day its view moves back in. It is
    unjudged now, as under --no-properties. No corpus carries one.

  • unescaped-brace-in-style and collapsed-brace-in-style read a <style>
    tag with attributes, in any case.
    Both rules bounded a stylesheet with the
    literal <style> … </style>, so a sheet opened as <style type="text/css">

    • the form a core:HTML content copied from an HTML page carries - was
      never judged, and its unescaped braces reached the XMLView parser as a
      binding. The opening tag is read as lib/css-classes.mjs reads it
      (<style\b[^>]*>, case-insensitive), the closing tag the same way.
  • hardcoded-binding-path stands down inside a <style> value again. The
    exemption the rule documents - {/ between <style> and </style> is an
    escaped CSS brace meeting a CSS comment, not a path - had gone out of
    judge( ) when the rule was narrowed to attribute values, and
    v = +'<style>.a \{/* brand */ color: red; \}</style>'+rwas reported. A literal that opens a<styletag is not judged, and a{/written behind a backslash is never a path, in any literal. The test that asserted the exemption did so on aDATA(css) = …assignment the rule no longer judges at all, which is why it kept passing; it asks on acontent` value now.

  • binding-to-local judges the ROOT of the bound name. The scan required
    the name to end at a blank or ), so client->_bind( ls_local-field ) -
    a component of a method-local structure, lost with it after the roundtrip -
    was never seen while _bind( lv_plain ) beside it was. The root decides, as
    it does for binding-to-static and binding-to-nonpublic. Two shapes the
    old scan did not judge stay unjudged: a -> dereference (a reference into
    data that lives elsewhere) and a call carrying tab =, the framework's
    cell binding whose path is derived from the table.

  • duplicate-for-iterator folds case. FOR i and FOR I in one method
    are one variable to the 7.02 downport that materializes them as
    DATA i TYPE i, and were two names to the rule. The member is reported as
    written.

  • One list of boolean types. abap-rules.mjs declared its own
    (abap_bool|abap_boolean|boolean|xfeld|flag) and guide-rules.mjs another
    with xsdboolean and boole_d on top, so a DATA mv_flag TYPE boole_d
    written through v = was not an unconverted-abap-boolean, not a
    manual-init-flag, not a lifecycle-is-initial candidate - while
    omit-initial-drops-false saw it. BOOLEAN_TYPES is exported from
    guide-rules.mjs and read by checkAbapBooleans( ), whose boolVars the
    two lifecycle rules and the text rule's isBool share.

  • Measured on abap2UI5 src + node/srv (103 findings in 39 files) and this
    repo's fixtures with --all-classes (327 in 80): no change per type.

Reconstruction

  • A void helper's parameter is the value its call passes. A helper the
    replay enters (add_row( page = page text = \Save` )) that writes its parameter where the view needs a value (a( n = `text` v = text )) left that value unresolved - a parameter is the caller's - so the attribute was dropped with every rule that would judge it. During the replay of one call the caller is known: the parameter is the value THAT call passes, resolved where the call stands (a parameter handed on to a second helper through the first one's call). Only when every call of the helper in the class passes a value that resolves, and the helper never writes the parameter itself: a me->call or aCALL METHOD(which the replay does not enter) or one argument computed at runtime keeps it a variable for every call. Eleven documents on the seven corpora gain their attributes this way (samples-controls 585, demo_001 and app 000; abap2UI5/samples 000, 502, 507 and 512; samples-stack 000; abap2UI5'sz2ui5_cl_ui5_app_start` and its downport; abap-cloud-gui's range
    popup) - no new render error, and one new finding (samples 507, see Read
    before upgrading).

  • A quote in a comment no longer silences the class. The comment and
    literal readers (scrub( ), blankLiterals( ), splitStatements( ))
    lost their place in three shapes - a string template whose embedded
    expression runs over two lines, a | in a literal inside an embedded
    expression (sep = `|`), and a UTF-8 BOM in front of a * comment
    line. The comment behind it was then kept as code, a ' in its text
    (" don't care) opened a literal that ran to the end of the file, and
    most ABAP-side rules reported nothing for the rest of the class. scrub( )
    now carries an open embedded expression over the line break and reads a
    " inside one as a comment, a BOM is blanked, and every reader ends a
    quote or backtick literal - and a template's text - at its line, as ABAP
    does, so a reader that is ever wrong again is wrong for one line. The one
    literalEnd( ) is shared by parenRegion, topSplit, namedArgMarks,
    the reconstructor's paren scan and chain-house-layout, whose own scanner
    closed a template at the first | and so lost the ) of a crammed chain
    call behind |{ concat_lines_of( table = mt sep = | ) }|.

  • A CONSTANTS value resolves. CONSTANTS c_base_url TYPE string VALUE \https://…`andt = c_base_url && `sample1.jpg`, the chained CONSTANTS:form and one level ofBEGIN OF cs … END OF csread ascs-name` were dropped as values the gate cannot follow, so a URL, a
    colour or an enum value kept in a constant was judged by nothing and
    rendered as absent.

  • A name written once with a string template resolves to it.
    DATA(expr) = |\{= ${ client->_bind( flag ) } ? 'A' : 'B' \}|. and then
    v = expr on five controls - how a port hands one expression binding
    around - was dropped (samples-controls apps 445 and 452 carried fifteen
    unresolved-attribute-value hints for it). So was a name written once with
    cl_abap_char_utilities=>newline. A second write of the name in its scope
    keeps it a variable, unresolved as before, and so does a template with a
    piece the scan cannot compute: |{ tenths DIV 10 }.{ tenths MOD 10 }em|
    would stand as the guess .em, which the render gate refuses as a
    CSSSize.

  • A name is read in its scope. A literal or a template the class gives
    a name was read class-wide - the last literal assignment anywhere - so a
    helper's v = type came out as main's DATA(type) = \Bogus`, reported as invalid-property-valueand rendered with a value nobody passed, and two methods' locals of one name both read the later one. A name that is a variable of the method it is used in - an inline or aDATA/STATICS/ CONSTANTSdeclaration there, or one of its parameters - now reads that method's own writes, the last one before the use; any other name is the class's attribute and reads the class's writes to it, as before. A parameter's value is the caller's, so a helper'sv = typeis unresolved rather than borrowed. On the seven corpora no document and no finding changes from the scopes; abap-cloud-gui'sz2ui5_cl_cgui_rangeno longer carries the guessvalue="{}"for|{{ part }}|`.

  • A pragma behind an argument value (v = `Bogus` ##NO_TEXT) is no part
    of it: the attribute was dropped as unresolved, and every check of its
    value with it.

  • A builder handle kept in an attribute and stringified in another method
    is skipped, not failed.
    abap2UI5's node/srv/zcl_tst_host builds its
    page in one method, keeps the handle in mo_main_page, lets a sub-app
    created by name build into it and stringifies it in another; the replay
    enters only the methods that open a factory, so the render gate failed the
    class with no view reconstructed from builder calls. When the replay
    yields no document, the builder calls in the methods it never entered are
    counted as helperTokens, so the class is skipped with the usual "built in
    helper methods" note (abap2UI5's render-error waiver for it now waives
    nothing; sapgui's zcl_sapgui_frame and zcl_sapgui_se91 are the same
    shape). A class whose replay yields a document is unchanged.

  • me-> is read as the attribute it names, everywhere the reconstructor
    reads a target or a handle.
    me->mv_type = +'Error'+rseeded nothing: the bound enum was modelled as"", and the render gate reported a false render-erroron a correct class.me->mo_view = …factory( ), me->mo_page = me->mo_view->ele( … )andme->mo_page->tag( … ) were no handle, so a view held in attributes produced no document at all. One fragment (OWNinlib/reconstruct.mjs) now stands in front of every such name - the literal and template writes of the resolver, the scalar and table seeds, the derived seeds, the factory, capture and handle chains, and the capture scan that chooses the handle-aware reconstruction - and the bare name is the key. A write spelled me->xis the ATTRIBUTE's write even where the method declares a localx. FINAL(view) = …is an inline declaration likeDATA(view) = … in the same places (INLINE`).

  • The client under another name binds. bindingOf( ) knew
    client->_bind( ) alone; mo_client->_bind_edit( mv_name ) and
    me->client->_bind( mv_name ) were "unresolved value expression dropped",
    the attribute gone from every gate's view of the class. It reads any word
    ending in client, with or without me-> - the reading the event rules
    already had (clientArrows in lib/abap.mjs).

  • Helper declarations are read per METHODS element. Three things hid a
    helper: the keyword was required in front of EVERY name, so a
    colon-chained METHODS: render_toolbar …, add_button … RETURNING … declared
    no helper; the lazy IMPORTING[\s\S]*?RETURNING ran from a void method's
    declaration across its period into the next one, so a void method declared
    ahead of a RETURNING helper swallowed it; and the returning parameter had
    to be called result. parseHelpers( ) reads each METHODS statement
    through splitStatements + topSplit(',') (as paramsOf does), judges
    every element on its own, and takes the returning parameter's name from
    the signature (ro_view = io_view->tag( … ) is the chain).

  • A RETURNING helper's parameter is substituted in code only. The
    inlined body had its parameter names replaced textually, literals
    included: a parameter named text turned n = +'text'+rinton = +'``Save``'+r, which parsed as no attribute at all (the Button lost
    its text, and every rule that judges it with it). codeOnly( ) masks the
    literals - the embedded expressions of a |…{ text }…| template stay code
    and are still substituted - and an argument NAME in front of = is never
    the parameter.

  • An element or attribute named by a constant resolves. ele( n = c_page )
    and a( n = c_attr v = … ) were "unparsed": the element was dropped, its
    children reparented into the element above (a false unknown-property on
    their attributes) and its end( ) closed that one instead (a false
    excess-shut). n = is read through the resolver now (nameOf( )),
    which knew the constant all along. A name that does not resolve - a
    parameter, a computed string - still OPENS an element: a nameless
    placeholder node (unresolvedName) keeps the tree's shape and the end( )
    count right; toXml( ) and the walk look through it as they do through the
    document wrapper, and its attributes stay off the element above.

  • A pragma after VALUE no longer hides the declaration. DATA mv_state TYPE string VALUE 'Success' ##NO_TEXT. read as no declaration ("bound
    variable has no DATA declaration - mocked as string", value lost), and
    CONSTANTS c_title … VALUE 'My Title' ##NO_TEXT. as no constant. Pragmas
    are blanked (blankPragmas) before DATA_DECL, LIKE_DECL, the
    constants reader, the literal writes and the scalar seeds.

  • A conditional inside a VALUE #( ) row leaks nothing. The row reader
    took a seed pair by pair, so state = COND #( WHEN mv_ok = abap_true THEN … ) made MV_OK a FIELD of the row and seeded STATE as "" - a false
    render error on the enum ("" is not a ValueState). A parenthesised value
    (COND #( ), SWITCH #( ), CONV string( ), a method call, a bare
    ( … )) is skipped as one region (SEED_CALL); a conditional whose every
    branch is a literal seeds its ELSE value (literalBranches with a
    literal-only resolver), and anything else leaves the field out of the
    render model - the control keeps its default, as for an unseeded scalar -
    while the shape keeps the declared default so the path is still judged.
    The leak hid a true unknown-binding-path on {MV_OK}, which the probe
    class now reports.

  • An IMPORTING … DEFAULT is the value a call that omits the parameter
    passes
    , so add_button( io_page = page ) resolves text to the
    declared +'Save'+r in the replay (defaultsOf beside paramsOf). And a
    numeric literal (v = 20) is its digits, not a name to look up: it used to
    be dropped as unresolvable.

  • Measured (abap2UI5 src + node/srv, 39 files, and the 100 fixtures with
    --all-classes): finding counts per type, per-file stats and reconstructor
    notes identical before and after; the render gate's errors identical. The
    probe classes that reproduce each item are in
    test/review/round-2026-10-12-reconstruct.mjs.

--fix

  • --fix runs to a fixed point. One pass applies every fix whose span
    overlaps none applied before it, and a fix can leave a shape another rule
    fixes (a deleted trailing t_arg row leaves the one-row table
    event-arg-single-row-table rewrites; chain-house-layout re-lays a
    chain another fix sat in) - so a run ended with "N deferred to the next
    run" and the reader ran --fix two or three times. The CLI now repeats
    the pass in memory, re-checking the files the previous pass changed (and
    any whose class-index facts - superclass chain, outside reads - moved),
    and writes each file once: the text the old --fix reached when it was
    run until it changed nothing. Bounded at MAX_FIX_PASSES (10, ESLint's
    bound, exported on ./fix); a run that stops there says so. The summary
    names the passes (fixed 6 problem(s) in 1 file(s) in 2 passes), and
    --fix-dry-run lists a later pass's fixes with (pass N).

  • --fix keeps a CRLF file CRLF. The fixes that insert or re-lay lines
    (chain-house-layout, missing-on-navigated-branch, the inserted
    class_constructor, …) wrote LF, so with crlf-line-ending switched off a
    fixed file came back with mixed line endings. Every fix now writes the
    source's line ending (matchLineEndings( ) on ./fix, applied by the
    entry points to what survives the rules and directives) - LF while
    crlf-line-ending is fixing the file to LF in the same pass. And
    chain-house-layout compared a CRLF line break as if it were wrong, so it
    reported (and rewrote) every multi-line chain of a CRLF class whatever its
    layout.

  • chain-house-layout --fix indented a * comment inside the chain -
    and a * is a comment in column 1 only.
    The fix moved every
    continuation line of a re-laid segment by the segment's delta, a comment
    line among them, so the rewritten class did not activate. A continuation
    line whose first non-blank character is a comment keeps its column now - a
    " comment too, whose indent is the author's and not the chain's
    (lib/chain-layout.mjs).

  • redundant-serializable no longer deletes a comment with the line. The
    fix was anchored on the scrubbed copy, where a trailing comment is blanked
    to spaces that the pattern's [ \t]* before the line break swallowed, so
    INTERFACES if_serializable_object. " keep: the draft needs it went
    whole. A line the original spells differently from the scrubbed match is
    reported without a fix - the same( ) rule lifecycle-rules.mjs applies
    to its own deletions - and the bare line keeps it.

  • A moved declaration lands directly under its new section's last line.
    moveDeclarationFix( ) (binding-to-nonpublic, private-app-attribute)
    inserted at the line start of the NEXT section header, below the blank line
    that closes the target section, so the moved DATA stood alone under the
    gap, detached from its siblings. The blank lines are walked back over and
    the declaration is appended under the section's last declaration (under
    its header when the section is empty); the gap keeps closing the section.

  • Four rules that rewrote under --fix are listed as fixable and say what
    the fix does.
    unknown-view-slot (main → MAIN),
    invalid-frontend-action (a filter-row operator up to letter case,
    contains → Contains), association-unknown-id and
    unknown-source-property (the id or $source> segment up to letter case
    and separators) carried fixes without being in FIXABLE, so the rules page
    showed no --fix badge and no fixNote, and --fix rewrote what the page
    said it would not touch. The filter-operator fix rides on the finding now
    (as unknown-view-slot's does) instead of only on the pipeline's
    attachSuggestionFixes( ), so a caller of checkAbapRules( ) alone sees
    it too. And the missing direction is gated:
    test/review/round-2026-10-12-rules.mjs observes every finding type
    produced with a fix over the cards and the four shapes, and fails on one
    that FIXABLE does not list.

CLI, output and config

  • The class index covers every class under the configured paths. A
    class's verdict reads two facts out of OTHER classes: a cs_event it
    inherits, and a public attribute another class reads back
    (outsideReads). The index was built over the files the run checked, so
    it changed with what the run was handed: without --all-classes the
    superclass or caller that builds no view was never in it, and a one-file
    run (a pre-commit hook) saw no other class at all - both reported what the
    full run, and the editor, which indexes its whole workspace, silence. The
    CLI now indexes every ABAP class under the config's paths (the run's own
    paths without a config), minus ignore, whatever it checks - --stdin
    included, with the piped source in place of the file it names - and
    --watch re-runs when one of them is saved. Cost, measured on
    samples-controls (642 classes, 12 MB): a one-file run 0.25 s -> 0.58 s
    (the walk 40 ms, the reads 25 ms, classIndexOf( ) 275 ms); a full run
    unchanged within noise, since its own files are most of the index.
    Nothing changes on the seven corpora.

  • --stdin refuses a path beside it (exit 2). The path was neither read
    nor linted, and the run reported on the piped source as if the file had
    been checked: cat a.clas.abap | abap2ui5lint --stdin b.view.xml was
    green whatever b.view.xml held. The message names --stdin-filename
    and --config.

  • The stylish report no longer prints control characters out of the
    source.
    A message quotes the checked file, and an ESC in a literal
    reached the reviewer's terminal raw - an escape sequence that clears,
    retitles or recolours it, or hides the line it sits on. The stylish
    report, the annotations, the --verbose notes, the stale-baseline lines
    and the --fix listing print every C0/C1 control, DEL and the bidi
    override/isolate characters as a visible \xNN / \uNNNN (a tab or line
    break as a blank); terminalSafe( ) on ./report. JSON and SARIF carry
    the text as written.

  • --format checkstyle and --format junit stay well-formed whatever a
    message quotes: a control character becomes U+FFFD, and a line break or
    tab is written as a character reference, which an attribute keeps. Both,
    and the GitHub annotations, write /-separated paths on Windows too, as
    SARIF always did - an annotation with a \ path never reached its line.

  • A run with nothing to check prints the EMPTY document of a machine format
    (checkstyle, junit, sarif, as json already did) with the sentence
    on stderr, and still writes --sarif-out and --json-out: a workflow's
    upload-sarif step behind the Action's sarif input failed on a file that
    was never written.

  • --update-baseline beside a machine format writes its "baseline: wrote"
    line to stderr; stdout is the document a caller parses.

  • --advisory / --fail-on never exit 0 under --max-warnings (or a
    config maxWarnings) too, as they do for stale baseline entries; the
    excess is still said on stderr.

  • --max-warnings, --jobs and --render-pages take digits only. ''
    and ' ' were read as 0 and 0x2 as 2, so --max-warnings "$MAX" with
    the variable unset failed the build on the first warning.

  • A baseline file that is valid JSON but no object (null, a number, an
    array, a string) is refused with the file's name and the shape it should
    have, instead of Cannot read properties of null.

  • The config's ignore count ("N files ignored by config") is taken in the
    same directory walk; the CLI walked the tree a second time and read every
    class twice. collectFiles( ) takes an onIgnored callback for it.

  • A config writes only inside its own directory. The badge files (on
    every run) and the baseline (--update-baseline) a config names are
    repository content, and a cloned repository's "badge": {"file": "../../.bashrc", "label": "..."} had a plain npx @abap2ui5/linter in it
    overwrite that file with JSON whose label the config chose. A path that is
    absolute or leaves the config's directory is now refused when the config
    is loaded (exit 2, the message names the key), and one that leaves it
    through a symbolic link the repository ships is refused when it would be
    written. --badge, --badge-corpus and --baseline on the command line
    still write wherever they point. Every downstream config names files
    under its own directory and is unaffected.

  • A UTF-8 BOM no longer shifts the columns of line 1 by one.

  • isXmlSource( ) on the main export: whether checkFiles( ) reads a
    source as a raw view or as a class - the one predicate the library, the
    --fix passes and the cache's class index share.

  • --update-baseline does not write the default baseline through a
    symbolic link out of its directory.
    Without --baseline the file now
    lands next to a discovered config, in the repository rather than the
    cwd, so a cloned repository that shipped abap2ui5lint-baseline.json as
    a dangling link (nothing to parse first) had the update write the
    baseline JSON - whose keys carry the repository's own file names - to
    wherever the link pointed, a shell start-up file included. That is
    refused now (exit 2), as for a config-named baseline (below); a file
    named with --baseline is still the user's and is written where it
    points.

  • cli.mjs housekeeping, no behaviour: one ownPackage( )/ownVersion( )
    reads package.json (it was parsed three times: the schema URL, --version,
    the cache key), one shownPath( ) prints a path relative to the cwd where
    that is shorter (configShown in the report and nice in the watch loop
    were the same function), and the baseline block is one baselineStep( )
    shared by the normal run and the empty walk.

  • lib/config.mjs's key list documents baseline, which the loader has
    accepted since 0.8.x.

  • @abap2ui5/linter/schema is typed. The exports map has served
    data/abap2ui5lint.schema.json as ./schema and types.d.ts declared
    every subpath but that one, so a TypeScript consumer importing it got an
    implicit any under --strict. declare module "@abap2ui5/linter/schema"
    exports it as Record<string, unknown>, and the typings gate in npm test
    now requires a declare module block for EVERY key of the exports map -
    the JSON ones too, which the previous filter (typeof v === 'object')
    never asked about.

--cache

  • The cache file is never written through a symbolic link. It sits in
    the working directory - the repository - so a repository that shipped
    .abap2ui5lintcache as a link to a file of the user's, beside
    "cache": true, had every run overwrite that file. A link there is now
    refused with one line on stderr (the run itself is unaffected, as for any
    cache that cannot be written).

  • --cache follows the class index and the linter's own code. A class
    is judged against what its superclass declares (cs_event) and what the
    other classes read of it (outsideReads), and a cached run built that
    index out of the cache MISSES only: a subclass re-checked without its
    cached superclass read the inherited constant as unknown, and an edited
    superclass left its subclasses' stale entries in place. The index is built
    over every file of the run, and each entry is also keyed on the index
    facts its check read (deps, classIndexDeps( )). The context hash takes
    a fingerprint of cli.mjs, lib/*.mjs and data/*.json beside the
    version, so a checkout between releases or a regenerated snapshot does not
    replay the verdicts of the code before it. A run over part of a tree keeps
    the entries of every other file (and drops those whose file is gone), and
    the per-document models (docModels) are no longer stored. The cache file
    format moves to version 2; an old file is simply a cold cache.
    The index the deps are read from is the wider one of the CLI entry
    above, so editing a class the run only indexes - a helper that stops
    reading a popup's result - re-judges the popup too.

Render gate

  • An answer that arrives after a document's render window is charged to
    that document.
    A control that sends a request (a Card fetching its
    manifest) reports what comes back whenever it comes back, and the page
    meanwhile rendered the next document, whose window collected the error -
    samples-controls apps 118 and 168 failed app 180, 183, 184 or 185
    depending on the run. The harness counts the fetch/XHR requests a page has
    in flight and lets them settle into the window of the document that sent
    them; a page still waiting after settleTimeout (openRenderer( ),
    default RENDER_SETTLE_TIMEOUT_MS, 2 s) is reloaded before the next
    document, so a late answer can be lost but never misfiled. Not covered: an
    error a control raises from a bare timer long after its view is gone.

  • The browser's own network error is waived. A Card whose manifest lives
    on sdk.openui5.org fetches it when it renders; where that host does not
    answer (a sandbox, an offline runner) TypeError: Failed to fetch is
    environment noise like the other waived messages - whether a remote URL
    answers is not a property of the view. What the control says about the
    missing resource is still reported.

  • A raw .fragment.xml renders as a fragment. The gate decided view or
    fragment by ^<core:FragmentDefinition on the text, so a fragment file
    that opened with an XML declaration or a comment, wrote
    FragmentDefinition in the default namespace, or had a bare control as
    its root failed with "XMLView's root node must be 'View'". The file name
    decides now (.fragment.xml / .view.xml, on the result's docKinds and
    for --screenshot), and the renderer's own fallback reads past a BOM, the
    XML declaration, comments and a doctype and accepts any prefix. A leading
    BOM is dropped before the page parses the document, as the decoder drops
    it when the runtime loads the file: Chrome 153 (Playwright's chromium
    1243) refuses U+FEFF ahead of <?xml, and UI5 reported the parser's
    error document as "Using native HTML content in XMLViews is deprecated".

Performance

  • Linear on long input. Measured quadratic and now linear:
    default-key-table (a class of 10,000 TYPE TABLE OF lines took a
    minute, also without a terminator behind the declarations), the PUBLIC
    attribute reader and unbound-public-attribute (three regex scans of the
    class per attribute), client-handle-capture (a scan per captured
    handle), commercial-ui5-host (a regex tried after every - of one long
    hyphenated token) and ten regexes anchored on ^\s* under the multiline
    flag (the model's literal seeds and table seeds, the section DATA blocks,
    the event dispatch, declaresApp( ), …), which let every blank line -
    and every blanked comment line - start a scan over all the ones behind
    it. That last one is what the VS Code extension met: prepareAbap( ) runs
    on every edit, and 2,000 commented-out lines took a second in 0.8.5, 8,000
    sixteen. The findings are unchanged on the corpora; test/timing.mjs
    judges each case by n against 4n, so a loaded runner cannot fail it.

  • The class index (outsideReads) finds typed references by their TYPE REF TO keyword instead of trying a name at every word of every class.

  • Every rule is swept for growth. test/review/timing-sweep.mjs takes
    each rule's card - the example the rules page wraps into a class and
    verifies - and grows it around the reported line in nine shapes (a long
    run of blanks, a long literal, a long comment; a block of comment lines,
    of declarations, the statement repeated, the method copied; nested IFs and
    a nested view), every case judged n against 4n (screenedLinearly( ) in
    test/timing.mjs: one run each, the best of three only where that does
    not pass), plus four raw-view shapes. What it and the shapes behind it
    found, all three linear now: a class of many views with unused namespace
    declarations counted each prefix over the whole source once per finding
    (200 views, 5.7 s); an unclosed call handed the rule reading it the rest of
    the file as its argument list, so 2,000 of them took 11 s (parenRegion( )
    ends such a list at the statement's period now, and reads a " or *
    comment as a comment); and unescaped-text-in-attribute's write reader
    split a long blank run after a statement's first word every possible way
    (CLASS-METHODS and 32,000 blanks, a second). No finding changes on the
    seven corpora.

  • Three per-item RegExp rescans read once. navigation-lost-on-rebuild
    rebuilt two patterns and rescanned the class for every bound field of
    every navigation wire (literalsOf( ), memoized per field);
    omit-initial-drops-false built one per boolean column per omit_initial
    bind (the declared boolean names are read once per class);
    manual-init-flag built the = abap_true pattern per IF (cached per
    flag). Same verdicts, measured on both corpora.

Action

  • render: False means false. GitHub compares strings in an if:
    case-insensitively and the shell does not, so render: False skipped the
    runtime install and then ran the gate anyway, and annotations: False
    annotated. Both inputs are lower-cased in the shell, in the lint and the
    screenshot step.

  • The Chromium cache works on Windows and macOS runners. The cache step
    saved ~/.cache/ms-playwright, Playwright's browser directory on Linux
    only, so elsewhere every job downloaded Chromium again. The install, lint
    and screenshot steps set PLAYWRIGHT_BROWSERS_PATH to one directory under
    runner.temp, and that is what is cached (a Linux runner misses the cache
    once).

  • The Action's exit-code output describes exit 2 too: the run did not
    happen (bad usage, a bad config, a missing render runtime), and the count
    outputs are empty then.

Metadata

  • The snapshot is OpenUI5 1.153.0 (from 1.152.0): data/properties.json,
    data/icons.json and the @openui5/* pins of @abap2ui5/linter-render
    moved together (RELEASING.md step 1c). One control is new
    (sap.ui.core.tooltip.TooltipFocusGuard) and three enums
    (sap.m.InputDescriptionAlign, sap.ui.core.popover.PopoverPlacement,
    sap.ui.table.ShowScrollHandle); the icon font is unchanged (707 names).
    A member new in 1.153 was an unknown-* "typo?" finding and a render
    failure; it is now *-too-new against a lower minUi5, which a consumer
    can accept. A @abap2ui5/linter-render of 0.8.x serves 1.152 and is named
    on stderr as a version gap (runtimeSnapshotMismatch): install the 0.9.0
    runtime with this release.

  • The generator gives a class the methods it owns wherever the file
    writes them.
    sap/ui/unified/ColorPicker.js defines the private
    _ColorPickerBox between the picker's extend call and the picker's own
    methods, and a file was read "from one extend call to the next": the
    picker's fireChange({ …, colorString }) was harvested for the box, so
    $parameters>/colorString on a ColorPicker - what the demo kit sample
    reads - was an unknown-event-parameter. data/properties.json now
    carries colorString and formatHSL on the picker's change and
    liveChange; no other class moved.

  • data/compat.json says the mirrors are synced against abap2UI5 1.147.0
    (framework.mirrored, was 1.146.0 - the mirrors had been synced, the record
    had not; npm run generate-compat -- --local <abap2UI5 checkout> is the
    step that was skipped). app-template's check-pin and the VS Code extension
    read this field, so a 1.147.0 app stops reading as "newer than the linter
    knows". The weekly upstream-sync workflow now compares the field against
    upstream's package.json version and prints a warning line on lag - a
    warning, not a failure or an issue: a version bump upstream that touches
    none of the four mirrored files is not drift, and that workflow files issues
    for drift only.

Also in this release

  • ./check: the entry point without the renderer. checkAbapSource( )
    and checkXmlSource( ) move to lib/check.mjs, exported as
    @abap2ui5/linter/check together with what they decide with -
    declaresApp, VIEWLESS_APP_RULE, sizeLimitRaised,
    standDownUnusedNamespaces (the unused-namespace-declaration stand-down,
    a function now instead of a block inside checkAbapSource( )),
    frozenBuilderOf/FROZEN_BUILDERS, isXmlSource, isAbapGitXml,
    xmlFileKind. The main entry re-exports all of it unchanged; what it adds
    is checkFiles( ) with the worker pool and the renderer (http, os,
    module), which a browser bundle cannot resolve - so the VS Code
    extension's web build re-implemented each of those decisions and pinned
    every copy to this source with a test of its own. Both entry points take
    data (the snapshot itself, for a host without a path - from the new
    snapshotFromJson( ) on ./properties, which reads data/properties.json's
    text or object the way loadSnapshot( ) reads the file) and prep (a
    prepareAbap( ) of the same source the caller already holds;
    checkFiles( ) ignores it). npm test imports ./check in a fresh process
    with a resolve hook and fails on the renderer, the entry point, a
    worker_threads/http/os/module import or anything of the render
    runtime. The pool's threads load ./check too, not the renderer.

  • A view nested 5,000 levels deep is judged. Every walk over a tree
    recursed once per level, and the property walk ran out of stack at about
    a thousand: RangeError: Maximum call stack size exceeded out of the
    middle of a run - from the namespace scopes, the main walk, toXml( ),
    the portable walk and the namespace stand-down in turn. They go through
    lib/tree.mjs now (walkTree( ), nodesOf( )), which keeps the
    recursion's order without the recursion, so every finding comes out where
    it did. What the browser makes of such a document is the render gate's to
    report, as a render error: UI5 itself runs out of stack between 400 and
    1,000 levels, and the browser's XML parser refuses 5,000.

  • The first display is modelled. The model a view is judged and rendered
    against took its scalar seeds from the whole class, so a value only an
    event handler assigns was rendered as if it were there from the start.
    samples-stack app 013 bound a MessageStrip type to an attribute seeded
    Error in a CATCH block: both gates rendered Error, every real start
    shipped "" and terminated the app. A document built on the start path -
    main( ) ahead of its dispatch, the check_on_init( ) arm (or the
    check_on_navigated( ) arm of a chain without one), the constructor and
    what they call - is now judged and rendered against a model seeded from
    that path only (initModel, initialFields, docOnInit on
    prepareAbap( ), docModels on the result). A popup built in a handler,
    and every class with no such dispatch, keeps the class-wide model.
    Measured on six corpora (samples-controls, samples-stack, abap2UI5, popups,
    sapgui, abap-cloud-gui): no new finding, no new render error.

  • New rule enum-bound-to-initial-field (error), the root-field twin of
    enum-field-unset-on-insert: an enum-typed property bound to an attribute
    the start path gives no value - no literal seed, no declared VALUE, no
    other write. The first display ships "", validateProperty throws and the
    app terminates, hidden control or not. A field the start path writes
    non-literally, one bound under omit_initial, and a document the first
    display does not show are not judged.

  • New rule rows-hidden-by-visible (warning). The items of a
    non-growing list (sap.m.ListBase: List, Table, GridList, Tree) bound
    without a length and with a binding-valued visible on the row template,
    in a class that raises no size limit: a JSONModel hands such a binding at
    most 100 entries and the hidden rows are among them, so the list stops
    short of its data without a word. samples-controls' overview app listed a
    few dozen of its 622 ports that way. The narrow successor of the
    bound-aggregation-over-size-limit rule dropped in 0.3. Where the limit
    does not apply it is silent: a growing list pages with lengths of its own,
    a binding info's length is passed through, and a raw view has no class
    to ask. Neither is a MultiInput's tokens or a layout container's
    repeated items judged - a value set or a repeater, not rows of data; the
    first version reported the hidden tokens of abap2UI5/samples app 078, of
    popups' z2ui5_cl_popup_get_range_m and sample 19 and of abap2UI5's
    frozen copy, its only hits on seven corpora.

  • New rule invalid-css-value (error, fixable). A size, colour or
    percentage literal its UI5 type refuses - width="100", 100 px,
    calc(100%-2rem), fit-content, a capitalised colour name. UI5 checks
    sap.ui.core.CSSSize, AbsoluteCSSSize, CSSSizeShortHand, CSSColor
    and Percentage with one regex each; the XML parser only logs a refused
    value, then the control's setter throws and the whole view fails to load.
    The regexes are harvested into the snapshot (typePatterns, every
    string-based DataType whose isValid( ) is one regex test) and are the
    same from 1.71 to the pinned release. --fix repairs only the mechanical
    cases: surrounding blanks, a blank between number and unit, a colour name's
    case. A bare number keeps the finding without a suggestion.

  • New rule unknown-binding-type (error, fixable). A binding info's
    type: (by global name) or a core:require module path in
    sap.ui.model.type / sap.ui.model.odata.type that names no model type.
    By global name the binding runs without its type (the raw value is shown,
    input is never parsed back); through core:require the module 404s and the
    view is never created. sap.ui.model.type.Decimal and .Number are the
    plausible names that do not exist. The snapshot lists both namespaces
    (modelTypes); a type of the app's own, a relative .MyType and an alias
    are not judged (the alias at its core:require). A name that matches a type
    up to letter case is rewritten by --fix.

  • New rule binding-type-too-new (warning), the version half of the
    same check: a model type newer than the floor
    (sap.ui.model.odata.type.DateTimeWithTimezone, @SInCE 1.99), read from the
    class's own JSDoc into modelTypes.

  • The metadata snapshot gains two sections, both additive:
    typePatterns and modelTypes (loadSnapshot( ) attaches them as
    __typePatterns / __modelTypes). A snapshot without them - one a
    consumer generated with an older generator - leaves the three rules above
    silent. A consumer that builds the snapshot object itself rather than
    through loadSnapshot( ) (the VS Code extension's web host) has to attach
    the two the same way to get them.

  • portable-app: the vendored profile follows protocol revision 0.3
    (604d267) - the client-API names are read from actions.api, the names a
    raw view's .eF( ) may use from actions.wire.custom.

  • New opt-in rule portable-app (warning once asked for). Reports
    everything in an app outside the abap2UI5 protocol's portable profile v1
    (abap2UI5/protocol profiles/portable.md) - what keeps the app from
    running unchanged on a non-UI5 frontend such as the UI5 Web Components
    frontend: controls and members the profile does not list, z2ui5.cc
    custom controls, named models and other binding forms, expression
    constructs outside its grammar, event wires and arguments ($event,
    prevent_default_expr), frontend actions (CONTROL_BY_ID, …), the nested
    view slots (decision Q1) and view_display( switch_default_model_path ).
    One id, a reason per finding. Off until "rules": { "portable-app": "error" }. The profile is vendored verbatim as data/portable-v1.json
    (source commit in data/portable-v1.source.json, refreshed with
    npm run sync-portable-profile, drift reported by upstream-sync), and
    the rule reads both the current and the Q10 shape of its frontend actions.
    New export ./portable (checkPortable), new option portableProfile.

  • z2ui5_if_ui5_monitor is released API. abap2UI5 adds a roundtrip
    monitor seam to its released package src/02 (an addon such as
    abap2UI5-addons/admin-cockpit implements it to log usage, timing and
    errors). Unlisted, non-released-api would have reported every
    implementing class, since the name falls under the internal
    z2ui5_if_ui5_* prefix. Merge together with the abap2UI5 change, or
    check-upstream reports the extra name as drift until it lands.

  • ExcelBridge is mirrored. abap2UI5 ships a new companion control,
    z2ui5.cc.ExcelBridge (the workbook bridge of an Excel add-in,
    abap2UI5/office-addin). Unmirrored, a view naming it would fail view
    CREATION in the render gate with a module 404 and the property walk would
    look away from it; check-upstream reports the drift until both land.

  • NativeBridgeScan is mirrored. abap2UI5 ships a new companion
    control, z2ui5.cc.NativeBridgeScan (the scan button of the native mobile
    shell, abap2UI5/mobile-shell). Unmirrored, a view naming it failed view
    CREATION in the render gate with a module 404 and the property walk looked
    away from it; check-upstream reported the drift.

  • The property gate is more than twice as fast on a corpus. The
    samples-controls corpus (642 classes, --no-render) took 15.2 s and takes
    6.6 s, with byte-identical results on it, on abap2UI5/samples and on
    abap2UI5. The source readers no longer rebuild strings one character at a
    time, a call's argument list is parsed once instead of a dozen times, two
    whole-class scans per attribute or per match are gone, and
    checkFiles( { jobs } ) spreads the files over worker threads (one per 64
    files) and returns the results in file order. The CLI's new --jobs <n>
    defaults to the machine's cores, at most 4; --jobs 1 keeps one thread.
    The library default is 1.

  • New rule unknown-message-box-type (warning, fixable).
    message_box_display( type = … ) takes the sap.m.MessageBox display
    methods - information, warning, error, success, confirm, alert,
    show - and the server silently falls back to a plain show( ) box for
    anything else: an error written type = \E`opens with no icon and no title. The four message-type letters with a box of their own (E, W, S, I) are rewritten by--fix. The list is mirrored from ct_box_typeand gated bycheck-upstream`.

  • New rule bind-path-as-value (error, fixable). _bind_path( x ) and
    _bind( val = x path = abap_true ) return the bare path for a binding the
    app assembles itself; as the whole v = of an attribute they make the
    attribute the TEXT /X. --fix turns the call into _bind( x ).

  • New rule html-content-not-markup (error, fixable). A
    sap.ui.core.HTML content that does not open with a tag is read by
    jQuery as a selector: nothing renders, or rendering throws. --fix wraps
    plain text in a <span>.

  • New rule navigation-lost-on-rebuild (hint), the navigation twin of
    control-state-lost-on-rebuild: a NavContainer, SplitApp or
    FlexibleColumnLayout moved with to/backToPage/toDetail/toMaster
    only from a handler, while a bound field keeps the page - after the next
    view_display( ) the container is back on its initial page and the field
    names another. Silent when a display-path method navigates the same
    container (abap2UI5 backlog item navcontainer-position-not-reissued).

  • raw-javascript-to-frontend sees two more shapes: an inline on…=
    handler in markup (core:HTML content) and a javascript: URL in a
    URI-typed property. The default CSP refuses both, so neither ever ran.

  • Three rules gained a --fix. abapdoc-html-tag escapes the tag's
    brackets as &lt;/&gt;; control-call-arg-kind rewrites a bool
    argument spelled abap_true, x, TRUE, abap_false or FALSE to the
    spelling the frontend reads; duplicate-property deletes a second
    attribute write that repeats the first word for word (two different values
    keep the finding).

  • frontend-action-as-backend-event reads an inherited cs_event as the
    class's own.
    A class declaring its backend events as CONSTANTS: BEGIN OF cs_event was already exempt, but a SUBCLASS reaching the same
    constant was not: abap2UI5-addons/rap-ext's worklist, INHERITING FROM z2ui5_cl_rap_list_report, was warned on client->_event( cs_event-back )
    and even on client->_event( z2ui5_cl_rap_list_report=>cs_event-back ),
    failed the gate, and had to copy the constants into a local variable. A
    class-qualified <class>=>cs_event-… is now that class's constant, the
    client's only for z2ui5_if_client; and the superclass chain is resolved
    through the other files of the run (checkFiles builds the index,
    classIndexOf( ) from ./abap-rules for a caller judging one source at a
    time, passed as classIndex). A superclass the run does not have might
    declare one, so the bare spelling in such a class is not reported; a
    chain known to the end, or INHERITING FROM object, is judged as before.

  • A binding assembled in place is no longer "fixed" into text.
    unescaped-text-in-attribute read a name's origin from its =
    assignments only, so a binding info built with
    CONCATENATE `{path:'` lv_path `'}` INTO lv_bind (around a
    client->_bind( … path = … ) result) looked like a name nothing writes -
    data by definition - and --fix moved a( v = lv_bind ) onto t, whose
    escape_literal( ) turned the Tree's items binding into text: a silent
    runtime break made by the fixer (oblomov-dev/cloudy-sapgui). The rule now
    also reads CONCATENATE … INTO x, x &&= …, REPLACE … IN x WITH … and
    me->x = … / ls_row-f = … as writes; in a statement that assembles a
    string (those, or an operand of &&) a literal holding a brace counts as
    binding vocabulary, and a name the write reads carries its own writes'
    vocabulary along (lv_bind = lv_path && …). A brace literal assigned
    whole (label = `{/X}` ) is still data.

  • missing-on-navigated-branch --fix no longer swallows a popup's
    result.
    A called app that leaves with an event (nav_app_leave( event = … ), how the confirm and select popups hand back their answer) raises
    check_on_navigated( ) AND that event on the same roundtrip. The fix wrote
    its ELSEIF client->check_on_navigated( ). right behind the init branch,
    ahead of the event arms, so the fixed class took the navigated arm and
    never saw POPUP_TRUE (found migrating abap2UI5-addons/popups onto 0.8.5).
    In a class that calls other apps the arm is now written LAST in the chain,
    where it only catches roundtrips the chain ignored before; a chain ending
    in ELSE there is reported without a fix. A class that calls no app keeps
    the arm right behind init, as before.

  • Neither navigated fix copies a display of a branch-local variable.
    client->view_display( view->stringify( ) ) next to DATA(view) = … in the
    init branch was copied into the new arm, where it compiles (an inline
    declaration is method-wide) and dereferences an initial reference on the
    first hop back. Such a statement is now reported without a fix, in
    missing-on-navigated-branch and missing-view-display-on-navigated alike.

  • missing-view-display-on-navigated reads a negated guard as a guard.
    IF client->check_on_navigated( ) = abap_false. RETURN. ENDIF. was judged
    as a navigated branch that never re-displays - its branch is the roundtrip
    that did NOT navigate, and the re-display stood right below it. A negated
    condition whose branch leaves the method is now judged on the rest of the
    method (reported without a fix when nothing there displays); one that does
    not leave is not a navigated branch.

  • @abap2ui5/linter-render asks for playwright ^1.64.0. The workspace
    manifest still said ^1.61.1 while @abap2ui5/mcp-server, samples-controls
    and the playground had moved on, so a project installing the render
    runtime next to one of them resolved TWO Playwright versions - two
    packages in the tree and two Chromium downloads for one browser. The range
    now follows the sibling repositories (^1.64.0, as @abap2ui5/mcp-server
    asks), and one install shares the one Playwright. The @openui5 pins moved
    with the metadata snapshot (see Metadata), not as a dependency.

  • The package size the docs quote is measured, not remembered. README,
    AGENTS.md, RELEASING.md and release.yml said ~340 kB (release.yml ~240 kB);
    npm pack --dry-run produces a ~600 kB tarball (44 files, 2.2 MB unpacked -
    data/properties.json grew with every snapshot and nothing counted). All
    four say ~600 kB, and a new gate (test/review/round-2026-10-12-hygiene.mjs)
    reads the README figure, requires the other three to agree with it, and
    fails when it is more than 25% off the tarball npm pack produces.

  • Workflows: downstream.yml no longer cancels an in-progress run on
    main
    - the same cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
    as ci.yml, so main's runs stay the record of what landed while a
    superseded branch push still frees its runner. The package job's
    npm audit --omit=dev comment now says what it audits: the production set
    of BOTH published packages (the linter has none; the linter-render
    workspace's @openui5, playwright and less-openui5 are in the root lockfile),
    not "the linter minus an optional peer" - --workspaces=false was measured
    and does not narrow npm audit, so the flag was not added.

  • Docs: the README's "Working on this repository" block no longer runs
    node cli.mjs src
    (a directory this checkout does not have - it answered
    no such file or directory); it lints test/fixtures --no-render and shows
    the sibling-app form. CONTRIBUTING.md names npm run check as what a pull
    request must pass (npm test plus the github-app dry run) and lists
    generate-compat with the other gated generators; test/review/README.md
    describes what the directory holds today (review rounds, audits, corpus
    sweeps, topic files) instead of "the 2026-09 review round".