agentleFS
abap2UI5abap2UI5/app-template/.claude/skills/abap-check/SKILL.md

abap-check

Skill1 starsChanged 12 days agoMITView on GitHub

The catalogue of ABAP problems a green CI does not catch - abapGit round-trip and import failures (BOM, line endings, trailing whitespace, EOF newline, 255-character lines, metadata sidecars for CLAS and for DDLS/BDEF/TABL), activation errors abaplint does not model (class_constructor visibility, LOCAL FRIENDS, generic types on older releases, reserved table field names, RAP and CDS), extended-check (SLIN/ATC) traps, downport and transpiler traps, and runtime breakage that only shows on a real system. Use before finishing any change under src/, after editing a .clas.xml or any other metadata sidecar, when a pull into a system produced unexpected diffs, an import error or an activation error - and add the case here whenever a new one is found.

The file

We split this skill into the rules it gives an agent

10 rules. About 0 are common, 5 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    This is a growing catalogue: Every entry is a defect that actually reached main and was found by hand afterwards — several of them by a …

    line 24
  2. Distinctive:

    Run first, in this order:

    line 43
  3. Very specific:

    The transpiler ignores visibility: Every ABAP member becomes a plain JS property, so npm run unit stays green on a class pool a real system …

    line 68
  4. Distinctive:

    Every "abaplint does not catch X" claim in this file was measured, and the measurement has one trap that makes every result meaningless:

    line 78
  5. Distinctive:

    No script decides this one.

    line 195
  6. Very specific:

    never inside a parameter list: Document parameters with "!

    line 716
  7. Distinctive:

    When a repository lists its rules explicitly, a new upstream rule does not reach it by moving the pin.

    line 754
  8. Very specific:

    No redundant conversions: Do not wrap a value in CONV string( … ) when the source already has the target type — and not in CONV i( … ) when …

    line 905
  9. Very specific:

    Not verified from here: abap2UI5-local was not attached to the session that wrote this, so the bundler itself was not read.

    line 1079
  10. Very specific:

    Every framework file is downported to 7.02 (npm run auto_downport) and transpiled to JS (npm run auto_transpile), and is linted against …

    line 1089

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- GENERATED by scripts/generate-skills.mjs in abap2UI5/app-template from .claude/skills/abap-check/SKILL.md in abap2UI5/abap2UI5 - "this repository" in the text is the framework's. Do not edit; regenerate. -->

What a green CI does not prove

Scope. This skill is about ABAP that has to survive a real SAP system — it applies to framework code and to app code alike, and most of it applies to any repository shipped through abapGit. The other skill, build-an-app, is about writing an app with abap2UI5: the app class, the lifecycle, the view builder, binding, events. They compose — build with build-an-app, check with this one before you finish. The commands quoted below are the framework repository's (abap2UI5/abap2UI5); the rules behind them are not.

npm run verify proves the code parses, lints and runs under the transpiler. Everything in this file is a way for that to be true while a real SAP system still refuses the change — because the files come back different, because the object does not even import, because the class pool does not activate, because the extended check fires, or because the runtime behaves differently there than under the transpiler.

This is a growing catalogue. Every entry is a defect that actually reached main and was found by hand afterwards — several of them by a user, after a pull. When you hit a new one, add it (see the last section) — the point is that nobody has to discover it twice.

Every numbered section below carries a Gate: line, the same idea as the Linter: lines in ui5-check — but the answer here is rarely "a linter rule". Most of this is decided by a script in this repository, which means a consumer running nothing but npx @abap2ui5/linter is not covered by it. The line says which of the three it is, because that is the difference between a defect that cannot reach main and one that cannot reach main here:

linteran abap2ui5lint rule — travels to every repository that runs it
this repoa script under .github/scripts/ — covers this tree only
abaplinta rule in abaplint.jsonc — covers repositories that enable it
opennothing decides it; the entry is a thing you have to know

Run first, in this order:

npm run check:abap            # abaplint (fast inner loop)
npm run check:abap2ui5:fast   # the abap2UI5-linter without the render gate
npm run doctor                # the sidecars (BOM, LF, CLSNAME), the pins, the browser
npm run check:all             # before any PR: the pin, abaplint, the linter with its render gate

Why the tooling here is blind to most of this

Four separate holes, worth knowing precisely:

  • abaplint parses ABAP, it does not model abapGit's file format. The sidecar is only read for the few facts its rules name. Verified against abaplint 2.120.24 with 73 rules active and check_syntax live: a .clas.xml with the BOM stripped, the terminating newline removed, CRLF line endings throughout and a raw apostrophe in <DESCRIPT> — four defects at once — produces zero findings. One of the four has since been closed upstream: xml_bom (2.120.32) flags a sidecar without the BOM and quick-fixes it in. The other three still produce nothing — re-measured on 2.120.33 with the three remaining defects in one sidecar, using xml_bom itself as the second control probe.
  • abaplint's global.files is not all of src/. It covers src/00 (with noIssues), src/01 and src/02 — src/99 is never read at all.
  • The transpiler ignores visibility. Every ABAP member becomes a plain JS property, so npm run unit stays green on a class pool a real system rejects with a syntax error.
  • Every check here runs against one release. abaplint's default target is v750 and the transpiler is a JS runtime. Neither is 7.02, neither is a 7.40 or low-SP 7.50 system, and neither is ABAP Cloud — and a good half of this catalogue is code that is valid on one of those and rejected by another.

Measuring one of these against abaplint — read this first

Every "abaplint does not catch X" claim in this file was measured, and the measurement has one trap that makes every result meaningless:

An abaplint config with no rules block runs NO rules. Not the defaults — none. A run over a deliberately broken file then prints 0 issue(s) found, 3 file(s) analyzed, which looks exactly like a confirmed gap and is nothing of the kind.

Three of four claims measured this way in one sitting came back "gap", and one of them was wrong: local_testclass_consistency covers <WITH_UNIT_TESTS> in both directions and says so in plain words. It had simply not been switched on.

So: enable the rules explicitly, and run a control probe first. Break something abaplint certainly catches — an undefined variable is the cheapest — and only trust a zero once you have seen a one:

{ "global": { "files": "/src/**/*.*" },
  "syntax": { "version": "v750", "errorNamespace": "^(Z|Y)" },
  "rules": { "check_syntax": true, /* …and the rule you are testing… */ } }

Two more things that silently void a result: an errorNamespace that does not match your test class's name, and a syntax.version too old for the syntax you wrote (DATA(x) = … on v702 is a parser error that masks everything after it).

Verified gaps as of abaplint 2.120.24 — measured this way, control probe passed, nothing abap2UI5-specific about any of them, and therefore candidates to push upstream rather than to reimplement here. The BOM half of the third row is the one that was pushed upstream: it is xml_bom since 2.120.32, and the row records what is left.

CaseWhy it matters
CLASS-METHODS class_constructor. in a PRIVATE SECTIONABAP requires the static constructor in the public section; the class pool does not activate. constructor_visibility_public sees only the instance constructor
a test class calling a PRIVATE member without CLASS <global> DEFINITION LOCAL FRIENDS <ltcl>.same activation failure; it has reached users twice (cadfb7ae, #2146)
the .clas.xml byte format — line endings, terminating newline, &apos; (the BOM is xml_bom since 2.120.32, and on here)abapGit re-serializes it differently on every pull, for everyone
meth( )->* under syntax.version v750dereferencing a call result is not accepted at 7.50; the system refuses the class — reached a user via a test class of z2ui5_cl_ui5_srv_model (#2722, section 2)
INTO CORRESPONDING FIELDS OF TABLE @DATA(…) under syntax.version v7507.55 syntax; every older system refuses the class — reached a user via abap2UI5/samples app 348 (section 2)
a VALUE header default plus a per-row assignment of the same component"The component … was specified more than once" — the system refuses the class; reached a user via abap2UI5/samples-controls app 241 (section 2)
a closing string literal followed straight by a name character — `x`y (parser_error since 2.120.52, and on here)"There must be a space or equivalent character … after …" — the statement does not parse, so the class does not activate; reached a user via abap2UI5/samples-controls apps 136 and 588 (section 2)

1. abapGit round trip — the file must come back byte-identical

Gated by npm run check:abapgit (.github/scripts/abapgit-format-gate.mjs), over all of src/.

Gate: this repo, for the whole family — BOM, EOF newline, line endings, tabs, &apos;, file names, sidecar pairing, <CLSNAME>, <LANGU>. Three of them reach further: the 255-character line is linter — source-line-too-long (an error on every app class it checks), <WITH_UNIT_TESTS> plus <CLSNAME> are abaplint — local_testclass_consistency, xml_consistency (measured, both directions), and the BOM on an object's sidecar is abaplint — xml_bom, new in 2.120.32 and on here in abaplint.jsonc, in the three target configs and in the autofix config, where its quick fix inserts the BOM rather than reporting it. Trailing whitespace is abaplint — whitespace_end, per repository and not everywhere; see below. Everything else in the table is this repository's script and nothing else — and a consumer repository that has not turned xml_bom on can still ship a BOM-less sidecar with a green CI. The EOF newline is abaplint — final_newline too (abaplint/abaplint#4412), on in abaplint.jsonc since the 2.120.75 pin, next to check:abapgit.

abapGit writes every file one specific way. Write it another way and it differs from what the system serializes back — permanently, on every pull, for everyone.

RuleWhat abapGit doesWhere it bit us
bom.xml starts with the UTF-8 BOM EF BB BF; .abap never does8e272492, 54bce5b6 — both titled "fix abapgit diffs", both a .clas.xml written without the BOM. Again in abap2UI5/samples bc1f3d2 across 15 sidecars at once. The first half is abaplint — xml_bom now (one XML per object, getXMLFile( ), with a quick fix); the second half — a .abap file that carries a BOM — has no rule anywhere and stays this script's
eofexactly one terminating \n, no blank line after itc7185c38 — z2ui5_if_action.intf.xml ended with \ No newline at end of file
crlfLF only, everywhere.gitattributes used to normalize this and was deleted in b62ea07; the gate is what enforces it now
tabno tabs — the ABAP editor expands them, so the pulled source is not this file—
aposXML goes through the ABAP iXML renderer, which escapes the apostrophe as &apos;abap2UI5/samples 702813e "fix abapgit" — nine .clas.xml carried a raw ' in <DESCRIPT> and came back changed on the next pull
filenamelower case; abapGit derives the object name from it—
sidecarevery *.clas.abap / *.intf.abap has its .xml, and every .xml has its source. Metadata without source creates an empty object; source without metadata is not imported at all. The class-pool includes (.testclasses, .locals_def, .locals_imp, .macros) have no sidecar of their own—
packageevery folder under src/ has a package.devc.xml — it is the package—
clsname<CLSNAME> equals the file name, upper-cased—
langu<LANGU> equals <MASTER_LANGUAGE> from .abapgit.xml (E). An object created while logged on in another language serializes with that language and diffs against every other developer—
unit-tests<WITH_UNIT_TESTS>X</WITH_UNIT_TESTS> exactly when a .testclasses.abap exists. Also owned by abaplint's local_testclass_consistency, in both directions — so in a repository that enables that rule this one is belt and braces, not the only gated5eaaa79 "fix unit test metadata" — z2ui5_cl_util_range had the test include and not the flag

The size limits — where the import fails, not the diff

Both of these are ABAP limits, both were hit by generated code, and both fail in the same nasty way: abapGit reports the error for that object and carries on, so what stays behind in the system is an empty class stub. The tree looks imported; the app is gone.

  • 255 characters per source line. Longer and the import dies with "Literals across more than one line are not allowed". abap2UI5/samples#669 (8c8a04e): a merged reorganization carried such lines, and it left z2ui5_cl_demo_app_038 and _378 as empty stubs and reverted a catalog class to an older state. Split long literals into && chunks. Gated by check:abapgit (linelen); abaplint's line_length rule is off in this repository and would not cover src/00 and src/99 anyway. For scale: the longest line in src/ today is 253.

    Also gated outside this repository now, which is what the samples case argues for: the abap2UI5 linter reports it as source-line-too-long (an error) on every app class it checks, so a consumer repo that runs nothing but npx @abap2ui5/linter is covered too. Note what that does not replace — the linter reads app classes and views, not .clas.xml sidecars or src/00 and src/99, so check:abapgit remains this repository's gate for the rest of the round-trip family.

  • Maximum statement length. A single result = VALUE #( … ) with a few hundred rows exceeds it. Gate: none, and a character count is not one — written against the linter on 2026-08-30, measured, and deleted. A view builder CHAIN is one statement by construction, so on the samples-controls corpus the median over-limit statement was 23,000 characters and the longest 253,000, across 156 ports that all import fine. Length does not discriminate; do not re-propose the rule without a threshold somebody has measured. abap2UI5/samples-controls#38 (ee28671): the 246-row catalog constructor of the overview app could not be imported at all. Split it — the second statement appends with VALUE #( BASE result ). No script decides this one; it is a character count over a whole statement and the threshold is not documented as a fixed number.

    The same PR carries the second lesson: the file was generated, the split was first made by hand, and the next npm run overview would have restored the over-length statement. Fix the generator, then regenerate — see section 6.

Object types other than CLAS and INTF

The metadata node name is the serializer's, and it is not guessable. Each abapGit serializer reads exactly one node, named after the internal structure it fills. Write a plausible-looking node instead and it is silently ignored — the object arrives with empty metadata and the import fails with a message that usually points somewhere else:

  • abap2UI5/samples-stack 92cb8a4 — the .ddls.xml files carried <DD02BQ><STRUCOBJN>; the DDLS serializer reads <DDLS> with DDLNAME, DDLANGUAGE, DDTEXT and SOURCE_TYPE (W for a CDS view entity). With the wrong node the name and description stayed empty and the import failed with "Entity name and name of DDL source are not identical" and "Object description is missing".
  • abap2UI5/samples-stack 0a0aba6 — the .bdef.xml files carried <MAIN>; the BDEF serializer reads <BDEF> (the METADATA substructure of CL_BLUE_SOURCE_OBJECT_DATA=>TY_OBJECT_DATA). Same silent-ignore, and because the behavior definitions share their names with the CDS entities the error looked like a DDLS problem. The follow-up "Error updating where-used list for CLAS …" was downstream: classes using EML cannot compile while the BDEF is missing.

Rule: never hand-author a metadata sidecar for an object type you have not exported from a real system. Create the object once, let abapGit serialize it, and commit that. This is the same rule as "never hand-edit a .clas.xml" below, one step earlier.

The same rule, one field at a time: a TABL field's INTTYPE. abap2UI5-addons/admin-cockpit wrote its table sidecars by hand and gave every INT4 field <INTTYPE>I</INTTYPE>. The tables activated, but a user's pull (2026-10-06) listed every table with an INT4 field as changed: the system serializes INT4 as <INTTYPE>X</INTTYPE>. That was 27 fields in 4 tables, fixed in abap2UI5-addons/admin-cockpit#6. Real exports agree: abap-cloud-gui/tools/report2ui5/test/ddic/spfli.tabl.xml has X as well. The pairs confirmed so far are CHAR → C (with INTLEN = 2 × LENG), INT4 → X, DEC → P, STRG → g. Copy a field from an exported table of the same type rather than spelling it out. Gate: abaplint — xml_consistency, from 2.120.73 on (pinned here since 2026-10-10): abaplint/abaplint#4390 (merged 2026-10-09) makes it compare INTTYPE with DATATYPE for the confirmed pairs. Measured with that change, it finds exactly the 27 fields on the pre-fix cockpit and nothing in 114 exported tables.

Two round-trip rules no gate can decide

  • Never hand-edit a .clas.xml / .intf.xml to tidy it. It is a serialization, not a config file. Element order, indentation and which optional elements are present all come from the serializer; "cleaning it up" creates exactly the diff this section exists to prevent. If a sidecar looks wrong, fix the object in a system and commit what abapGit writes.
  • <DESCRIPTIONS> follows the components. A class whose methods and attributes carry short texts in the system serializes them as <SEOCOMPOTX> entries. b088c8a ("fix xml") had to add 2 272 lines of them to z2ui5_cl_xml_view.clas.xml in one go, because the block had been missing while the class kept growing. Add a component to a class whose sidecar has a DESCRIPTIONS block and the block is now incomplete; delete one and its entry is stale. The gate cannot know which components have a description in the system — you have to look.

Trailing whitespace — owned by a rule that is not in every repository

The ABAP editor strips a trailing blank when it saves, so a line that ends in one comes back different on the next pull — the same defect as the rest of this section, and the one most likely to be written here, because reflowing a chain or a string template leaves blanks behind that no diff view shows.

It is not in the abapGit gate: in this repository abaplint's whitespace_end already owns it for src/01 and src/02, and the historical blanks in mirrored src/00 and frozen src/99 are not ours to fix.

That rule is per repository, and it is not everywhere. abap2UI5/samples had no whitespace_end in its abaplint.jsonc at all: the port of the corpus to z2ui5_cl_ui5_view_builder (#752) left a trailing blank on seven lines in seven app classes, all four CI workflows stayed green, and a maintainer had to find and push them by hand — b7edbc6 "fix abaplint diffs". The rule is enabled there now, so that specific hole is closed.

So, before you finish in any repository: if its abaplint config does not list whitespace_end, the check is yours.

grep -rnP '[ \t]+$' --include='*.abap' --include='*.xml' src/ && echo "trailing whitespace"

(grep -E '[ \t]+$' does not work — in a POSIX bracket expression \t is a backslash and a t, so it matches every line ending in t, which is why a scan can come back full of hits and still have found nothing. Use -P.) Strip what it finds with sed -i 's/[[:space:]]\+$//' — that is all npm run strip_trailing_ws does — and add whitespace_end to that repository's abaplint config in the same pull request once the tree is clean. That is what turns one fix into the last one.

2. Activation — compiles here, syntax error there

Gate: this repo, and it is the section with the widest hole. The private class_constructor is check:abapgit — and, since 2026-08-30, abap2ui5lint's class-constructor-visibility as well, which is what carries it to a consumer whose only gate is npx @abap2ui5/linter. LOCAL FRIENDS is npm run check_visibility and stays here: it needs the test class and the main class read together, which the linter does not do. Neither is an abaplint rule — measured against 2.120.24 with 73 rules on and a control probe (see above), both produce zero findings, which is why they are on the upstream shortlist rather than reimplemented there. Generic types on older releases are abaplint — downport, fully_type_itabs, cloud_types where a repository enables them; the app-template core does, the sample repositories do not yet.

Measured again 2026-09-19 on abaplint 2.120.52 (check_syntax on, an isolated class per shape): both class-pool traps are upstream findings now — a CLASS-METHODS class_constructor. in a PRIVATE SECTION is "CLASS_CONSTRUCTOR must be declared in the public section", and a test class calling a PROTECTED method of the global class without LOCAL FRIENDS is "Method … is protected and cannot be accessed". So the upstream shortlist entry above is closed, and the two repository scripts are belt and braces for whoever pins an older abaplint. What is still nobody's upstream is in the **Backlog:** lines below — each names an item under backlog/items/, written to be filed against abaplint, since a rule about ABAP as a language belongs there and not in the abap2UI5 linter (which keeps the abap2UI5-specific checks).

The class pool

TrapRule
class_constructor must be in the PUBLIC SECTIONABAP requires it; the class pool does not activate otherwise. abaplint's constructor_visibility_public only looks at the instance constructor — verified: a CLASS-METHODS class_constructor. in a PRIVATE SECTION produces no finding. This is why z2ui5_cl_ui5_frontend fills ct_box_type lazily in box_resolve( ) instead of in a static constructor (#2547) — see the comment on the attribute. Gated by check:abapgit, and by abap2ui5lint's class-constructor-visibility
CREATE DATA … TYPE HANDLE takes a data object, not a method callCREATE DATA lr TYPE HANDLE cl_abap_structdescr=>create( lt_comp ). is "No method can be specified in the current position" on a system - the operand has to be a variable holding the descriptor. abaplint parses the call as an expression and the transpiler runs it, so a test class shipped this way through every gate and a user's system reported it (2026-09-02, ltcl_app_shapes in z2ui5_cl_ui5_srv_model). Gated by check:atc (handle_call): a TYPE HANDLE operand with a ( in it. Upstream from abaplint 2.120.65 on (abaplint/abaplint#4356): the operand is a plain data object in the CREATE DATA grammar, so a call there is a parser_error
->* needs a data reference variable in front of itresult = row_ref( iv_name )->*. is a syntax error on 7.50 - the dereferencing operator takes a reference variable, not the result of a functional method call or a constructor expression; hoist the reference into its own variable and dereference that. abaplint parses the chain at syntax.version v750 and the transpiler runs it, so the line was green through every gate here and a user on SAP_ABA 750 SP33 reported it (#2722, ltcl_00_base~row in z2ui5_cl_ui5_srv_model - the same test class handle_call came from). Gated by check:atc (deref_call): a ) directly in front of a ->*. Measured on a 7.58 system (2026-10-03): get_ref( )->* and CAST ty( … )->* are fine there, NEW ty( … )->* is a syntax error even there. Upstream from abaplint 2.120.66 on (abaplint/abaplint#4361): a ->* directly after a method call is a parser_error below v756, and from 2.120.68 on (abaplint/abaplint#4371) check_syntax reports NEW ty( … )->* in every release
setup( ) / teardown( ) cannot be called directlyThe special method "TEARDOWN" cannot be called directly. Two test classes in abap2UI5-addons/abap-cloud-gui had setup( ) call teardown( ) to clear their table rows first. A 7.58 SYNTAX_CHECK refused both (2026-10-04, fixed in abap2UI5-addons/abap-cloud-gui#9). The same holds for class_setup / class_teardown. Put the shared code into a method of its own and call that from both. Upstream from abaplint 2.120.68 on (abaplint/abaplint#4372): check_syntax reports the call in a FOR TESTING class, with or without me->, through the class name and as CALL METHOD
super-> calls only the method it stands inSUPER-> can only be used to call the previous implementation of the same method. z2ui5_cl_rap_worklist->load_data called super->get_where_clause( ) to get the WHERE without its own redefinition of that method (2026-10-04, 7.58, fixed in abap2UI5-addons/rap-ext#19). Give the superclass a second, non-redefined method that returns the base value, and call that instead. Upstream from abaplint 2.120.68 on (abaplint/abaplint#4368): check_syntax reports it, also in a method that is no redefinition, while an alias of the same method stays allowed
A test class touching PRIVATE/PROTECTED members needs CLASS <global> DEFINITION LOCAL FRIENDS <ltcl>.Same failure mode, and it reaches users: ltcl_rtti got to main without it and had to be repaired (cadfb7ae), and #2146 is a user reporting a shipped test class that calls the PROTECTED request_json_to_abap. The transpiler makes every member a plain JS property, so npm run unit is green on a class pool the system rejects. Gated by npm run check_visibility
A PRIVATE/PROTECTED member is out of reach for every OTHER classField "MV_SESSION_STICKY" is unknown — four times on a user's system (2026-09-23): the attribute sat in the PRIVATE SECTION of z2ui5_cl_ui5_handler while z2ui5_cl_ui5_http_handler wrote it, z2ui5_cl_ui5_action read it and the action's test class set it. abaplint's check_syntax did not check attribute visibility then and the transpiler makes every member a JS property, so npm run check, npm run unit and every gate were green; check_visibility only compares a test class with its OWN class under test. Make the member PUBLIC (READ-ONLY where only the owner writes it) — LOCAL FRIENDS is no fix here, it only reaches local classes of the owner's own pool. Gated by npm run check:members: ref->member resolved through a method-local declaration, a parameter or an attribute, and class=>member; friends, subclasses and friend interfaces are legal. Upstream from abaplint 2.120.67 on (abaplint/abaplint#4362): check_syntax reports a PRIVATE/PROTECTED attribute or constant of another class as well
A local type of the class pool types only the PRIVATE SECTIONThe types, classes and interfaces of <class>.clas.locals_def.abap (CCDEF) are read before the global class, but only its PRIVATE SECTION may use them - a PUBLIC or PROTECTED attribute, parameter or type built on one is a syntax error on the system. abaplint 2.120.75 resolves the type and reports nothing (measured 2026-10-24, v750 and ABAP Cloud), and the transpiled unit run is green. abap-cloud-gui's report2cloud met it with the globals of a report typed with its local classes - it makes them private attributes (planLocalTypes( )). Move the type into the public section of the global class, or the component into its PRIVATE SECTION

Backlog: abaplint · abaplint-local-type-outside-private

Generic types on older releases — the recurring one

This class of defect has now bitten three times, twice reported by users after a pull, and it is the single most likely thing to break a system that is not on the newest release. abaplint's default target accepts all of it; at syntax.version v750 or v702, check_syntax reports the first shape (below v756), and from 2.120.72 on (abaplint/abaplint#4403, pinned here) the other two as well.

  • A generic REF TO data cannot be dereferenced inline. lr_ref->* in an expression, and ASSIGN COMPONENT … OF STRUCTURE mr_data->*, both fail with "A generic reference cannot be dereferenced (->) in the current statement". Reported on SAP_ABA 750 SP22 (#1922) and before that on another low release (#1856) — fixed in a16e2465 (#1923) and 75bf5515 (#1857). Assign it to a field symbol first, then work on the field symbol:
    ASSIGN lr_ref->* TO FIELD-SYMBOL(<val>).
    ASSIGN COMPONENT lv_name OF STRUCTURE <val> TO FIELD-SYMBOL(<comp>).
    abaplint reported this only as a source. From 2.120.67 on (abaplint/abaplint#4364) check_syntax also reports it as a target below v756: CLEAR lr->*, IMPORTING ev = lr->*, lr->* = ….
  • The dynamic component selector needs a reference variable, not TYPE any. ASSIGN val->(lv_name) TO … where val is TYPE any is rejected on NW 7.52 and 7.02 with "VAL is not a reference variable" (#2409). Cast once — DATA(lx) = CAST cx_root( val ). — and address lx->(lv_name). check_syntax reports it below v756 from 2.120.72 on.
  • CORRESPONDING #( <generic> ) is not available. Same commit (a16e2465): use CLEAR + MOVE-CORRESPONDING. check_syntax reports a generic target below v756 from 2.120.72 on. abaplint's prefer_corresponding rule had to be switched off for the low-release config because it recommends the construct that does not compile there.

VALUE constructor — a header default plus a per-row value is a syntax error

  • A component assigned before the first line spec cannot be assigned again inside a line. VALUE #( selectable = abap_true ( … ) ( … selectable = abap_false ) ) is refused by the system's syntax check with "The component "SELECTABLE" was specified more than once." — the header assignment is a default for ALL following lines, not an overridable one. Found by a user running Code Inspector variant SYNTAX_CHECK over a pulled abap2UI5/samples-controls main (2026-08-17): z2ui5_cl_smpc_app_241's model_init carried it. Write the value per row instead — or close the header's scope with a second group, since the default only binds to the lines after it. abaplint 2.120.24 accepts the construct without a finding (check_syntax on, control probe fired) — its VALUE grammar does not model the one-assignment rule. Gate: abap2ui5lint — value-header-default-reassigned (2026-08-30), which follows the source-line-too-long precedent: for a consumer whose only gate is npx @abap2ui5/linter, a class that does not activate is the most severe thing this tool can find. Measured again 2026-09-19 on abaplint 2.120.52: check_syntax reports the row's second assignment as "Duplicate field assignment", so on that pin the construct no longer reaches main through abaplint either, and the linter rule is a duplicate for such repositories.

A literal that ends where the next token begins

  • A closing string literal needs a separator after it. arg = `${$parameters>/expanded}`s_ctrl = VALUE #( … ) is refused by the system's syntax check with "There must be a space or equivalent character (":", ",", ".") after ${$parameters>/expanded}", and — because the statement never parses — a second error on the same method, "The statement "ENDMETHOD" is missing." Found by a user running Code Inspector variant SYNTAX_CHECK over a pulled abap2UI5/samples-controls main (2026-09-13), same route as the VALUE header case above: two _event( ) wires, z2ui5_cl_smpc_app_136's toggle and z2ui5_cl_smpc_app_588's beforeNavigate, had lost the blank between the arg literal and s_ctrl. abaplint 2.120.38 accepts it — control probe fired, config in the measurement recipe above: its lexer ends a literal at the closing delimiter and starts the next token right there, where the kernel wants a separator. The same hole covered 'x', `x` and |x| alike. Nothing here was abap2UI5-specific, so it went upstream rather than being reimplemented: abaplint/abaplint#4289 (2026-09-14), closed in the lexer by #4292. Gate: parser_error since 2.120.52 — measured on that version, all three delimiter forms are reported where 2.120.38 found zero issues in the same isolated project, and app 136's pre-fix line fires again the moment it is put back. Every abaplint config in these repositories runs parser_error: true, so the stopgap pattern-lint rule literal-no-separator (2026-09-13 to 2026-09-16) came back out. It never had anything to exempt: the one suffix form ABAP has after a literal, the text symbol 'text'(001), opens a parenthesis, not a name character — and upstream is equally silent on a doubled delimiter, a comment, an escaped \| inside a template, a literal followed by a symbol, and a template spanning lines. One correction to the issue's own exclusion list: `x`&&`y` IS reported, rightly — ABAP wants blanks around && just as it does around =.

A LOOP over a secondary key wants a WHERE the key can optimize

  • USING KEY <secondary> WHERE a = 1 OR b = 2 is a syntax error. A LOOP that names a secondary key reads through it, so the key components have to be compared with = and joined to the rest by AND; an OR at the top of the condition leaves nothing every row must meet. Found in review of abaplint/transpiler#1929 (2026-09-30): the PR's unit test carried exactly this LOOP, passed every check the transpiler runs, and the maintainer's real system refused it, twice. Measured on a system (2026-10-01): a = 1 AND ( b = 2 OR b = 0 ) runs. A dynamic key name, USING KEY (name), runs with the OR and loops every matching row, which is what the test now uses. A dynamic WHERE (cond) with the OR is a runtime error that no CATCH cx_root catches. abaplint 2.120.64 accepts the static form (check_syntax on, control USING KEY nokey fired): its LOOP syntax resolves the key and checks IS INITIAL before 7.40 SP02, and does not read how the condition combines the key components. Not abap2UI5-specific, so it went upstream: abaplint/abaplint#4353 (merged 2026-10-02) reports a top-level OR under a statically named secondary key in check_syntax, from the release after 2.120.64 on. An OR in parentheses, a dynamic key name and primary_key stay unreported there, as measured.

A function call in front of IS INITIAL

  • IF condense( lv ) IS INITIAL. is a syntax error, also on 7.58: "Unexpected operator "IS"." abap2UI5#2767 first met it on 7.02/7.31 through the downport, which made it look like an old-release limit. A 7.58 system refuses it just as well (measured 2026-10-03), and a Code Inspector SYNTAX_CHECK found it in two more classes on that system (2026-10-04). abaplint accepted it at every syntax version; from 2.120.71 on (abaplint/abaplint#4378) check_syntax reports a built-in function with a character-like result before IS INITIAL. Compare instead: IF condense( lv ) = ``., IF lines( lt ) = 0.. A functional method call in the same position is fine: IF upd_mode_text( ) IS NOT INITIAL and IF session_command_text( lv_cmd ) IS NOT INITIAL in abap2UI5-addons/sapgui went through the same run without a finding, so the restriction is about built-in functions. Constructor and table expressions there are not measured yet. More sites from the same run, since fixed: two in sapgui (abap2UI5-addons/sapgui#8) and one in rap-ext (abap2UI5-addons/rap-ext#19).

Release-gated ABAP SQL — the syntax version switch does not gate it

  • INTO CORRESPONDING FIELDS OF TABLE @DATA(…) is 7.55 syntax. Below that release the system refuses the class with "Inline data declarations cannot be used together with INTO CORRESPONDING additions", plus one follow-up "Field … is unknown" for every later read of the never-declared table — three errors whose cause is the first one. Found by a user pulling abap2UI5/samples main (2026-08-17): z2ui5_cl_smp_app_348 carried it in both of its SELECTs; fixed by declaring the tables with DATA … TYPE STANDARD TABLE OF … WITH EMPTY KEY and selecting INTO CORRESPONDING FIELDS OF TABLE @lt_… (samples 0d082a3). abaplint stays green because its SELECT grammar puts no version gate on the inline declaration — measured on 2.120.24 with check_syntax and downport on at syntax.version v750: zero findings, control probe fired. Plain INTO TABLE @DATA(…) is fine from 7.40 on; it is only the combination with CORRESPONDING that is late. Gate: abap2ui5lint — into-corresponding-inline-decl (2026-08-30). Upstream from abaplint 2.120.65 on (abaplint/abaplint#4354): check_syntax reports the inline declaration below 7.55. The linter keeps its rule for projects that pin an older abaplint.

RAP and CDS (abap2UI5/samples-stack)

The RAP objects activate, or do not, for reasons abaplint has no model of at all — it cannot even parse some of the syntax (section 6).

  • A behavior pool needs FOR BEHAVIOR OF. Without it the class is an ordinary ABSTRACT FINAL class that happens to contain a subclass of cl_abap_behavior_handler; it activates cleanly, RAP finds no handler, and every EML statement dumps on the first thing it asks for — CX_RAP_HANDLER_NOT_IMPLEMENTED, Method: GLOBAL_AUTHORIZATION, wrapped in CX_SADL_DUMP_APPL_MODEL_ERROR. The authorization check runs before every operation, which is why create, delete and data generation all failed identically while the apps themselves ran. de43a84.
  • @Semantics.currencyCode: true is DDIC-CDS only. In a view entity it fails activation with "Annotation Semantics.currencyCode is not allowed in view entities". A CUKY field derives the marker by itself, and the @Semantics.amount.currencyCode references are what establish the currency reference. f07cba4.
  • A draft table mirrors the CDS element names, not the persistent table's field names. The BDEF mapping applies to the persistent table only, so the draft table is checked against the entity directly and every snake_case field is an error, starting with "must have a key field 'TRAVELUUID' in position 2". af1a928. The draft-admin include additionally needs <GROUPNAME>%ADMIN</GROUPNAME> (7459f39).

A table field must not be an SQL reserved word

abap2UI5-addons/admin-cockpit shipped three tables with a key field DAY, one of them with HOUR as well. A user's pull (2026-10-06) refused all three: "DAY is a reserved word (choose another field name)", the same for HOUR, then "Table Z2UI5_T_CK_AGG could not be activated", and the same for Z2UI5_T_CK_LOG and Z2UI5_T_CK_USR. Every class that reads them failed with them. The dictionary checks each field name against the DDIC table TRESE (the reserved words of every supported database). abaplint was green: its xml_consistency has a short list of confirmed names only (ZONE, HANDLER, SECTION, PARAMETER, abaplint/abaplint#4331). TRESE also holds names a system allows as fields (TEXT, LENGTH), so the list cannot simply be TRESE. The fix renamed the columns to UTC_DAY and UTC_HOUR.

  • Prefix a field that names a date part, a unit or an SQL keyword — UTC_DAY, not DAY. A prefixed name is never in TRESE.
  • Rename the internal types that mirror the columns, too. A SELECT … INTO CORRESPONDING FIELDS into a structure that still says day stays green in abaplint and in the transpiler and leaves the component empty. An alias (utc_day AS day) would put the reserved word back into the SQL.

Gate: abaplint — xml_consistency, once DAY and HOUR are in its list (abaplint/abaplint#4388, open). Every other reserved word stays open until a system confirms it. Add it to that list when one does.

A database work area must be as long as the table line

z2ui5_cl_cockpit_setup (abap2UI5-addons/admin-cockpit) wrote its settings table from a local type:

TYPES: BEGIN OF ty_s_row,
         name  TYPE c LENGTH 30,
         value TYPE c LENGTH 255,
       END OF ty_s_row.
DATA(ls_row) = VALUE ty_s_row( name = name value = value ).
MODIFY z2ui5_t_ck_set FROM @ls_row.    " MANDT, NAME, VALUE

A user's pull (2026-10-06) did not activate the class: "The work area "LS_ROW" is not long enough.", twice. INSERT, UPDATE and MODIFY dbtab FROM wa take the work area as the table line. It must be at least as long, client field included, even though the client is filled in automatically. A structure with only the non-client columns is too short. Type the row as the table (VALUE z2ui5_t_ck_set( … )), fixed in abap2UI5-addons/admin-cockpit#6. abaplint 2.120.64 with check_syntax on is green on the broken class, and so is the transpiled unit run.

This one took abap2UI5 down with it. The class was a dependency of the cockpit's roundtrip monitor, so every abap2UI5 request dumped with "Syntax error in program Z2UI5_CL_COCKPIT_SETUP" (section 5).

Gate: abaplint — check_syntax, from 2.120.73 on (pinned here since 2026-10-10): abaplint/abaplint#4389 (merged 2026-10-09) makes it report "The work area ... is not long enough for table ...". Measured with that change on the pre-fix cockpit, it finds exactly the two statements the system named, and nothing in abap2UI5.

A parameter called default is read as an addition

abap2UI5-addons/admin-cockpit shipped this private method in z2ui5_cl_cockpit_setup:

CLASS-METHODS to_int
  IMPORTING
    val           TYPE clike
    default       TYPE i
  RETURNING
    VALUE(result) TYPE i.

A user's pull (2026-10-06) did not activate the class: "Unable to interpret "RETURNING". Possible causes of error include incorrect spellings or comma errors.", then "Compilation was canceled". The system reads default as the DEFAULT addition of val, takes TYPE as its value, and then finds nothing it can make of i and RETURNING. abaplint parses the same declaration as two parameters. Measured on 2.120.70 with every default rule on and check_syntax live: no finding, while an undefined variable in the same class fired. The parameter is called fallback now.

It bit again two days later. abap2UI5-addons/abap-agent-runtime shipped three such parameters - z2ui5_cl_agent_settings=>check_llm, z2ui5_cl_agent_session->rows_of, z2ui5_cl_agent_llm_anthropic=>setting - and a user's pull (2026-10-08) refused 11 classes: the three themselves ("Unable to interpret "DEFAULT"", "… "RETURNING"", "… "OPTIONAL"" - the token after the swallowed TYPE), and every class that uses z2ui5_cl_agent_settings, with the same error at the same line of its public section. One misnamed parameter in a central class reads as a broken addon. The parameters are called default_value now.

  • Do not name a parameter like an addition of the parameter list. default and optional after a parameter without OPTIONAL or DEFAULT, preferred, exporting, changing, returning, raising and exceptions depending on the section - measured on 758 SP03 for abaplint/abaplint#4391, whose description has the full table. !default is accepted everywhere, but a different name is the fix that no reader trips over. A structure component may carry these names: a BEGIN OF … END OF has no such additions.

Gate: abaplint — check_syntax, from 2.120.74 on (abaplint/abaplint#4391, pinned here since 2026-10-11): it reports the cases of that table, "Parameter "default" is read as the DEFAULT addition of the parameter before it, rename it or escape it as !default". Measured with that change, it finds exactly the cockpit's to_int and nothing in abap2UI5, the addons or open-abap-core; on the agent runtime before its fix exactly the three parameters the system named, and nothing after it (bool( … row … OPTIONAL default … ) of z2ui5_cl_agent_snapshot stands after OPTIONAL and is accepted, by the system and by the rule).

Do not depend on DDIC objects that are not everywhere

A sample that selects from VBAK compiles here — abaplint resolves it from the API dependency — and is a syntax error in every system without SD. abap2UI5/abap2UI5#2286, reported against the samples repository. Shipped sample and framework code may only touch DDIC objects that exist in a bare system, or must fetch data dynamically.

The same holds for classes. abap2UI5/samples-stack's z2ui5_cl_smps_llm_cloud, the HTTP transport for ABAP Cloud, names cl_http_destination_provider, if_a4c_cp_service and the cl_web_http_client_manager family statically. Its own comment expected that only a 7.40 system would refuse it. A Code Inspector SYNTAX_CHECK on an S/4HANA 7.58 on-premise system (2026-10-04) refused it as well: Type "CL_HTTP_DESTINATION_PROVIDER" is unknown, then Compilation was canceled. The class does not activate there, even though the factory never instantiates it on that system. abaplint is green because samples-stack resolves against the steampunk-2305-api dependency, where every one of these types exists. A class that has to exist everywhere but use an API that is only on some releases must name that API dynamically (CALL METHOD ('CL_…')=>(…), TYPE REF TO object, CATCH cx_root). Then the class activates everywhere and fails only when it is called on the wrong system.

3. Extended check (SLIN/ATC) — runs in real systems, not here

Gate: this repo, partially — npm run check:atc (.github/scripts/extended-check-gate.mjs) decides the traps a script can decide. The rest is open by construction: SLIN and ATC run in a system, and no gate outside one can stand in for them.

Backlog: abaplint · abaplint-avoid-use-extensions, abaplint-get-reference-obsolete

Partly gated by npm run check:atc (.github/scripts/extended-check-gate.mjs). Prose was tried first and did not hold: 43515c97, 0d9a7485, 5b9e16ea and 44642cbe are four separate sweeps of findings that had to be collected from a system afterwards. What a script can decide is now a gate; the rest is below. This section IS the full list - AGENTS.md ("Extended-check (SLIN/ATC) pitfalls") keeps one paragraph and points here, so a new trap is added here and nowhere else.

Gated:

  • A range-table row is checked against the domain of SIGN and OPTION. Any structure with the components sign/option/low/high is a selection structure to the syntax check, and a VALUE row of one without an option warns Specification "OPTION" is missing in the selection structure, one with a literal outside the domain — lower case included — "eq" is not a permitted value for component "OPTION". Three of them came from a user's system on 2026-09-23, out of test_token_odd_option in z2ui5_cl_ui5_util_context, a test that feeds such rows on purpose. A test that needs an odd row builds it field by field and passes the odd value through a variable. Gated by abaplint — range_row_values (abaplint/abaplint#4415), on in abaplint.jsonc since the 2.120.75 pin; it replaced check:atc's range_row. Only literals are judged, and a header default (VALUE #( sign = Ioption =EQ ( low = … ) )) counts.
  • LOOP AT … WHERE over a standard table is a sequential read and wants "#EC CI_SORTSEQ on the statement. Fifteen were annotated in the three sweeps above, and the gate found seven more that had accumulated since. "Sequential read" is all three spellings, not just the LOOP AT … WHERE the gate started with: READ TABLE … WITH KEY (not WITH TABLE KEY, which is a primary-key read) and a table expression keyed on a component — line_exists( tab[ name = … ] ) — are the same finding, and four of them shipped unannotated while the repository's own precedent carried the pragma. The gate reads all three now. A LOOP that names a secondary key (USING KEY … WHERE <key component> =) is a keyed read and wants no pragma; the gate skips it. The one such key in the framework is parent on z2ui5_if_ui5_types=>ty_t_attri (2026-09) - its component is written before the row is inserted and never through a reference afterwards, which is the condition a secondary key component has to meet.
  • An empty CATCH block wants ##NO_HANDLER — that is how you say the empty handler is deliberate. CATCH cx_root INTO DATA(x) ##NO_HANDLER. 14 handlers without the pragma sat in the vendored ajson test classes here (2026-09-19). From abaplint 2.120.73 on (abaplint/abaplint#4387, merged 2026-10-09): empty_structure has the option catch, off by default, that reports an empty CATCH block without ##NO_HANDLER or "#EC NO_HANDLER. A comment in the block does not count, as SLIN does not count it either. It is on in abaplint.jsonc (2026-10-10).
  • A table declared without a key clause has the default key — DATA t TYPE TABLE OF x. is the same table as … WITH DEFAULT KEY: every character-like component, in declaration order, and SORT without BY, COLLECT and DELETE ADJACENT DUPLICATES use it unasked. abaplint's avoid_use.defaultKey reports only the spelled-out form; the implicit one is fully_type_itabs' ("Specify table type" / "Specify table key", measured 2026-09-23 on 2.120.52 and 2.120.59), so a repository that switches that rule off loses it - abap2UI5/samples-controls app 034 shipped one and the corpus grew a regex for it. Write WITH EMPTY KEY, or the key you mean. 31 sites in this repository (2026-09-19), all in the vendored src/00/01 code (noIssues) and the frozen src/99 (not linted by abaplint.jsonc).
  • FIND/REPLACE … REGEX is POSIX, which is deprecated. FIND PCRE only exists on >= 7.55 and this repo targets v750/7.02. Prefer plain string logic; when a regex is genuinely needed, carry ##REGEX_POSIX (the vendored AJSON code does the same). abaplint's obsolete_statement (option regex, on by default) reports it only from syntax.version v756 on, where PCRE exists - so at this repository's v750 nothing reports a POSIX regex, by design rather than as a gap (measured 2026-10-09 on 2.120.71).
  • "! position. A doc comment sits directly before the one declaration it documents — inside a chained statement, not before the chain keyword — and never inside a parameter list. Document parameters with "! @parameter <name> | <text> in the method's own block. SLIN reports it as "ABAP Doc comment is in the wrong position"; nothing breaks, the text is simply never shown, which is why it kept recurring: first z2ui5_if_client=>cs_nav_mode here, then cs_status in abap2UI5/samples-stack (7459f39), then five findings on samples-stack's overview app from a user's system (2026-08-17) — two blocks before CONSTANTS:, three comments between the parameters of one method. Then a fourth: "directly before" means with nothing in between, a plain " comment included — a note to whoever edits the framework, placed between the block and the METHODS it documents, detaches it (bit us on z2ui5_if_client~check_on_navigated, 2026-09-16; put such a note ABOVE the block). A plain " comment inside a parameter list stays legal, which is why the "obsolete … note on _bind( )'s path has no !. That third recurrence turned it into a gate: decided here by check:atc, in samples-stack by its own npm run check:abapdoc, both by the same purely structural test (what statement follows the block; what the code line above it ends with). abaplint now has the rule: wrong_abapdoc_position, shipped in 2.120.51 (abaplint/abaplint#4286, 2026-09-12), written by larshp from the case this catalogue collected — the three recurrences, the three positions and the probe's 2 sites against 597 look-alikes, put on his own five-year-old request abaplint/abaplint#1951 as a comment. It reports the same three positions and carries cs_nav_mode as its bad example. So it is the linter's finding here now: wrong_abapdoc_position is on in abaplint.jsonc since the 2.120.51 pin, and check:atc kept exactly one shape the rule does not decide — a block directly before END OF, measured against that version. The plain comment between block and declaration was check:atc's too until 2.120.75, where the rule took it over (abaplint/abaplint#4414). (No site of that shape has ever been found in any of the five checkouts, which is why it is a leftover regex and not a backlog item.) abap2UI5/samples-stack still runs its own check:abapdoc: its abaplint pin has not moved yet, and that script goes the same way when it does. A rule that exists is only a gate where it is switched on: abap2UI5-addons/abap-agent-runtime pins 2.120.64 and still shipped a "! between the parameters of z2ui5_cl_agent_audit=>read, reported by a user's syntax check (2026-10-08) - its abaplint.jsonc lists its rules one by one and wrong_abapdoc_position was not among them. On now, in that repository's standard and cloud configs; the pre-fix file fires once. When a repository lists its rules explicitly, a new upstream rule does not reach it by moving the pin.

    Two more ways to detach a block, both found by a Code Inspector SYNTAX_CHECK on a 7.58 system over the addons (2026-10-04):

    • A blank line, inside the block or between the block and its declaration, detaches it like a comment does. Found in abap-cloud-gui (z2ui5_cl_cgui_layout, z2ui5_cl_cgui_report, z2ui5_if_cgui_variant_store, fixed in abap2UI5-addons/abap-cloud-gui#9). check:atc asserted the opposite until that day and then reported the shape itself, until wrong_abapdoc_position took it over in 2.120.75 (abaplint/abaplint#4414), on here since that pin.
    • A "! line that begins with @ is read as a command: "! @UI.presentationVariant's sortOrder … is "A command was expected after ABAP Doc symbol "@"". Only @parameter, @raising and @exception may start a line. Break the line one word earlier, so the annotation sits mid-line, where it is plain text. Four sites in rap-ext, fixed in abap2UI5-addons/rap-ext#19. Gated by abaplint — abapdoc_leading_at (abaplint/abaplint#4421), on in abaplint.jsonc since the 2.120.75 pin.

    And one the rule does decide, where the repository has it off: four blocks in front of a chained TYPES: in rap-ext (same PR). That is wrong_abapdoc_position's first position, and it reports exactly those four once switched on.

  • ABAP Doc is parsed as HTML. A field symbol or a placeholder written as <wa>, <row>, <CLASS> inside a "! block is "HTML tag not supported" and "not closed" in a system, and the block renders wrong. Write &lt;wa&gt;. AGENTS.md carried the rule as prose, and three of them still shipped in z2ui5_if_client (#2705) until a user's system reported them (2026-09-02) - so it was made a gate, check:atc (abapdoc_html). Gated by abaplint — abapdoc_html_tag (abaplint/abaplint#4422, from this catalogue) since the 2.120.75 pin, on in abaplint.jsonc; the check:atc part is gone. It reports a tag-like token outside the tags ABAP Doc knows (p em strong ul ol li h1-h3 br); a comparison a < b is not a token and not a finding.
  • PREFERRED PARAMETER is ignored unless every IMPORTING parameter is optional. The addition names the parameter a short-form call meth( x ) fills; with a mandatory parameter in the list it does nothing, and the compiler warns against the declaration: "Declare the parameter "VAL" as OPTIONAL. The addition PREFERRED PARAMETER is ignored if non-optional parameters are used". Nothing breaks — the short form goes to the single mandatory parameter, which is the same binding — so it is a warning that only a system reports, and a user's did (2026-09-05, z2ui5_cl_ui5_util_context: Private Section @82). msg_get_internal had one importing parameter and no addition until #2719 gave it a second, defaulted one and added PREFERRED PARAMETER val to keep its positional callers reading the same. They read the same without it, so the addition is gone rather than val made OPTIONAL — a mandatory parameter is the contract, and the ignored addition was the only thing wrong.

    Why nothing here saw it: abaplint parses the addition (MethodDefImporting accepts PREFERRED PARAMETER <field> as part of the IMPORTING clause) and no rule reads it — the token appears once in the whole 2.120.38 bundle, in the grammar. Measured on the pre-fix file: npx abaplint green, 0 issues in 264 files. Now gated by check:atc (preferred_param), which reads the IMPORTING block of every METHODS statement that carries the addition and reports the first parameter with neither OPTIONAL nor DEFAULT. Two boundaries: WITH DEFAULT KEY is a table type and not a parameter default, and a chained METHODS: is split into its elements first, so the addition is read against the signature that carries it. Both are in the gate's self-test, because a green run proves little on its own: the one declaration the rule was written for was repaired in the same change, and the seven uses left in src/ are correct ones it stays silent on (measured by inverting the rule - it reads all seven, the escaped !previous form included, and finds every parameter optional).

    The callers are fine either way. Since abaplint#2843 the syntax check lets a call leave a mandatory preferred parameter out, and a system does the same: measured on S/4HANA (ADT syntax check, 2026-10-02), with_pref( other = 2 ) with val mandatory and preferred is accepted with only the declaration warning, while the same signature without the addition is "No value was passed to the mandatory parameter VAL". Upstream from abaplint 2.120.65 on (abaplint/abaplint#4357): the rule preferred_parameter_ignored reports the declaration, and its quick fix declares the mandatory importing parameters OPTIONAL. It is a rule, so it reports only where a project switches it on - here it is on in abaplint.jsonc, which leaves src/99 out, so check:atc keeps reading all of src.

Not gated — a script cannot decide these:

  • DATA( ) from a packed computation. DATA(margin) = order-requireddate - order-shippeddate. with both operands p LENGTH 8 DECIMALS 0 is a syntax-check warning: "For the result of a computation with type P, the type P(8,0) is used here implicitly because it is not possible to determine …". The inline declaration cannot take length and decimals from an arithmetic expression, so the system falls back to P(8,0) and says so. That P(8,0) silently drops decimals when the operands have any. Declare the variable with its type and assign it. A COND #( … THEN packed_field … ) takes its type from the operand and is not this. Found by a Code Inspector SYNTAX_CHECK over a pulled abap2UI5/samples-controls (2026-10-04, z2ui5_cl_smpc_demo_002->row_json, fixed in abap2UI5/samples-controls#251). An abaplint-based probe over abap2UI5, samples-controls and samples-stack found no other instance.

    Second case, with * and / (2026-10-06). A user's pull of abap2UI5-addons/admin-cockpit reported the same warning for z2ui5_cl_cockpit_stats->p95: DATA(lv_target) = lv_total * 95 / 100. with lv_total TYPE p LENGTH 16 DECIMALS 0. So the length of the operand does not carry over either: a P(16,0) operand still gives P(8,0). Fixed by declaring lv_target with the operand's type. Gated by abaplint — inline_packed_computation (abaplint/abaplint#4413), both shapes, on in abaplint.jsonc since the 2.120.75 pin.

  • SORT or DELETE ADJACENT DUPLICATES on a table with an empty key. SORT result. and DELETE ADJACENT DUPLICATES FROM result. on a STANDARD TABLE OF string WITH EMPTY KEY give the warning ""RESULT" is a table with an empty primary key. Check the semantics of the statement." Both statements fall back to the primary key when no BY or COMPARING is given. With an empty key, SORT does nothing and DELETE ADJACENT DUPLICATES compares nothing. Write SORT result BY table_line. and DELETE ADJACENT DUPLICATES FROM result COMPARING table_line., or the components you mean. Found by a user's pull of abap2UI5-addons/admin-cockpit (2026-10-06, z2ui5_cl_cockpit_inst->get_implementers). abaplint 2.120.70 reports nothing. No script here could decide it: the key is in the table's type, which can be a returning parameter typed in another class. Gated by abaplint — sort_empty_key (abaplint/abaplint#4419), on in abaplint.jsonc since the 2.120.75 pin.
  • DATA( ) from a generic parameter (DATA(lv) = val with val TYPE clike) is "the fixed type STRING is used for the generic type CLIKE": the inline declaration has to pick a type, and SLIN objects to the pick. Declare the variable (DATA lv TYPE string.) and assign. Found by a user's system in z2ui5_cl_ui5_util_context=>url_param_get_tab (2026-09-02). Deciding it needs the parameter's type, which the statement does not carry.
  • Re-raising a variable typed cx_root (CATCH cx_root INTO DATA(lx). … RAISE EXCEPTION lx.) in a method whose signature declares no exception is "CX_STATIC_CHECK is not caught or declared in the RAISING clause" - the static type could be one. When the point is to run code on the way out and let the original exception travel on, use CLEANUP instead of catch-and-re-raise; it runs on the way to the outer handler and touches nothing (z2ui5_cl_ui5_http_handler=>_http_post, found by a user's system 2026-09-02). Not gated: whether the re-raise leaves the method depends on the enclosing TRY blocks, and the same statement inside an outer CATCH cx_root is fine (z2ui5_cl_ui5_srv_model=>main_json_to_attri).
  • SELECT without a WHERE clause wants "#EC CI_NOWHERE (z2ui5_cl_ui5_srv_draft=>count_entries_total, 43515c97). Whether the missing WHERE is correct is a judgement, not a pattern — here it is deliberate ( the method reports the whole table's size ), so the pragma records the decision instead of hiding it. count_entries next to it is owner-scoped and needs no pragma.
  • CREATE OBJECT … TYPE (name) into a generic reference, then CAST, is flagged as insecure object creation. Declare the typed reference and create into it directly (b25388ff):
    DATA li_app TYPE REF TO z2ui5_if_app.
    CREATE OBJECT li_app TYPE (lv_classname).
  • No redundant conversions. Do not wrap a value in CONV string( … ) when the source already has the target type — and not in CONV i( … ) when the assignment target is already TYPE i: the assignment converts by itself, and SLIN warns "Redundant conversion for type I". Found by a user on a real system (2026-08-17): z2ui5_cl_smpc_app_295's slider_value = CONV i( client->get_event_arg( ) ) in abap2UI5/samples-controls. abaplint's value_conversion / unnecessary_pragma do not cover it (measured on 2.120.24, control probe fired).

    Now gated, and the earlier claim here was wrong. This entry used to end "the type inference makes it undecidable for a text-level gate". It is undecidable in general and decidable for the shape SLIN actually flags, which is the only one that matters: the CONV is the WHOLE right-hand side of an assignment (<name> = CONV i( x ).) into a name the same file declares TYPE i — a DATA/CLASS-DATA line or a typed parameter. That is abap2ui5lint's redundant-conv-i — promoted out of samples-controls' scripts/pattern-lint.mjs on 2026-08-30, scope boundaries and all, and retired there.

    Two boundaries the rule keeps, both learned by getting them wrong first: a CONV inside a comparison (COND #( WHEN CONV i( x ) < 14 …) or an arithmetic expression (CONV i( x ) + 1) is load-bearing or at least arguable and is NOT flagged — the first draft reported apps 350 and 353, which SLIN itself had left alone. And a CONV inside a string template (|{ CONV i( x ) WIDTH = 2 }|) is a real conversion.

    What the gate is worth, measured: a user's system reported nine findings (534, 546 ×2, 547 ×2, 548, 549, 566, 609) on 2026-08-23. The rule reproduced all nine and found four more the system run had not — 356 once, 363 three times. A system run sees one package at a time; the gate sees the corpus.

    Same finding, CONV string( ), in THIS repository (2026-09-03). A user's system reported z2ui5_cl_ui5_util_context->data_render_struc @16 — "Redundant conversion for type STRING" on DATA(lv_name) = CONV string( lr_attri->name ), an RTTI attribute name inlined through a CONV. Three more of the identical shape sat in the same class — error_get_attributes, data_render_oref, and rtti_get_t_attri_by_any's CONV string( lo_struct->absolute_name ). All four are now a declared DATA … TYPE string. plus a plain assignment, which keeps the string type (the RTTI name is a CHAR field whose trailing blanks must not travel) without a conversion operator. That also retires the "a redundant CONV string( )" line in the BUNDLE paragraph below, where it was filed as a possible artefact of the fold: it was this code all along.

    The generic-parameter form is NOT this finding and has to stay. DATA(lv_url) = CONV string( url ) with url TYPE clike is the prescribed fix for the "fixed type STRING for the generic type CLIKE" trap above, and no system run has ever reported one of the sixteen in src. The discriminator is the operand: concretely typed → the assignment converts by itself and the CONV is redundant; generic → the CONV has to be there, it is what fixes the type. Two sites in src still have the flagged shape and were left alone because no system run has named them: z2ui5_cl_ui5_util_http's CONV string( …cv_char_util_cr_lf(1) ) pair, a CHAR constant with an offset. Fix them when a run does.

    abaplint has a rule for this and it does not reach it. redundant_conversion reports only a CONV whose operand ALREADY has the target type (text = CONV string( text )), not one whose target does. Measured on 2.120.38 against the pre-fix file with the rule switched on: 0 findings; the control probe (lv_probe = CONV string( lv_probe ) in that same method) fired immediately. The rule is on in abaplint.jsonc since this fix — as a floor, not as the gate for this finding. What would decide this one is redundant-conv-i's shape generalized from i to any concrete type with a concretely-typed operand, and that is not written yet.

    The shape it does reach still reached a user, because a repository had the rule off. abap2UI5-addons/admin-cockpit shipped DATA(lv_app) = CONV ty_s_error-app( is_error-app )., the operand already of the target type. A user's pull (2026-10-06) warned "Redundant conversion for type APP". redundant_conversion reports exactly this line (measured on 2.120.64, the version the cockpit pins), but the cockpit's abaplint.jsonc did not switch the rule on. It does now. Turn the rule on in every repository that ships ABAP.

    The CONV around a built-in function result (CONV string( to_upper( x ) )) is the same system warning, "Redundant conversion for type STRING", and redundant_conversion did not read it. Upstream from abaplint 2.120.71 on (abaplint/abaplint#4386): the rule reports a CONV whose operand is a built-in function with a result of the target type, and a CONV inside a function argument.

  • A text symbol ('text'(001)) is a CHARACTER literal, so it is not type-compatible with a formal parameter typed string — the view builder's v, for one: '...'(001) is not type-compatible with formal parameter "V", a SYNTAX_ERROR of the whole class (bit us on abap2UI5/samples' app 519, the sample whose subject is translatable texts, 2026-09-16). Read it into a variable and pass that; a plain assignment is a conversion and always allowed, and a symbol inside a string template needs nothing, an embedded expression being a general expression position. Gated by check:atc. Upstream from abaplint 2.120.65 on (abaplint/abaplint#4358): check_syntax reports a text symbol bound to an importing method parameter typed string, named and positional; PERFORM … USING and CREATE OBJECT … EXPORTING are not checked there, as not measured.
  • An Open SQL literal is a host expression: @( … ). In strict Open SQL every value in a WHERE comparison is escaped, a literal included — bare WHERE id = \TEST_COUNT_FOREIGN`becomesWHERE id = @( `TEST_COUNT_FOREIGN` ). Fixed by a user on a real system (2026-08-23, abap2UI5#2657) in z2ui5_cl_ui5_srv_draft`'s test class; the transpiled tests and abaplint both accepted it.

    Do not "fix" an internal table. DELETE lt_param WHERE n = \app_start`is ITAB syntax, where@( )is neither needed nor valid, and a grep forWHERE <name> = <literal>finds ten of those in this repository for the one real case. The discriminator is the statement:DELETE FROM <dbtab>/SELECT … FROM <dbtab>/UPDATE/MODIFYagainst a database table. Measured after the fix: that one line was the only bare literal in Open SQL acrosssrc`.

  • GET REFERENCE OF … INTO x is the old spelling; write x = REF #( … ). Same pull request, same reason — it is not released for ABAP Cloud, and nothing on this side reports it: check:cloud and the transpiled unit run are both green with it in place.

    The three in z2ui5_cl_ui5_srv_model (the alias re-wiring on the load, attri_get_val_ref) were rewritten in 2026-09 - and two of them came back from a system as "Unexpected operator REF" the same day. REF #( ) infers the reference type from the TARGET, and the target of those two is a field symbol TYPE any after a dynamic ASSIGN, which has no type to infer from. Write REF data( … ) (or the concrete REF ty( … )) whenever the left side is generic; REF #( ) only into a typed variable or parameter. abaplint 2.120.38 accepts the generic form without a finding (check_syntax on, default and cloud configs). Upstream from abaplint 2.120.65 on (abaplint/abaplint#4355): check_syntax reports REF #( ) into a field symbol typed any or data. The last one outside the upstream mirrors and the frozen package - conv_get_as_data_ref in src/00/03/z2ui5_cl_ui5_util_context.clas.abap - was rewritten here (2026-09): the context class is not a read-only mirror, this copy leads and abap-util follows (AGENTS.md, "Utilities"), so nothing waits on upstream for it.

  • A RAP handler names the entity by its BDEF alias. Where the behavior definition declares alias Ticket, an event handler's FOR ENTITY EVENT ... FOR z2ui5_r_smps_tck~TicketCreated draws "The alias Ticket from the behavior definition should be used instead" — twice on z2ui5_cl_smps_evt_tck (abap2UI5/samples-stack, found on a user's system 2026-08-17). Write FOR ticket~TicketCreated. abaplint has no grammar for FOR ENTITY EVENT (section 6), so no rule can reach it.
  • A draft Activate wants optimized. A plain draft action Activate; in a behavior definition draws "should be defined as optimized to enable optimized execution of determinations and validations" — samples-stack's ticket BO carried it while the travel BO one package over already had draft action Activate optimized; (same finding source, 2026-08-17). abaplint does not read BDEF sources at all.
  • ABAP Doc is parsed as HTML. A literal <, > or & must be escaped as &lt;, &gt;, &amp;.

Not gated: the BUNDLE. check:atc walks this repository's src/ — that is its stated scope, and over src/ it holds: every FIND/REPLACE … REGEX here carries ##REGEX_POSIX, including the five in the vendored AJSON where the pragma sits on the statement's CONTINUATION line (a line-based grep reports those as missing; the gate flattens statements first, so it does not).

abap2UI5-local is a different artefact. trigger_local.yaml copies these sources into that repository, where a script folds them into ONE class whose "Local Implementations" hold everything. A user's system run on z2ui5_cl_abap2ui5_local (2026-08-23) reported eight SLIN warnings against that folded include — POSIX regex twice, ABAP Doc position, <CLASS> not supported / not closed three times, a redundant CONV string( ) — at character OFFSETS (@16232, @38704), not at lines in any file here. Three of the four kinds are the families above and are silenced in src/ by a pragma, a ##REGEX_POSIX, or an escape. They come back after the fold, so what the fold does to those annotations is the thing to establish; the sources on this side are clean and no change here would move them. The fourth, the redundant CONV string( ), turned out NOT to be a fold artefact — the same finding came back on 2026-09-03 with a location in this tree (data_render_struc @16, the entry above), and the four sites it named are fixed here. The <CLASS> pair is worth looking at first: <p class="shorttext synchronized"> is the standard ADT shorttext form, and a tag scanner that splits on whitespace reads the attribute name as a second tag — which would make it an artefact of the fold rather than of the doc comment.

Not verified from here: abap2UI5-local was not attached to the session that wrote this, so the bundler itself was not read. What IS measured is that src/ carries the annotations and that check:atc is green over it.

4. Downport and transpile — one source, three targets plus a JS runtime

Gate: this repo — npm run verify builds all three targets and runs the transpiled tests. Nothing here is a linter rule and nothing here should be: the downport is a build of this repository, not a property of somebody's app.

Every framework file is downported to 7.02 (npm run auto_downport) and transpiled to JS (npm run auto_transpile), and is linted against check:standard and check:cloud. A construct can be valid ABAP and still break one of those four.

Backlog: open-abap · runtime-packed-copy-precision, core-class-source-not-available, core-seo-interface-implem-get-all, runtime-alpha-in-width, runtime-importing-copy-by-type-name, runtime-create-object-classic-exceptions, runtime-default-inherited-constant, core-serialize-object-identity, core-deserialize-nested-data-ref, transpiler-new-data-type-components, transpiler-case-type-without-into, transpiler-raise-resumable, transpiler-super-constructor-root-class, runtime-absolute-class-name, transpiler-ref-with-type, transpiler-let-field-symbol, core-abap-matcher-create

  • A test guarded with IF sy-sysid = 'ABC'. RETURN. never runs under npm run unit - ABC is the fixed sy-sysid of @abaplint/runtime, so the guard is a Node skip in disguise, and one that abap_transpile.json does not list: the suite reports the method as run. Nine tests in src/99 sat behind it (2026-10-12); seven pass on Node with the guard gone, the two that do not are in the skip list with their backlog item. Skip through the config, which names the reason, never through sy-sysid. The same inventory found five runtime gaps that each keep one test off Node - a lower-case dynamic type name in CREATE DATA … TYPE (name), an ASSIGN COMPONENT name with a dash in it, cl_abap_refdescr=>create( ) without kind, cl_abap_tstmp=>add( ) dropping the fraction of a timestampl, and describe_by_data_ref( ) on a reference to a reference - every one written up in the backlog with the fix, measured in memory against the transpiled tree. All five are fixed upstream and pinned here since 2026-10-13 (@abaplint/runtime 2.14.5, open-abap-core 9c1271b), and the tests they kept off Node run. Two in-code probes were dead: CREATE OBJECT TYPE ('CL_ABAP_ZIP') and describe_by_name( 'DFIES' ) both succeed in open-abap-core, so the tests behind them run in full.
  • A timestampl from GET TIME STAMP is noisy in the runtime, and on a :00 second it is not a time. @abaplint/runtime fills the long stamp by adding the milliseconds as a JS double to a 21-digit value, and the copy of a short timestamp into a timestampl takes the same double (abaplint/transpiler#1962). The noise stays inside the second until the clock is on a whole minute with 0 or 1 ms: then 20261014103400 arrives as …103399.9990784, the ISO text reads 10:33:99, and cl_abap_tstmp=>add( ) / subtractsecs( ) die with a JS RangeError: Invalid time value that no CATCH sees. It cost the release of 1.147.0 its first backend-prebuilt run (2026-10-14, npm run unit at 05:34:00 UTC in zcl_serializer_live's test_sweep_expiry); the same tree was green minutes before and after. So a unit test that fails once and passes on the re-run, with that stack, is this and not the change under test. A test that needs a clock should take a literal stamp or a whole hour (those copy exactly) rather than time_get_timestampl( ), until the two fixes land; the production sweep on the node runtime is exposed the same way, about two in sixty thousand sweeps. The GET TIME STAMP half is fixed upstream (abaplint/transpiler#2012, @abaplint/runtime 2.14.11; the lock here is still on 2.14.5); the copy half is backlog item runtime-packed-copy-precision.
  • Never put a 7.02 built-in function inside a table-expression key. This is the sharpest case in this section, because all four checks were green and a user's system was not. line_exists( mt_names[ table_line = to_upper( is_node-name ) ] ) is valid at v750; the downport rewrites it to READ TABLE mt_names WITH KEY table_line = to_upper( is_node-name ), carrying the call over verbatim, and a WITH KEY operand is not a general expression position before 7.40. A built-in function is only read as one where a string expression is allowed, so 7.02/7.31 falls back to the only other reading of name( … ) — a functional method call — and the class pool dies with SYNTAX_ERROR, "method TO_UPPER is unknown". That class is z2ui5_cl_ui5_client, so every app on the system was down (#2664). Hoist the call into a variable on the line above; a plain assignment IS an expression position at 7.02, so the variable is the whole fix. Same for a WITH [TABLE] KEY you write yourself and for an internal-table WHERE. Not affected: a functional method call in those positions (that reading is what 7.02 already applies), and the pre-7.02 built-ins — the same downported method has READ TABLE lt_parts INDEX lines( lt_parts ) ten lines above the failure and the compiler accepted it. The downport hoists it now. abaplint/abaplint#4272 (2.120.46, pinned here at 2.120.52) lowers line_exists( tab[ k = to_upper( x ) ] ) into temp1 = to_upper( x ). plus READ TABLE tab WITH KEY k = temp1, so the table-expression shape is safe at the version this repository builds with - measured on it, and check:downport no longer reports that shape. Gate: npm run check:downport over src/ keeps the two positions an author writes 7.02-ready themselves, which the downport passes through as they stand: a WITH [TABLE] KEY operand and an internal-table WHERE. abaplint's check_syntax reports the READ TABLE … WITH [TABLE] KEY one below 7.40 as well (abaplint/abaplint#4432, merged 2026-10-12, from this catalogue) from 2.120.76 on, pinned here; the WHERE positions and the predicate operand stay this gate's alone. A third position, same cause: the operand of a predicate expression. condense( val ) IS INITIAL answers Unexpected operator "IS" on 7.02 — the name is only read as a function where a string expression is allowed, and the compiler falls back to reading it as a method call (bit us in z2ui5_cl_ui5_handler=>request_parse_body, 2026-09-16). Same fix, same gate: a variable on the line above, npm run check:downport.
  • An object name over 25 characters breaks the namespace rename. There is a fourth target, and it is easy to forget because nothing in src/ mentions it: build-rename.yaml produces the rename_<name> branches for a consumer who needs a different namespace, by running abaplint --rename with ^z2ui5(.*)$ → <namespace>$1. The placeholder the PR gate renames to, znamespace, is the worst case the workflow allows — 10 characters, five more than z2ui5 — so an object name of 27 characters becomes 32 and ABAP's 30-character limit refuses it. abaplint stops the whole rename with a bare Error: Name not allowed and no file name, so the message does not say which object it choked on; the line above it in the output does. Every object name in src/ must be 25 characters or less. The rule is written down in .github/abaplint/rename.jsonc's own header — and it is worth reading that file before adding an object, because the only place the budget appears is a comment inside the config that enforces it. Found on z2ui5_if_ui5_app_serializer / z2ui5_cl_ui5_app_serializer (2026-09-19, #2772): 27 characters each, the only two objects in the tree over the budget, renamed to z2ui5_if_ui5_serializer / z2ui5_cl_ui5_serializer (23). Note what did not see it — check:naming reads the namespace segment and not the length, check:abapgit checks that <CLSNAME> matches the file name and not how long either is, and abaplint over src/ is green because the name is legal until it is renamed. Gate: npm run rename, which abaplint.yaml runs last for exactly this reason.
  • Never pass REF #( <fs> ) of a generically typed field symbol as an operand. bind( REF #( <tab> ) ) with <tab> TYPE STANDARD TABLE is valid at v750; the downport lowers the operand-position REF #( ) into DATA temp17 LIKE REF TO <tab>. plus GET REFERENCE OF <tab> INTO temp17, and a 7.02–7.4x system refuses the declaration - "The field "<TAB>" specified under LIKE either does not have a type or has a generic type", the whole class pool with it. Found in two test classes, z2ui5_cl_ui5_srv_bind and z2ui5_cl_ui5_srv_model (2026-09-23). Every check was green: the source is valid, the transpiled suite runs v750, and abaplint's v702 check_syntax over the downported tree reports 0 issues on that exact line (measured on 2.120.52); from 2.120.66 on (abaplint/abaplint#4363) it reports LIKE REF TO a generically typed table, the shape a 7.58 system refuses as well. Assign to a typed variable first - lr_tab = REF #( <tab> ). with lr_tab TYPE REF TO data, then bind( lr_tab ); the downport turns that into a plain GET REFERENCE OF <tab> INTO lr_tab. A typed field symbol is fine - the temporary copies its type. Up to 2.120.71 the downport itself wrote the LIKE REF TO; from 2.120.72 on (abaplint/abaplint#4405, pinned here) it writes TYPE REF TO data for a generic source, and the gate stays as a guard. Gate: npm run downport ends with downport-fix.mjs check-generic-like, which reads the OUTPUT (the only place the shape exists) and fails on any DATA … LIKE [REF TO | LINE OF] a field symbol the same method types generically; test.yaml runs it on every pull request. Generic parameters (val TYPE any) in the same position are not covered - no case yet.
  • Do not let an inline DATA(…) take its type from an offset/length expression. DATA(lv_field) = ls_attri->name+9. made abaplint's definitions_top infer TYPE name, which is no DDIC type at v702, and auto_downport exited with an error — the whole downport pipeline, over one temporary variable. Inline the expression into its uses instead (6b80329a, #2268). abaplint 2.120.71 declares such a variable TYPE c LENGTH <n> (measured 2026-10-09 on a c LENGTH 30 component +9: LENGTH 21), so this is history on the current pin.
  • Do not combine an inline declaration with a table-typed VALUE constructor. DATA(lt_in) = VALUE STANDARD TABLE OF … is a parser_error in the abap_cloud and abap_standard configs. Declare with DATA, then assign (78d4731f, #2128).
  • NEW builds objects, CREATE DATA builds data. The downport lowers every NEW into CREATE OBJECT, also when the target is a data reference. mr = NEW #( `x` ) with mr TYPE REF TO string, or mr = NEW string( `x` ) with mr TYPE REF TO data, comes out as CREATE OBJECT mr TYPE string ClassDefinitionNotFound ERROR., which is no statement at all: a parser_error in the downported tree. Without a value (NEW #( ), NEW string( )) it becomes a plain CREATE OBJECT mr, which check_syntax refuses there ("Target must be an object reference", "TYPE "string" not found"). Valid at v750, so the source is green and the transpiled suite never sees it, because it transpiles the downport. Found in a test class of z2ui5_cl_ui5_serializer (2026-10-09). Write CREATE DATA mr. (or CREATE DATA mr TYPE string.) and assign through mr->*. Measured on abaplint 2.120.68 in an isolated project (downport, check_syntax and parser_error on, v702; still so on 2.120.71, 2026-10-09). From 2.120.72 on (abaplint/abaplint#4404, pinned here) the downport writes CREATE DATA and the value through the reference. Gate: this repo - npm run downport ends red, but read the next entry before trusting what it names.
  • After a parser error, the downport stops for the whole file, and the next error it names is often not a real one. The CREATE OBJECT … ERROR above leaves the file unparseable, and every construct the downport had not reached yet stays as 7.40 source. abaplint then reports those at v702: xsdbool( lv_x IS INITIAL ) as a parser_error, and xsdbool( lv_x = lv_y ) - two plain names - as a method call with a named parameter (Method importing parameter "LV_X" does not exist, method parameter "VAL" must be supplied). That reads like a downport defect of xsdbool and is not one: on its own, xsdbool( a = b ) downports to boolc( a = b ) in every position measured (an assignment, a RETURNING value, a positional and a named argument, object references, <>, a parenthesized comparison). The order is not the source order either. The downport rewrites a statement that does not parse at v702 before one that does, and xsdbool( a = b ) parses as a method call. So a NEW further down the file is fixed first, and the xsdbool above it is left behind. A test assertion of z2ui5_cl_ui5_serializer was rewritten into an IF for that reason (2026-10-09) and put back once this was measured. Fix the first parser_error in the file and run the downport again before you change anything the later findings name. Measured on 2.120.68, same setup. Gate: none - the run is red either way, the trap is in reading it.
  • In a table VALUE, every row names every component an earlier row named. The downport builds all rows in one work area and never clears it, so after VALUE #( ( name = 'A' t_sub = lt_x ) ( name = 'B' ) ) row B carries A's T_SUB - on the 702 branch, and in npm run unit, which transpiles the downport output. Scalars, structures, an empty row ( ) and BASE leak the same way; inside a loop even the first row inherits the previous pass's last row. FOR rows and a shared prefix are fine. Found 2026-10-03 in abap2UI5/headless-frontend, whose transpiled run showed a nested table in a row that had none, and pinned down with a 17-test repro: green transpiled from src/, 16 red transpiled from the downport, so the transpiler, open-abap and the draft roundtrip are all innocent. Every check is green - the source is right, and abaplint up to 2.120.68 does not report its own output. Fixed in the downport from abaplint 2.120.70 on (abaplint/abaplint#4377): a CLEAR plus the shared prefix at each row, only where the rows differ in shape. A repository that pins an older abaplint has to write the omitted component out (t_sub = VALUE #( ), qty = 0) in every row, or keep every row the same shape. Not gated: a probe counted 159 constructors across the checkouts (2026-10-03, abap2UI5's framework code has none, samples' app overview has one that puts the first tile's intro on every tile at 702), and a gate would have failed the sample repositories on a bug that is not theirs; samples spells its eight constructors out.
  • A LET in a VALUE without FOR, or in a CONV, does not survive a transpiler before 2.13.98 when it reads 7.40 source: the binding is never declared and the statement dies with ReferenceError: s is not defined at runtime, not at transpile time. VALUE with FOR, COND and REDUCE are fine. This tree never meets it - npm run unit transpiles the downport, which outlines the LET first - but a host transpiling its own 7.40 classes against @abap2ui5/node-runtime does. Outline the binding into a DATA there, or transpile with 2.13.98 or later (abaplint/transpiler#1961). The same release sorts SORT itab BY (name) by the named component (abaplint/transpiler#1960); before it the dynamic component was dropped, so the table was sorted by its primary key, or not at all with an empty key.
  • A method parameter typed with the generic TYPE p breaks the class in the transpiled runtime. The transpiler has no type for it and emits new abap.types.typeTodoPGenericType() - for a CHANGING parameter, an optional IMPORTING one, an EXPORTING one nobody receives, and for the class's METHODS metadata. Calling the method dies with TypeError: … is not a constructor, and so does describing the class, which abap2UI5 does to every app (rtti_get_t_attri_by_oref); CATCH cx_root does not see a JS TypeError. A FORM parameter TYPE p and a method parameter TYPE numeric are fine. Found 2026-10-03 by report2cloud, whose FORMs become methods; type the parameter TYPE p LENGTH … DECIMALS …, LIKE the actual, or TYPE numeric. Nothing in the four checkouts has one. Fixed from @abaplint/transpiler-cli 2.14.0 on (abaplint/transpiler#1971, pinned here since 2.14.2): the parameter becomes a Packed. A host on an older transpiler still needs the rewrite above.
  • rescale( ) is not in the runtime before 2.14.4 - abap.builtin.rescale is not a function at runtime, the transpile is green. Fixed from @abaplint/runtime 2.14.4 on (abaplint/transpiler#1997, pinned here). round( ) with dec, prec and every mode is in from 2.13.94 on (2.13.93 throws round(), todo, handle decimals for any dec other than 0 - still the version the MCP server's Node backend installs). On an older runtime use round( ) where the scale of the result does not matter.
  • substring_after( ) / substring_before( ) ignore occ, len and case in the runtime. substring_after( val = path sub = '-' occ = -1 ) cuts at the FIRST dash under npm run unit, at the last on a system. Found 2026-10-08 in abap2UI5-addons/admin-cockpit, whose field-path parsing found 1 of 3 business objects. Take the offset from find( val = … sub = … occ = -1 ), which the runtime does resolve, and cut with substring( ). Fixed in the runtime from 2.14.3 on (abaplint/transpiler#1985, pinned here).
  • REPLACE ... OF ... WITH reads its WITH text like a regex replacement in the runtime. REPLACE ALL OCCURRENCES OF '{' IN s WITH '\{' leaves { under npm run unit (the backslash of \{, \}, \$ is dropped), and $& / $$ in the WITH text become the match / $ - on a system the text is inserted as written. The built-in replace( sub = … with = … ) keeps the backslash but has the same $ patterns. Found 2026-10-08 in abap2UI5-addons/admin-cockpit, whose brace escaping for a UI5 attribute could not be tested - and a double escape broke a popup on a real system unnoticed. Escape for UI5 with replace( … occ = 0 ) (abap2UI5's escape_literal does), keep $ out of literal WITH texts. Fixed in the runtime from 2.14.3 on (abaplint/transpiler#1987, pinned here).
  • A line_exists( ) in an ELSEIF is moved in front of the IF by the downport - as a READ TABLE … TRANSPORTING NO FIELDS, which sets sy-subrc. An IF sy-subrc <> 0. after an earlier READ then tests the wrong READ on the 702 branch and in npm run unit; on 7.50 the source is right. Found 2026-10-08 in abap2UI5-addons/admin-cockpit (z2ui5_cl_cockpit_session=>outcomes_of, a unit test answered "unknown" for a draft that exists). Keep a READ's result at once (DATA(lv_found) = xsdbool( sy-subrc = 0 ).) when the chain has a line_exists( ) / line_index( ) in an ELSEIF. Fixed in the downport from abaplint 2.120.72 on (abaplint/abaplint#4401, pinned here): it saves and restores sy-subrc and sy-tabix around the inserted READ.
  • A time or date assigned to a number keeps its digits in the runtime. lv_secs = lv_time. (t into i) is the seconds since midnight on a system, and 235930 under npm run unit - the same for int8, p and f, and for a d source (the digits instead of the day count). Arithmetic is right (lv_date_to - lv_date_from), and so is the other direction (t = i). Found 2026-10-08 in abap2UI5-addons/admin-cockpit, whose test across midnight answered -149490 instead of 70. Compute the seconds from the parts, lv_time(2) * 3600 + lv_time+2(2) * 60 + lv_time+4(2), which is right on both. Gate: open - the source type of an assignment is a question for abaplint, not a regex. Fixed upstream: a d source counts the days since 01.01.0001 from @abaplint/runtime 2.14.2 on (abaplint/transpiler#1977), a t source the seconds since midnight from 2.14.3 on (abaplint/transpiler#1976, merged 2026-10-09). 2.14.3 is pinned here (2026-10-10), so both are right under npm run unit; the arithmetic from the parts stays the form that reads the same on every runtime a host may have installed.
  • A packed value with more than 15 significant digits loses its last digits when it is assigned to another packed field in the runtime. A timestampl (p LENGTH 11 DECIMALS 7, 21 digits) set from a string is exact, but lv_b = lv_a. copies it through a JS double, so 20240229235959 becomes 20240229235958.9986304 and ...123000 becomes ...122999.9988736 - only values the double happens to hold exactly, a whole hour among them, survive. A system copies the digits. ajson's to_timestampl returns its result by such a copy, so its to_timestamp finds a fraction in |{ lv_timestampl }| and raises Unexpected timestamp format: get_timestamp( ) answers 0, and a bound table with a TIMESTAMP column holding 23:59:59 is refused whole on its way back in (to_abap) under npm run unit and in the node runtime. Found 2026-10-08 by the date round trip of z2ui5_cl_ui5_srv_model's test class, which uses a whole hour for that reason. Measured on @abaplint/runtime 2.13.99 and 2.14.0 (Packed.set( ) hands another Packed to this.set(value.get()), and get( ) is a Number). In a test, pick a value the double holds - a whole hour - and never conclude from a runtime run that such a copy is wrong on a system. Gate: open - the fix belongs in the runtime.
  • Transpiler-specific rewrites from the same PR — each of these was green in ABAP and wrong or unsupported under the JS runtime: SHIFT … DELETING LEADING/TRAILING → substring( ); CP used as a containment test → CS; IS NOT INITIAL on an internal table → lines( … ) > 0; a local class type in CREATE DATA … TYPE → LIKE.
  • An ABAP name that is a reserved JS word used to break the transpiler. The importing parameter with of c_replace_all was emitted as let with = …, illegal in strict mode, and the unit job went red. The fix at the time was a keywords entry in node/setup/abap_transpile.json rather than a rename of a public parameter (e3d8889c, #2351), and that list grew to seven words, each one discovered by a red build. The transpiler escapes every reserved word itself since abaplint/transpiler#1840, so the list is gone — do not start a new one. A reserved name that still comes out raw is an upstream bug and belongs in the transpiler's DEFAULT_KEYWORDS, not in a per-project option here.
  • A PARTIALLY IMPLEMENTED interface in a test double gets no stubs from the transpiler. On a system, calling an interface method the double does not implement raises CX_SY_DYN_CALL_ILLEGAL_METHOD (ABAP keyword documentation, INTERFACES - PARTIALLY IMPLEMENTED; an earlier version of this entry claimed "answers initial", which is wrong); the JS runtime generates nothing, so the first call to one of them is a TypeError: ... is not a function inside the method under test instead of that exception (abaplint/transpiler#2024 makes it the exception). Found the day the MCP server's run_unit_tests first ran app-template's starter test in the transpiled backend (2026-09-19): its ltd_client implemented the six lifecycle and output methods, and view_display( ) called check_app_prev_stack( ), _event_nav_app_leave( ), _bind( ) and _event( ) on the way — green on a system, red in the runtime. Implement every method the code under test calls, however trivially (_bind may answer a fixed token, _event its name), and keep PARTIALLY IMPLEMENTED only for the ones it never reaches. abaplint cannot decide it - which calls a test reaches is a question of control flow - and the runtime names the missing method, so a failing run_unit_tests is the gate.
  • The 702 build did not compile on 7.02 SP13 — six shapes abaplint's v702 profile passes (abap2UI5/abap2UI5#2947, found by a user on ECC 6.0 EHP5, SAP_BASIS 702 SP13, who fixed the 1.148.0 build by hand). ASSERT_TRUE / ASSERT_FALSE (7.40 - check_syntax resolves cl_abap_unit_assert against open-abap-core, which has both); EXPORTING/IMPORTING/CHANGING in a chained call a( )->b( CHANGING … ) (the v702 parser accepts it); TYPE int8 (7.50); a built-in function as the INSERT operand (read as a method call); a line over 255 characters (the downport joins the operands of a multi-line && inside VALUE #( ) into one, and the 7.02 editor cuts it on import); and a generated statement of ~400 lines, which the 7.02 class scanner refuses - abapGit then creates an empty class shell (generate-guide-class.mjs now closes the chain every 80 operands). Gate: this repo — the downport rewrites assert_true/false( x ) into assert_equals( exp = abap_true/abap_false act = x ) (assert-bool in node/setup/downport-fix.mjs, main keeps the idiomatic call), and check-702 in the same script fails npm run downport on the other four read from the OUTPUT. In src/: assign the object to a variable before a call with CHANGING, TYPE p LENGTH 16 DECIMALS 0 for a big integer, the function result to a variable before INSERT, a long && chain as a plain statement before the VALUE #( ). The seventh finding was a runtime dump on that kernel: offset/length access (iv_name+lv_off(3)) on a parameter bound to a component of a dereferenced data reference raised GETWA_NOT_ASSIGNED in check_alias_capable; substring( ) does not. No script decides that one - read for it where a ->* component is passed on.
  • xsdbool, never boolc — the downport converts xsdbool to boolc automatically, so writing boolc yourself breaks in the other direction. abaplint — prefer_xsdbool decides it (measured on 2.120.52).
  • Not every released class is released in ABAP Cloud. CAST cl_abap_elemdescr( … )->get_ddic_field( ) is not, and z2ui5_cl_pop_table had to derive the label from absolute_name plus a context helper instead (bc230abe). Framework code reaches system functionality only through z2ui5_cl_ui5_util_context for exactly this reason.
  • What the downport cannot rewrite - written 7.02-ready in the source. Giving 15 addons a 702 branch (2026-11-02, a publish-702 workflow in each) turned up constructs abaplint --fix leaves unchanged or writes wrong, each reproduced in one class on 2.120.92. Write them the 7.02 way where a source is downported: a cast in a TRY instead of IS INSTANCE OF (one of these blocks the whole class, Expected ENDMETHOD); an ORDER BY in a SELECT that has a WHERE and UP TO; SELECT SINGLE * FROM instead of SELECT SINGLE FROM ... FIELDS *; a row loop instead of MOVE-CORRESPONDING/CORRESPONDING #( ) between tables; an assignment plus CLEAR instead of CORRESPONDING #( ... EXCEPT ); READ TABLE ... ASSIGNING instead of CLEAR itab[ ... ]-comp; WITH DEFAULT KEY in a dynamic CREATE DATA; a declared work area instead of INSERT dbtab FROM @( VALUE #( ... ) ) (the output looks downported and does not compile); no @ on cursor statements; and an explicit declaration where an inline one has a void type - the downport rule reports nothing there, and v702 check_syntax only says "DATA" not found. The cast for CX_SY_ITAB_LINE_NOT_FOUND the syfixes script patches is upstream already (abaplint/abaplint#3735, #3240), and syfixes only runs when the --fix call before it may fail: (abaplint --fix ... || true) && npm run syfixes.

Backlog: abaplint · abaplint-downport-select-up-to, abaplint-downport-select-fields, abaplint-downport-is-instance-of, abaplint-downport-move-corresponding-tables, abaplint-downport-corresponding-except, abaplint-downport-clear-table-expression, abaplint-downport-create-data-empty-key, abaplint-downport-insert-host-expression, abaplint-downport-cursor-host-variables, abaplint-downport-void-inline-silent

5. Runtime — green here, wrong there

Gate: mostly open. Three entries have moved: linter — live-event-roundtrip (a liveChange that drops events under fast input), obsolete-binder/obsolete-model-update/obsolete-frontend-event (API that is accepted and no longer evaluated) and non-released-api (naming anything outside src/02).

One more lands in 0.2.0, and it is the sharpest case of this whole file: a class that BUILDS its view with the frozen z2ui5_cl_xml_view used to be skipped by the linter entirely — the run ended with no checkable app classes and exit 0, which reads like approval while nothing had been looked at. frozen-view-builder reports it instead. Not pinned here yet; this repository's src/99 will need it excluded when it is, since that package IS the frozen legacy.

The rest needs a running system with real data and is written down here precisely because no gate will catch it.

  • A syntax error is not an exception — CATCH cx_root does not stop it. A class with a syntax error that is loaded at run time ends the request with the runtime error SYNTAX_ERROR, a short dump. No TRY around the call catches it. abap2UI5's roundtrip monitor "failed open": it caught everything a monitor raised and every class that could not be instantiated. Then a half-activated pull of abap2UI5-addons/admin-cockpit (2026-10-06) left z2ui5_cl_cockpit_setup with a syntax error (section 2, the database work area), and every abap2UI5 app answered "500 Internal Server Error - Syntax error in program Z2UI5_CL_COCKPIT_SETUP". Nobody had opened the cockpit. The monitor was called automatically once its class existed. The fix is not a better CATCH. The framework no longer calls a monitor unless the installation switches it on (check_monitor_active in the user exit's set_config_http_post). Code that runs automatically inside every request has to be opted into, never just installed. A "never raises" promise covers exceptions, not syntax errors.
  • cl_abap_tstmp raised on the system and ran under the transpiler. z2ui5_cl_cockpit_setup=>now_minus_seconds (abap2UI5-addons/admin-cockpit) called cl_abap_tstmp=>subtractsecs( tstmp = now( ) secs = seconds ), where now( ) is a method returning timestampl. On a user's system (2026-10-06) the first screen after the administrator claim failed with CX_PARAMETER_INVALID_TYPE - "Parameter TSTMP has invalid type". The exception was not declared, so the request ended in a 500. open-abap's cl_abap_tstmp takes the same call without complaint, so the transpiled unit run and every lint stayed green. Why the system refused it is not measured: the argument was a functional call, not a variable typed timestampl. The fix leaves the class out. It converts with CONVERT TIME STAMP … INTO DATE … TIME …, does the arithmetic on the date and time of day, and converts back (ts_minus_seconds, with a test across midnight and year end; abap2UI5-addons/admin-cockpit#8). Treat cl_abap_tstmp like any API whose type checks only a system runs. If you call it, pass a variable typed exactly timestamp or timestampl, and test on a system, not only under the transpiler. Gate: open. A rule would need the system's own type check of cl_abap_tstmp, which nobody has measured yet.
  • After ASSIGN, check IS ASSIGNED — not sy-subrc. A 7.40 SP7 system ran every abap2UI5 app into an endless loop because sy-subrc was still 4 from an earlier READ TABLE when attri_get_val_ref tested it: the dynamic ASSIGN (lv_name) sat in a branch that had not been taken, and on that release a successful ASSIGN did not reset it either. Reported by a user (#1937), fixed by testing the field symbol instead (41890d59):

    IF <attri> IS NOT ASSIGNED.

    The transpiler is more forgiving about sy-subrc than the release range this repository ships to, so no test here can reproduce it.

    But IS ASSIGNED is not a blind replacement. A FAILED ASSIGN leaves the field symbol bound to whatever it pointed at before — verified in @abaplint/runtime's assign, where every failure path sets sy-subrc = 4 and returns without clearing the target. Inside a LOOP, or anywhere the same symbol was already assigned, IS ASSIGNED therefore reads TRUE for a failure, which is worse than the bug it replaces. UNASSIGN <fs>. before the ASSIGN where that can happen. Measured across the four repositories (2026-08-17): 42 sites take the drop-in, 17 are inside a loop and 2 re-assign — about one in three needs the UNASSIGN. The #1937 fix itself is a simple case: <attri> is declared at method start and assigned in two mutually exclusive branches. Gated by abaplint — subrc_after_assign, from 2.120.75 on (abaplint/abaplint#4417, written from this repository's backlog), on in abaplint.jsonc. It replaced the script gate this repository ran for it: on 2.120.75 it reports the same 16 sites as the detector that gate ran (11 in the frozen src/99, 5 in the mirrors src/00/01 and src/00/02), and none in the packages the ruleset reports on. The UNASSIGN half stays a review point — the rule reports the test, not the missing UNASSIGN.

  • An object name inside a string literal is not a reference — a rename sweep will get it wrong and nothing will notice. #2564 renamed z2ui5_cl_exit to z2ui5_cl_ui5_user_exit and carried the rename into the interface literal of get_user_exit_class, which became Z2UI5_IF_USER_EXIT — an interface that does not exist; the shipped one is Z2UI5_IF_EXIT. From that commit on, no user exit was found in any system: no configured UI5 bootstrap, no theme, no CSP override, no error. Everything stayed green, because the lookups behind such literals read SEOCLASS/XCO — neither exists under npm run unit, so "names nothing" and "customer implemented nothing" produce the same empty table. Found by a user whose config class had stopped working. Gated by npm run check:dynamic (.github/scripts/dynamic-name-gate.mjs): every Z2UI5_* name in a string literal must exist under src/, with names owned by other repositories listed in EXTERNAL with their reason. After any rename, reread the string literals — and when adding a dynamic lookup for something outside this repository, put it in EXTERNAL rather than silencing the gate. A literal naming an object this repository DOES ship is the other half of the same trap. The namespace rename (npm run rename, the build-rename workflow, abaplint --rename) rewrites references and leaves literals as they are, so in a rename_<name> installation `Z2UI5_IF_APP` still named the original interface: every app start was refused with "does not implement", no exit and no monitor was found. Name a shipped object through a typed, unbound reference instead — DATA li_app TYPE REF TO z2ui5_if_app. and z2ui5_cl_ui5_util_context=>rtti_get_ref_type_name( li_app ) — which the rename rewrites. Gated by npm run check:rename (.github/scripts/rename-literal-gate.mjs, production code of src/00–src/02), and npm run rename re-runs it over the renamed tree.
  • Never "modernize" WITH DEFAULT KEY to WITH EMPTY KEY on a table passed to a classic function module. The key is part of the table type; an incompatible one makes CALL FUNCTION fail at runtime, silently when it sits inside a TRY … CATCH / EXCEPTIONS guard. Concrete case and the reason it must stay: the comment above lt_impl in z2ui5_cl_ui5_util_context=>rtti_get_classes_intf_std, which passes the table to SEO_INTERFACE_IMPLEM_GET_ALL — with EMPTY KEY it returns no implementers and user-exit discovery breaks without an error.
  • An unevaluated EML failure poisons the whole LUW. In RAP, a failed EML statement whose FAILED is neither evaluated nor rolled back marks the transaction for abortion, and every later statement in the same request aborts — the request ends in CX_SADL_DUMP_APPL_MODEL_ERROR and the user sees the SAP 500 page, far away from the statement that caused it. abap2UI5/samples-stack f7f0a02: a Discard issued for instances that had no draft, deliberately ignoring FAILED, took down the whole data regeneration. Read first, act only on what exists, evaluate FAILED, and ROLLBACK ENTITIES when it is filled.
  • RAP action derived types have %key, %is_draft and %pky — but no %tky. EXECUTE … FROM VALUE #( ( %tky = … ) ) does not compile: "No component exists with the name %TKY". Entity derived types (READ, DELETE, the response structures) do have it, which is why only the EXECUTE statements were flagged and the READ ENTITIES in the same class was not. cebe6bd.
  • DELETE itab INDEX sy-tabix inside a LOOP AT over the SAME table deletes whatever row sy-tabix names when the statement runs. The loop handles the deletion of its current row - the next pass is the row after it - but sy-tabix is the loop's index only until another statement in the body writes it: an inner LOOP, READ TABLE, APPEND, INSERT, COLLECT, a method that does any of these.

    LOOP AT tab INTO DATA(row).
      LOOP AT row-items INTO DATA(item) WHERE flag = abap_true.
        DELETE tab INDEX sy-tabix.     " sy-tabix is the index in row-items
        EXIT.
      ENDLOOP.
    ENDLOOP.

    Where the overwritten sy-tabix is positive, nothing dumps: the statement deletes another row and the result looks plausible and is WRONG. Where it is 0 - an inner LOOP that found nothing - index 0 is a short dump, and TABLE_INVALID_INDEX is the ABAP runtime error's own name for it. (DO sets sy-index, not sy-tabix.) When nothing in the body writes sy-tabix, the statement deletes the right row, and the next change to the body breaks it without a sign.

    Found by an e2e interaction driving samples-controls' app 352, whose listClose round-trip answered HTTP 500 with that error (2026-08-17). Do not read that as the transpiler being stricter than a system — it was checked, and it is not: @abaplint/runtime 2.13.59's deleteInternal raises for index 0 and sets sy-subrc = 4 for an out-of-range one, which is what a real system does. The transpiled backend found this because it was exercised, not because it judges harder.

    A pattern search then found it eight times across the ecosystem: four ports in abap2UI5/samples-controls (352 twice, 354, 298, 377), three in abap2UI5/samples (z2ui5_cl_smp_context's filter_itab and itab_filter_by_val, app 070), and one in the vendored ajson (z2ui5_cl_ajson_filter_lib, upstream — not patched here). Two of them were wrong twice over: in filter_itab the sy-tabix belonged to an INNER loop, so the index deleted was another table's.

    Build the result instead — collect what is kept and assign it back. It reads as what it does and has neither failure mode. DELETE itab WHERE … is fine too where the condition can be expressed there; DELETE itab INDEX sy-tabix right after a READ TABLE is correct and common (z2ui5_cl_ajson), which is why the shape cannot simply be banned. Gated by abaplint — delete_index_in_loop (abaplint/abaplint#4418, the read-then-delete form excepted), on in abaplint.jsonc since the 2.120.75 pin.

  • Three environments, one source. Code must work on NW 7.02, standard ABAP and ABAP Cloud (check:standard, check:cloud, downport). A statement that only exists in a newer release passes the default target and fails the others.
  • app/webapp/ source must be 7-bit ASCII — every frontend file is embedded verbatim into an ABAP class under src/01/03/, where abaplint's 7bit_ascii rule applies. Build non-ASCII runtime strings at run time, never as a literal.

Two the transpiled suite cannot see — found by a user's unit-test run (2026-09-02)

  • A failed conversion clears its target on a system. <comp> = 'abc' into a packed field, 'seven' into an integer: the exception (CX_SY_CONVERSION_NO_NUMBER) is catchable, but the target is already initial when the handler runs - and to_abap( ) clears its container before it fills it. The transpiled runtime leaves the target untouched, so "the cell is skipped and the old value stands" (z2ui5_cl_ui5_srv_model=>delta_apply_field) held in npm run unit and three z2ui5_cl_ui5_client tests failed on a system with the refused cells at zero. The rule: whenever a write may be refused and the old value is promised, copy it aside first and put it back in the handler - main_json_to_attri already did, the delta path now does. From runtime 2.14.3 on (abaplint/transpiler#1986, pinned here) the transpiled runtime clears the target too, so npm run unit shows it.
  • ASSIGN dref->* TO <fs> with a typed field symbol is a runtime error, not sy-subrc 4. A sorted table assigned to a TYPE STANDARD TABLE field symbol dumps with ASSIGN_TYPE_CONFLICT on a system; the transpiler answers sy-subrc 4, and test_skip_sorted_table was even skipped in Node with a note saying the real system takes "the subrc branch". Decide the kind by RTTI (cl_abap_tabledescr->table_kind) BEFORE the ASSIGN (z2ui5_cl_ui5_srv_model=>check_table_standard); the skip entry is gone.

A sorted-table line by field symbol is not a CHANGING parameter

abap2UI5-addons/admin-cockpit did not start on a user's system (2026-10-08):

Call of the method ADD_SUM of the class Z2UI5_CL_COCKPIT_STATS has failed;
the actual parameter for CS_SUM is write-protected

That is CX_SY_DYN_CALL_ILLEGAL_TYPE, raised on the initial rendering. Five methods grouped rows in a SORTED TABLE … WITH UNIQUE KEY app, reached the group with READ TABLE … ASSIGNING <sum> (or INSERT … ASSIGNING <sum>) and called add_sum( CHANGING cs_sum = <sum> ). Through a field symbol, the key components of a sorted or hashed line are write-protected. The whole line is therefore no actual parameter for CHANGING, even when the callee writes only non-key components. Writing <sum>-cnt directly is fine.

  • Fix: read the line INTO a work area, call with the work area, then MODIFY TABLE itab FROM wa (abap2UI5-addons/admin-cockpit#10). Or pass only the non-key components the callee changes.
  • Why nothing caught it: the class activated, so the system's syntax check accepts it. abaplint 2.120.64 reports nothing with every default rule on. The transpiled runtime runs the call (a test method with the same shape passes in npm run unit). Only a run on a system shows it.
  • Gate: abaplint — changing_sorted_line (abaplint/abaplint#4420), on in abaplint.jsonc since the 2.120.75 pin. A script could not decide it: it needs the table kind behind the field symbol, a type question.

6. Blind spots — green, or red, for the wrong reason

Gate: none, and that is the definition. An entry lands in this section because a check passed while the code was wrong, or failed while it was right. A gate cannot cover it — if one could, the entry would have moved to a numbered section above.

  • Never skip a test with IF sy-sysid = + backtick-ABC. ABC is the system ID of the Node runtime, so the guard makes the method a silent no-op in npm run unit while it still runs in a real system — CI stays green over assertions nobody executes. A test that genuinely cannot run under the transpiler belongs in the skip list of node/setup/abap_transpile.json with a note naming the missing runtime capability; the runner then prints it as skipped instead of pretending it passed. (The remaining sy-sysid guards are all in the frozen src/99 — they are history, not precedent.)
  • Generated code is not editable — fix the generator. src/01/03/ comes from npm run app2abap and a manual fix there is reverted by the next generation, with check:app2abap failing the PR. The same trap outside this repository, without a gate to catch it: in abap2UI5/samples-controls#38 an over-length statement was first split by hand in a file produced by scripts/generate-overview.mjs, and the split had to be moved into the generator in a follow-up commit. Before editing any src/ file, check whether something writes it.
  • A red linter is not always a real finding. abaplint cannot parse RAP's CLASS … DEFINITION PUBLIC FOR EVENTS OF <entity> or METHODS … FOR ENTITY EVENT, so the class pool does not parse and every finding on the file — the structure error, the parser errors, the check_syntax follow-ups — is a consequence of that, not of the ABAP. It also cannot resolve superclasses that are not in the API dependency: cl_apc_wsp_ext_stateless_base is on-premise only and absent from steampunk-2305-api, which made the WebSocket handler and its callers report unknown references. abap2UI5/samples-stack 468af3d sorted 25 findings into ten real ones and fifteen artefacts. Never rewrite valid ABAP to satisfy a linter that cannot see it. Suppress narrowly instead — per rule and per file, with the reason written next to the entry, and global.noIssues only when the error comes from the parser itself and no rule-level exclude can reach it. Scope it to the .abap files so the .clas.xml keeps its object_naming and xml_consistency coverage.

7. The rule stock — read for it by hand until abaplint decides it

Gate: open, by definition: every check below is an abaplint rule idea in backlog/ (backlog/ABAPLINT.md lists them, each item carries the bad example, the scope and the evidence). Sections 1–6 hold defects that reached main; this section is the other half — shapes other linters, Clean ABAP and the fix history of open-source ABAP code say go wrong, collected on 2026-10-11/12 and probed against abaplint 2.120.75: no rule reports them yet. Until one ships and is switched on in abaplint.jsonc, the only check is a reader. Read a change under src/ for them the way you read it for sections 1–5. When a rule lands, its item goes and the line here goes with it.

The groups are by consequence, worst first. A line names the shape, then the fix.

Wrong results and dumps — read every change for these

  • A stale field symbol behind IS ASSIGNED. READ TABLE … ASSIGNING <fs> in a loop, then IF <fs> IS ASSIGNED — a failed READ leaves <fs> on the previous row. UNASSIGN <fs> before the READ (section 5 has the #1937 half of this).
  • A stale system field. A sy-subrc test after a statement that does not set sy-subrc (a method call without EXCEPTIONS, an assignment, CLEAR); an INDEX sy-tabix after a nested READ or LOOP overwrote sy-tabix. Read the field right after the statement that sets it, into a variable if needed.
  • A read that needs a SORT. READ TABLE … BINARY SEARCH and DELETE ADJACENT DUPLICATES … COMPARING on a table not sorted by those fields silently miss rows. SORT by exactly those fields first, or use a sorted table.
  • LOOP and READ targets. The work area or field symbol read after ENDLOOP (it holds the last row, or nothing); an inner LOOP/READ reusing the outer target; READ TABLE … INTO <fs> overwriting the row <fs> points to (ASSIGNING was meant); a target typed with another structure than the line.
  • Loops that do not loop. A body that ends with RETURN/EXIT/RAISE on every path; a WHILE whose condition variables the body never writes.
  • Blanks a character literal loses. REPLACE ALL OCCURRENCES OF ' ' has an empty pattern and raises CX_SY_REPLACE_INFINITE_LOOP; 'Hello ' loses its blank in CONCATENATE and &&. Backtick literals keep the blank.
  • Constant comparisons. a = a, x <> A OR x <> B (always true), x = A AND x = B (always false) — copy-paste bugs (abap2UI5 7647c26).
  • Implicit conversions. A character field compared with a number, a d/t field with a formatted literal ('2024-01-01'); a literal or constant longer than its target; TIMESTAMPL assigned to TIMESTAMP (the fraction is cut); a constant offset/length on a string without a length check; DATA( ) from a generically typed parameter (the type is decided at runtime).
  • Field symbols and references. A field symbol read that nothing in the method assigns; a data reference dereferenced on a path where it can be initial.
  • VALUE for a unique-key table with two literal rows of the same key — a dump at runtime.
  • Classic EXCEPTIONS. CALL FUNCTION … EXCEPTIONS without OTHERS (an unlisted exception dumps), a named exception mapped to 0, the sy-subrc of the EXCEPTIONS never read, a method with classic exceptions called functionally.
  • CATCH that cannot catch. A CATCH of an exception nothing in the TRY can raise; RAISE of a cx_root-typed variable in a method without a RAISING clause; inside CATCH … INTO x, a RAISE that does not pass previous = x (section 3's cause rule, check:cause, covers the message form only).
  • Discarded results. A functional call whose RETURNING value nobody reads.
  • Copy-paste. Two WHEN blocks with identical bodies — merge the conditions, or one of them is the bug.
  • Shadowing. A local with the name of an attribute or a global (mv_x as a local), a DATA declared in an IF/LOOP block and used after it.
  • SQL that reads or writes too much or too little. DELETE FROM / UPDATE without WHERE; INNER JOIN on a text table (rows without a translation vanish — LEFT OUTER JOIN with the language in the ON); FOR ALL ENTRIES whose field list lacks the primary key (duplicates collapse); a COMMIT, a CALL FUNCTION … DESTINATION or a MESSAGE inside SELECT … ENDSELECT or an open cursor; SQL wildcards in CP (%) and ABAP wildcards in LIKE (*).
  • Function module calls whose actual parameter type does not fit the formal one — the dump comes at runtime, not at activation.
  • Control levels (AT NEW, AT END OF, AT FIRST/LAST) inside LOOP … WHERE or FROM/TO — they see the filtered rows and break wrongly.
  • RAP. COMMIT/ROLLBACK WORK, IN UPDATE TASK or SUBMIT in a behavior handler or saver.
  • Tests. An assertion that cannot fail (act = exp, two literals); a test that expects an exception but has no fail( ) after the call; DB writes, COMMIT, SUBMIT or RFC in a FOR TESTING class; ASSERT in production code (a dump, not an error) and in tests (dumps the run instead of failing it).

Backlog: abaplint · abaplint-stale-fs-is-assigned, abaplint-stale-sy-fields, abaplint-sort-before-read, abaplint-loop-target-misuse, abaplint-suspicious-loop, abaplint-literal-blanks, abaplint-self-comparison, abaplint-char-numeric-comparison, abaplint-literal-exceeds-target, abaplint-timestampl-to-timestamp, abaplint-string-offset-unguarded, abaplint-inline-generic-source, abaplint-fs-never-assigned, abaplint-possibly-unbound-reference, abaplint-value-duplicate-unique-key, abaplint-classic-exceptions, abaplint-caught-never-raised, abaplint-reraise-cx-root-undeclared, abaplint-catch-handling, abaplint-unused-returning-value, abaplint-duplicate-code, abaplint-variable-scope, abaplint-sql-without-where, abaplint-inner-join-text-table, abaplint-fae-duplicate-rows, abaplint-commit-in-select-loop, abaplint-wildcard-mismatch, abaplint-call-parameter-type-conflict, abaplint-control-level-restricted-loop, abaplint-rap-forbidden-statement, abaplint-unit-test-assertions, abaplint-unit-test-side-effects, abaplint-assert-in-production

Activation — the system refuses these, check_syntax does not yet

  • A CATCH of a subclass after the CATCH of its superclass in the same TRY.
  • RAISE EXCEPTION … MESSAGE for a class without IF_T100_MESSAGE.
  • RAISE EXCEPTION or THROW in a method or function module that declares classic EXCEPTIONS - even for a CX_NO_CHECK; raise it in a method of its own, from which it passes through.
  • An offset or length on a numeric variable (lv_int+1(2)) - only character-like and byte-like data take one. It comes in disguised as date arithmetic declared inline: DATA(x) = lv_date + 31. is a number, not a date, so x+6(2) does not activate (abaplint types some of these as d).
  • A write to a key component of a sorted or hashed line through a field symbol (<line>-key = … after LOOP/READ … ASSIGNING) — the CHANGING form is section 5's.
  • LOOP … USING KEY a sorted secondary key with a key component under OR or CP in the WHERE.
  • An SAP API or addition the configured release does not have; GET REFERENCE under the Cloud language version.

Backlog: abaplint · abaplint-syntax-catch-order, abaplint-syntax-raise-message-t100, abaplint-syntax-raise-in-classic-procedure, abaplint-syntax-offset-on-numeric, abaplint-inline-date-arithmetic-type, abaplint-syntax-sorted-key-write, abaplint-syntax-secondary-key-where, abaplint-release-api-availability

Security

  • A literal assigned to or passed as a password, secret or token.
  • A dynamic WHERE, field list or table name built from input without cl_abap_dyn_prg (escape_quotes, check_column_name, …).

Backlog: abaplint · abaplint-hardcoded-credentials, abaplint-sql-injection-dynamic

Style and Clean ABAP — only where the house style asks for it

These are Clean ABAP clean-ups, not defects. AGENTS.md ("Coding Style") decides which apply here; three of them it rules out:

CheckHere
Empty PROTECTED/PRIVATE SECTION removedno — AGENTS.md keeps all three sections
CATCH cx_root avoidedno — AGENTS.md catches cx_root and re-raises it with previous
OPTIONAL and boolean IMPORTING parameters avoidedno for the public API (z2ui5_if_client relies on them); fine in new private methods
Comments: " not *, indented like the next line, a space after ", no forgotten TODO/FIXMEyes
Guard clauses: IF … CONTINUE instead of CHECK in a loop; an early exit instead of a long IF at the end of a loop or methodyes
A method returning abap_bool named is_/has_/…yes for new methods; the check_on_*( ) names are API
+=, to_upper( ), condense( ) instead of x = x + …, TRANSLATE, CONDENSEyes — the downport expands += for 7.02; to_upper( ) and condense( ) exist on 7.02 already
FINAL( ) for inline declarations never changednot yet — 7.57; check that the downport handles it first
A table expression / VALUE #( … OPTIONAL ) instead of READ TABLE + sy-subrcyes, where not-found is skipped; the exception differs otherwise
SORT … BY with ASCENDING/DESCENDING statedoptional
CLEAR right after DATA, a needless me->yes
Classes of only static methods, interfaces of only constants, public methods outside an interfacejudge per case
Class size (methods, attributes per class), statements per WHENthe method limits in AGENTS.md apply; the rest is a smell
Magic numbers, sy-uname/sy-sysid compared with a literal, '@00@' icon literalsname them as constants
MESSAGE TYPE E/A/X or WRITE in a global class; MESSAGE ID sy-msgid … TYPE sy-msgtyno dialog in classes — raise an exception
COLLECT; a table with the implicit default keyavoid (AGENTS.md: no default key)
A CONV whose target already has the type; "#EC CI_SUBRC where sy-subrc is checked; an OPTIONAL parameter no caller supplies; a message nothing usesremove

Backlog: abaplint · abaplint-comment-style, abaplint-guard-clauses, abaplint-method-signature-style, abaplint-class-design, abaplint-size-metrics, abaplint-modern-statements, abaplint-needless-code, abaplint-prefer-final, abaplint-read-table-to-expression, abaplint-sort-explicit-order, abaplint-magic-number, abaplint-hardcoded-values, abaplint-message-usage, abaplint-avoid-use-extensions, abaplint-redundant-conv-target, abaplint-unnecessary-ec-ci-subrc, abaplint-unused-method-parameter-global, abaplint-unused-message

Objects and files

  • A UTF-8 BOM at the start of a .abap file (check:abapgit covers this repository; other repositories need the rule).
  • PROG/FUGR/CLAS XML without fixed point arithmetic.
  • An object whose original language is not the configured one.
  • Append-structure fields without the ZZ/YY prefix.
  • A transaction whose program, class or screen does not exist.
  • A global class with logic and no test class.

Backlog: abaplint · abaplint-abap-file-bom, abaplint-fixed-point-arithmetic, abaplint-master-language, abaplint-append-field-prefix, abaplint-tran-consistency, abaplint-class-without-test-class


When a pull does show diffs

Take the system's side. Commit the files exactly as abapGit serialized them, then run npm run check:abapgit on the result — whatever it now flags is the rule that was broken, and the fix belongs in this repository rather than in the system.

When an import fails rather than diffs, read the message twice: the object named in it is often not the broken one. A missing BDEF reports as a DDLS problem and as a where-used-list error on unrelated classes; an over-length line reports as a literal problem and leaves an empty class behind.

Adding a new case

Keep the catalogue worth reading: one entry, one defect that actually happened.

  1. Put it in the right section — 1 round trip and import, 2 activation, 3 extended check, 4 downport/transpile, 5 runtime, 6 blind spots. A new kind of failure gets a new section. Section 7 is the rule stock, not cases: a case that bites us moves out of it into 1–6.
  2. Name the evidence — the commit, PR, issue or file where it bit us. An entry without a case is a rule somebody will delete as speculation. Say when it was a user who found it; those are the expensive ones.
  3. Say why the tooling missed it. If the answer is "it did not, we just ignored it", the entry belongs in a lint config, not here.
  4. Gate it if a script can decide it. Byte format, metadata and source text go in .github/scripts/abapgit-format-gate.mjs; statement-level checks that only a real system runs go in .github/scripts/extended-check-gate.mjs. Keep the prose entry as the reasoning. Test the rule both ways — inject the defect, see it fire, confirm the tree is clean again.
  5. If a script cannot decide it, say so in the entry rather than leaving the reader wondering why it is not gated.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.