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.
In this repository5 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- build-an-appSkill
- ui5-checkSkill
- view-chain-layoutSkill
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
- Distinctive: line 24
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 …
- Distinctive: line 43
Run first, in this order:
- Very specific: line 68
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 …
- Distinctive: line 78
Every "abaplint does not catch X" claim in this file was measured, and the measurement has one trap that makes every result meaningless:
- Distinctive: line 195
No script decides this one.
- Very specific: line 716
never inside a parameter list: Document parameters with "!
- Distinctive: line 754
When a repository lists its rules explicitly, a new upstream rule does not reach it by moving the pin.
- Very specific: line 905
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 …
- Very specific: line 1079
Not verified from here: abap2UI5-local was not attached to the session that wrote this, so the bundler itself was not read.
- Very specific: line 1089
Every framework file is downported to 7.02 (npm run auto_downport) and transpiled to JS (npm run auto_transpile), and is linted against …
≈ “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.
Files
<!-- 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 withbuild-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:
| linter | an abap2ui5lint rule — travels to every repository that runs it |
| this repo | a script under .github/scripts/ — covers this tree only |
| abaplint | a rule in abaplint.jsonc — covers repositories that enable it |
| open | nothing 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 gateWhy 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_syntaxlive: a.clas.xmlwith 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, usingxml_bomitself as the second control probe. - abaplint's
global.filesis not all ofsrc/. It coverssrc/00(withnoIssues),src/01andsrc/02—src/99is never read at all. - The transpiler ignores visibility. Every ABAP member becomes a plain JS
property, so
npm run unitstays 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
rulesblock runs NO rules. Not the defaults — none. A run over a deliberately broken file then prints0 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.
| Case | Why it matters |
CLASS-METHODS class_constructor. in a PRIVATE SECTION | ABAP 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, ' (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 v750 | dereferencing 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 v750 | 7.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, ', 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.
| Rule | What abapGit does | Where it bit us |
bom | .xml starts with the UTF-8 BOM EF BB BF; .abap never does | 8e272492, 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 |
eof | exactly one terminating \n, no blank line after it | c7185c38 — z2ui5_if_action.intf.xml ended with \ No newline at end of file |
crlf | LF only, everywhere | .gitattributes used to normalize this and was deleted in b62ea07; the gate is what enforces it now |
tab | no tabs — the ABAP editor expands them, so the pulled source is not this file | — |
apos | XML goes through the ABAP iXML renderer, which escapes the apostrophe as ' | abap2UI5/samples 702813e "fix abapgit" — nine .clas.xml carried a raw ' in <DESCRIPT> and came back changed on the next pull |
filename | lower case; abapGit derives the object name from it | — |
sidecar | every *.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 | — |
package | every 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 gate | d5eaaa79 "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 leftz2ui5_cl_demo_app_038and_378as empty stubs and reverted a catalog class to an older state. Split long literals into&&chunks. Gated bycheck:abapgit(linelen); abaplint'sline_lengthrule is off in this repository and would not coversrc/00andsrc/99anyway. For scale: the longest line insrc/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 butnpx @abap2ui5/linteris covered too. Note what that does not replace — the linter reads app classes and views, not.clas.xmlsidecars orsrc/00andsrc/99, socheck:abapgitremains 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 withVALUE #( 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 overviewwould 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-stack92cb8a4— the.ddls.xmlfiles carried<DD02BQ><STRUCOBJN>; the DDLS serializer reads<DDLS>withDDLNAME,DDLANGUAGE,DDTEXTandSOURCE_TYPE(Wfor 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-stack0a0aba6— the.bdef.xmlfiles carried<MAIN>; the BDEF serializer reads<BDEF>(theMETADATAsubstructure ofCL_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.xmlto 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 toz2ui5_cl_xml_view.clas.xmlin one go, because the block had been missing while the class kept growing. Add a component to a class whose sidecar has aDESCRIPTIONSblock 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
| Trap | Rule |
class_constructor must be in the PUBLIC SECTION | ABAP 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 call | CREATE 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 it | result = 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 directly | The 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 in | SUPER-> 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 class | Field "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 SECTION | The 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 datacannot be dereferenced inline.lr_ref->*in an expression, andASSIGN 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 ina16e2465(#1923) and75bf5515(#1857). Assign it to a field symbol first, then work on the field symbol:
abaplint reported this only as a source. From 2.120.67 on (abaplint/abaplint#4364)ASSIGN lr_ref->* TO FIELD-SYMBOL(<val>). ASSIGN COMPONENT lv_name OF STRUCTURE <val> TO FIELD-SYMBOL(<comp>).check_syntaxalso 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 …wherevalisTYPE anyis 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 addresslx->(lv_name).check_syntaxreports it below v756 from 2.120.72 on. CORRESPONDING #( <generic> )is not available. Same commit (a16e2465): useCLEAR+MOVE-CORRESPONDING.check_syntaxreports a generic target below v756 from 2.120.72 on. abaplint'sprefer_correspondingrule 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 pulledabap2UI5/samples-controlsmain (2026-08-17):z2ui5_cl_smpc_app_241'smodel_initcarried 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_syntaxon, control probe fired) — its VALUE grammar does not model the one-assignment rule. Gate:abap2ui5lint—value-header-default-reassigned(2026-08-30), which follows thesource-line-too-longprecedent: for a consumer whose only gate isnpx @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_syntaxreports the row's second assignment as "Duplicate field assignment", so on that pin the construct no longer reachesmainthrough 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 pulledabap2UI5/samples-controlsmain (2026-09-13), same route as theVALUEheader case above: two_event( )wires,z2ui5_cl_smpc_app_136'stoggleandz2ui5_cl_smpc_app_588'sbeforeNavigate, had lost the blank between theargliteral ands_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_errorsince 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 runsparser_error: true, so the stopgappattern-lintruleliteral-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 = 2is 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 dynamicWHERE (cond)with the OR is a runtime error that noCATCH cx_rootcatches. abaplint 2.120.64 accepts the static form (check_syntaxon, controlUSING KEY nokeyfired): its LOOP syntax resolves the key and checksIS INITIALbefore 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 incheck_syntax, from the release after 2.120.64 on. An OR in parentheses, a dynamic key name andprimary_keystay 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_syntaxreports a built-in function with a character-like result beforeIS 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 INITIALandIF session_command_text( lv_cmd ) IS NOT INITIALin 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 pullingabap2UI5/samplesmain (2026-08-17):z2ui5_cl_smp_app_348carried it in both of its SELECTs; fixed by declaring the tables withDATA … TYPE STANDARD TABLE OF … WITH EMPTY KEYand selectingINTO CORRESPONDING FIELDS OF TABLE @lt_…(samples0d082a3). abaplint stays green because its SELECT grammar puts no version gate on the inline declaration — measured on 2.120.24 withcheck_syntaxanddownporton atsyntax.versionv750: zero findings, control probe fired. PlainINTO TABLE @DATA(…)is fine from 7.40 on; it is only the combination withCORRESPONDINGthat is late. Gate:abap2ui5lint—into-corresponding-inline-decl(2026-08-30). Upstream from abaplint 2.120.65 on (abaplint/abaplint#4354):check_syntaxreports 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 ordinaryABSTRACT FINALclass that happens to contain a subclass ofcl_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 inCX_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: trueis DDIC-CDS only. In a view entity it fails activation with "Annotation Semantics.currencyCode is not allowed in view entities". ACUKYfield derives the marker by itself, and the@Semantics.amount.currencyCodereferences are what establish the currency reference.f07cba4.- A draft table mirrors the CDS element names, not the persistent table's
field names. The BDEF
mappingapplies 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, notDAY. A prefixed name is never inTRESE. - Rename the internal types that mirror the columns, too. A
SELECT … INTO CORRESPONDING FIELDSinto a structure that still saysdaystays 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, VALUEA 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.
defaultandoptionalafter a parameter withoutOPTIONALorDEFAULT,preferred,exporting,changing,returning,raisingandexceptionsdepending on the section - measured on 758 SP03 for abaplint/abaplint#4391, whose description has the full table.!defaultis accepted everywhere, but a different name is the fix that no reader trips over. A structure component may carry these names: aBEGIN OF … END OFhas 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
SIGNandOPTION. Any structure with the componentssign/option/low/highis a selection structure to the syntax check, and aVALUErow 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 oftest_token_odd_optioninz2ui5_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 inabaplint.jsoncsince the 2.120.75 pin; it replacedcheck:atc'srange_row. Only literals are judged, and a header default (VALUE #( sign =Ioption =EQ( low = … ) )) counts. LOOP AT … WHEREover a standard table is a sequential read and wants"#EC CI_SORTSEQon 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 theLOOP AT … WHEREthe gate started with:READ TABLE … WITH KEY(notWITH 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 isparentonz2ui5_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
CATCHblock 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_structurehas the optioncatch, off by default, that reports an empty CATCH block without##NO_HANDLERor"#EC NO_HANDLER. A comment in the block does not count, as SLIN does not count it either. It is on inabaplint.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, andSORTwithoutBY,COLLECTandDELETE ADJACENT DUPLICATESuse it unasked. abaplint'savoid_use.defaultKeyreports only the spelled-out form; the implicit one isfully_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-controlsapp 034 shipped one and the corpus grew a regex for it. WriteWITH EMPTY KEY, or the key you mean. 31 sites in this repository (2026-09-19), all in the vendoredsrc/00/01code (noIssues) and the frozensrc/99(not linted byabaplint.jsonc). FIND/REPLACE … REGEXis POSIX, which is deprecated.FIND PCREonly 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'sobsolete_statement(optionregex, on by default) reports it only fromsyntax.versionv756 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: firstz2ui5_if_client=>cs_nav_modehere, thencs_statusinabap2UI5/samples-stack(7459f39), then five findings on samples-stack's overview app from a user's system (2026-08-17) — two blocks beforeCONSTANTS:, 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 theMETHODSit documents, detaches it (bit us onz2ui5_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( )'spathhas no!. That third recurrence turned it into a gate: decided here bycheck:atc, in samples-stack by its ownnpm 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 carriescs_nav_modeas its bad example. So it is the linter's finding here now:wrong_abapdoc_positionis on inabaplint.jsoncsince the 2.120.51 pin, andcheck:atckept exactly one shape the rule does not decide — a block directly beforeEND OF, measured against that version. The plain comment between block and declaration wascheck: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-stackstill runs its owncheck: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-runtimepins 2.120.64 and still shipped a"!between the parameters ofz2ui5_cl_agent_audit=>read, reported by a user's syntax check (2026-10-08) - itsabaplint.jsonclists its rules one by one andwrong_abapdoc_positionwas 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:atcasserted the opposite until that day and then reported the shape itself, untilwrong_abapdoc_positiontook 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,@raisingand@exceptionmay 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 inabaplint.jsoncsince 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 iswrong_abapdoc_position's first position, and it reports exactly those four once switched on.- A blank line, inside the block or between the block and its
declaration, detaches it like a comment does. Found in
abap-cloud-gui (
- 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<wa>. AGENTS.md carried the rule as prose, and three of them still shipped inz2ui5_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 inabaplint.jsonc; thecheck:atcpart is gone. It reports a tag-like token outside the tags ABAP Doc knows (p em strong ul ol li h1-h3 br); a comparisona < bis not a token and not a finding. PREFERRED PARAMETERis ignored unless every IMPORTING parameter is optional. The addition names the parameter a short-form callmeth( 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_internalhad one importing parameter and no addition until #2719 gave it a second, defaulted one and addedPREFERRED PARAMETER valto keep its positional callers reading the same. They read the same without it, so the addition is gone rather thanvalmade 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 (
MethodDefImportingacceptsPREFERRED 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 abaplintgreen, 0 issues in 264 files. Now gated bycheck:atc(preferred_param), which reads the IMPORTING block of everyMETHODSstatement that carries the addition and reports the first parameter with neitherOPTIONALnorDEFAULT. Two boundaries:WITH DEFAULT KEYis a table type and not a parameter default, and a chainedMETHODS: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 insrc/are correct ones it stays silent on (measured by inverting the rule - it reads all seven, the escaped!previousform 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 )withvalmandatory 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 rulepreferred_parameter_ignoredreports the declaration, and its quick fix declares the mandatory importing parametersOPTIONAL. It is a rule, so it reports only where a project switches it on - here it is on inabaplint.jsonc, which leavessrc/99out, socheck:atckeeps reading all ofsrc.
Not gated — a script cannot decide these:
DATA( )from a packed computation.DATA(margin) = order-requireddate - order-shippeddate.with both operandsp LENGTH 8 DECIMALS 0is 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. ACOND #( … THEN packed_field … )takes its type from the operand and is not this. Found by a Code Inspector SYNTAX_CHECK over a pulledabap2UI5/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 ofabap2UI5-addons/admin-cockpitreported the same warning forz2ui5_cl_cockpit_stats->p95:DATA(lv_target) = lv_total * 95 / 100.withlv_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 declaringlv_targetwith the operand's type. Gated by abaplint —inline_packed_computation(abaplint/abaplint#4413), both shapes, on inabaplint.jsoncsince the 2.120.75 pin.SORTorDELETE ADJACENT DUPLICATESon a table with an empty key.SORT result.andDELETE ADJACENT DUPLICATES FROM result.on aSTANDARD TABLE OF string WITH EMPTY KEYgive 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 noBYorCOMPARINGis given. With an empty key,SORTdoes nothing andDELETE ADJACENT DUPLICATEScompares nothing. WriteSORT result BY table_line.andDELETE ADJACENT DUPLICATES FROM result COMPARING table_line., or the components you mean. Found by a user's pull ofabap2UI5-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 inabaplint.jsoncsince the 2.120.75 pin.DATA( )from a generic parameter (DATA(lv) = valwithval 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 inz2ui5_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, useCLEANUPinstead 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 outerCATCH cx_rootis fine (z2ui5_cl_ui5_srv_model=>main_json_to_attri). SELECTwithout aWHEREclause wants"#EC CI_NOWHERE(z2ui5_cl_ui5_srv_draft=>count_entries_total,43515c97). Whether the missingWHEREis 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_entriesnext to it is owner-scoped and needs no pragma.CREATE OBJECT … TYPE (name)into a generic reference, thenCAST, 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 inCONV i( … )when the assignment target is alreadyTYPE 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'sslider_value = CONV i( client->get_event_arg( ) )inabap2UI5/samples-controls. abaplint'svalue_conversion/unnecessary_pragmado 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 declaresTYPE i— aDATA/CLASS-DATAline or a typed parameter. That isabap2ui5lint'sredundant-conv-i— promoted out ofsamples-controls'scripts/pattern-lint.mjson 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 reportedz2ui5_cl_ui5_util_context->data_render_struc @16— "Redundant conversion for type STRING" onDATA(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, andrtti_get_t_attri_by_any'sCONV string( lo_struct->absolute_name ). All four are now a declaredDATA … 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 redundantCONV 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 )withurl TYPE clikeis 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 insrc. 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 insrcstill have the flagged shape and were left alone because no system run has named them:z2ui5_cl_ui5_util_http'sCONV 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_conversionreports 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 inabaplint.jsoncsince this fix — as a floor, not as the gate for this finding. What would decide this one isredundant-conv-i's shape generalized fromito 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-cockpitshippedDATA(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_conversionreports exactly this line (measured on 2.120.64, the version the cockpit pins), but the cockpit'sabaplint.jsoncdid 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", andredundant_conversiondid 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 typedstring— the view builder'sv, for one:'...'(001) is not type-compatible with formal parameter "V", a SYNTAX_ERROR of the whole class (bit us onabap2UI5/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 bycheck:atc. Upstream from abaplint 2.120.65 on (abaplint/abaplint#4358):check_syntaxreports a text symbol bound to an importing method parameter typedstring, named and positional;PERFORM … USINGandCREATE OBJECT … EXPORTINGare 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 — bareWHERE id = \TEST_COUNT_FOREIGN`becomesWHERE id = @( `TEST_COUNT_FOREIGN` ). Fixed by a user on a real system (2026-08-23, abap2UI5#2657) inz2ui5_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 xis the old spelling; writex = REF #( … ). Same pull request, same reason — it is not released for ABAP Cloud, and nothing on this side reports it:check:cloudand 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 symbolTYPE anyafter a dynamic ASSIGN, which has no type to infer from. WriteREF data( … )(or the concreteREF 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_syntaxon, default and cloud configs). Upstream from abaplint 2.120.65 on (abaplint/abaplint#4355):check_syntaxreportsREF #( )into a field symbol typedanyordata. The last one outside the upstream mirrors and the frozen package -conv_get_as_data_refinsrc/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'sFOR ENTITY EVENT ... FOR z2ui5_r_smps_tck~TicketCreateddraws "The alias Ticket from the behavior definition should be used instead" — twice onz2ui5_cl_smps_evt_tck(abap2UI5/samples-stack, found on a user's system 2026-08-17). WriteFOR ticket~TicketCreated. abaplint has no grammar forFOR ENTITY EVENT(section 6), so no rule can reach it. - A draft
Activatewantsoptimized. A plaindraft 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 haddraft 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<,>,&.
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 undernpm run unit-ABCis the fixedsy-sysidof@abaplint/runtime, so the guard is a Node skip in disguise, and one thatabap_transpile.jsondoes not list: the suite reports the method as run. Nine tests insrc/99sat 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 throughsy-sysid. The same inventory found five runtime gaps that each keep one test off Node - a lower-case dynamic type name inCREATE DATA … TYPE (name), anASSIGN COMPONENTname with a dash in it,cl_abap_refdescr=>create( )withoutkind,cl_abap_tstmp=>add( )dropping the fraction of atimestampl, anddescribe_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/runtime2.14.5, open-abap-core9c1271b), and the tests they kept off Node run. Two in-code probes were dead:CREATE OBJECT TYPE ('CL_ABAP_ZIP')anddescribe_by_name( 'DFIES' )both succeed in open-abap-core, so the tests behind them run in full. - A
timestamplfromGET TIME STAMPis noisy in the runtime, and on a:00second it is not a time.@abaplint/runtimefills the long stamp by adding the milliseconds as a JS double to a 21-digit value, and the copy of a shorttimestampinto atimestampltakes 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: then20261014103400arrives as…103399.9990784, the ISO text reads10:33:99, andcl_abap_tstmp=>add( )/subtractsecs( )die with a JSRangeError: Invalid time valuethat noCATCHsees. It cost the release of 1.147.0 its first backend-prebuilt run (2026-10-14,npm run unitat 05:34:00 UTC inzcl_serializer_live'stest_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 thantime_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/runtime2.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 toREAD TABLE mt_names WITH KEY table_line = to_upper( is_node-name ), carrying the call over verbatim, and aWITH KEYoperand 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 ofname( … )— a functional method call — and the class pool dies withSYNTAX_ERROR, "method TO_UPPER is unknown". That class isz2ui5_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 aWITH [TABLE] KEYyou write yourself and for an internal-tableWHERE. 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 hasREAD 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) lowersline_exists( tab[ k = to_upper( x ) ] )intotemp1 = to_upper( x ).plusREAD TABLE tab WITH KEY k = temp1, so the table-expression shape is safe at the version this repository builds with - measured on it, andcheck:downportno longer reports that shape. Gate:npm run check:downportoversrc/keeps the two positions an author writes 7.02-ready themselves, which the downport passes through as they stand: aWITH [TABLE] KEYoperand and an internal-tableWHERE. abaplint'scheck_syntaxreports theREAD TABLE … WITH [TABLE] KEYone below 7.40 as well (abaplint/abaplint#4432, merged 2026-10-12, from this catalogue) from 2.120.76 on, pinned here; theWHEREpositions and the predicate operand stay this gate's alone. A third position, same cause: the operand of a predicate expression.condense( val ) IS INITIALanswersUnexpected 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 inz2ui5_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.yamlproduces therename_<name>branches for a consumer who needs a different namespace, by runningabaplint --renamewith^z2ui5(.*)$→<namespace>$1. The placeholder the PR gate renames to,znamespace, is the worst case the workflow allows — 10 characters, five more thanz2ui5— so an object name of 27 characters becomes 32 and ABAP's 30-character limit refuses it. abaplint stops the whole rename with a bareError: Name not allowedand 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 insrc/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 onz2ui5_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 toz2ui5_if_ui5_serializer/z2ui5_cl_ui5_serializer(23). Note what did not see it —check:namingreads the namespace segment and not the length,check:abapgitchecks that<CLSNAME>matches the file name and not how long either is, and abaplint oversrc/is green because the name is legal until it is renamed. Gate:npm run rename, whichabaplint.yamlruns 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 TABLEis valid at v750; the downport lowers the operand-positionREF #( )intoDATA temp17 LIKE REF TO <tab>.plusGET 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_bindandz2ui5_cl_ui5_srv_model(2026-09-23). Every check was green: the source is valid, the transpiled suite runs v750, and abaplint's v702check_syntaxover the downported tree reports 0 issues on that exact line (measured on 2.120.52); from 2.120.66 on (abaplint/abaplint#4363) it reportsLIKE REF TOa generically typed table, the shape a 7.58 system refuses as well. Assign to a typed variable first -lr_tab = REF #( <tab> ).withlr_tab TYPE REF TO data, thenbind( lr_tab ); the downport turns that into a plainGET 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 theLIKE REF TO; from 2.120.72 on (abaplint/abaplint#4405, pinned here) it writesTYPE REF TO datafor a generic source, and the gate stays as a guard. Gate:npm run downportends withdownport-fix.mjs check-generic-like, which reads the OUTPUT (the only place the shape exists) and fails on anyDATA … LIKE [REF TO | LINE OF]a field symbol the same method types generically;test.yamlruns 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'sdefinitions_topinferTYPE name, which is no DDIC type at v702, andauto_downportexited 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 variableTYPE c LENGTH <n>(measured 2026-10-09 on ac LENGTH 30component+9:LENGTH 21), so this is history on the current pin. - Do not combine an inline declaration with a table-typed
VALUEconstructor.DATA(lt_in) = VALUE STANDARD TABLE OF …is aparser_errorin theabap_cloudandabap_standardconfigs. Declare withDATA, then assign (78d4731f, #2128). NEWbuilds objects,CREATE DATAbuilds data. The downport lowers everyNEWintoCREATE OBJECT, also when the target is a data reference.mr = NEW #( `x` )withmr TYPE REF TO string, ormr = NEW string( `x` )withmr TYPE REF TO data, comes out asCREATE OBJECT mr TYPE string ClassDefinitionNotFound ERROR., which is no statement at all: aparser_errorin the downported tree. Without a value (NEW #( ),NEW string( )) it becomes a plainCREATE OBJECT mr, whichcheck_syntaxrefuses 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 ofz2ui5_cl_ui5_serializer(2026-10-09). WriteCREATE DATA mr.(orCREATE DATA mr TYPE string.) and assign throughmr->*. Measured on abaplint 2.120.68 in an isolated project (downport,check_syntaxandparser_erroron, v702; still so on 2.120.71, 2026-10-09). From 2.120.72 on (abaplint/abaplint#4404, pinned here) the downport writesCREATE DATAand the value through the reference. Gate: this repo -npm run downportends 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 … ERRORabove 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 aparser_error, andxsdbool( 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 ofxsdbooland is not one: on its own,xsdbool( a = b )downports toboolc( a = b )in every position measured (an assignment, aRETURNINGvalue, 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, andxsdbool( a = b )parses as a method call. So aNEWfurther down the file is fixed first, and thexsdboolabove it is left behind. A test assertion ofz2ui5_cl_ui5_serializerwas rewritten into anIFfor that reason (2026-10-09) and put back once this was measured. Fix the firstparser_errorin 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 afterVALUE #( ( name = 'A' t_sub = lt_x ) ( name = 'B' ) )row B carries A'sT_SUB- on the 702 branch, and innpm run unit, which transpiles the downport output. Scalars, structures, an empty row( )andBASEleak 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 fromsrc/, 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): aCLEARplus 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
LETin aVALUEwithoutFOR, or in aCONV, does not survive a transpiler before 2.13.98 when it reads 7.40 source: the binding is never declared and the statement dies withReferenceError: s is not definedat runtime, not at transpile time.VALUEwithFOR,CONDandREDUCEare fine. This tree never meets it -npm run unittranspiles the downport, which outlines theLETfirst - but a host transpiling its own 7.40 classes against@abap2ui5/node-runtimedoes. Outline the binding into aDATAthere, or transpile with 2.13.98 or later (abaplint/transpiler#1961). The same release sortsSORT 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 pbreaks the class in the transpiled runtime. The transpiler has no type for it and emitsnew 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 withTypeError: … is not a constructor, and so does describing the class, which abap2UI5 does to every app (rtti_get_t_attri_by_oref);CATCH cx_rootdoes not see a JSTypeError. A FORM parameterTYPE pand a method parameterTYPE numericare fine. Found 2026-10-03 by report2cloud, whose FORMs become methods; type the parameterTYPE p LENGTH … DECIMALS …,LIKEthe actual, orTYPE numeric. Nothing in the four checkouts has one. Fixed from@abaplint/transpiler-cli2.14.0 on (abaplint/transpiler#1971, pinned here since 2.14.2): the parameter becomes aPacked. 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 functionat runtime, the transpile is green. Fixed from@abaplint/runtime2.14.4 on (abaplint/transpiler#1997, pinned here).round( )withdec,precand everymodeis in from 2.13.94 on (2.13.93 throwsround(), todo, handle decimalsfor anydecother than 0 - still the version the MCP server's Node backend installs). On an older runtime useround( )where the scale of the result does not matter.substring_after( )/substring_before( )ignoreocc,lenandcasein the runtime.substring_after( val = path sub = '-' occ = -1 )cuts at the FIRST dash undernpm 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 fromfind( val = … sub = … occ = -1 ), which the runtime does resolve, and cut withsubstring( ). Fixed in the runtime from 2.14.3 on (abaplint/transpiler#1985, pinned here).REPLACE ... OF ... WITHreads its WITH text like a regex replacement in the runtime.REPLACE ALL OCCURRENCES OF '{' IN s WITH '\{'leaves{undernpm 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-inreplace( 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 withreplace( … occ = 0 )(abap2UI5'sescape_literaldoes), keep$out of literal WITH texts. Fixed in the runtime from 2.14.3 on (abaplint/transpiler#1987, pinned here).- A
line_exists( )in anELSEIFis moved in front of theIFby the downport - as aREAD TABLE … TRANSPORTING NO FIELDS, which setssy-subrc. AnIF sy-subrc <> 0.after an earlierREADthen tests the wrong READ on the 702 branch and innpm 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 aline_exists( )/line_index( )in anELSEIF. Fixed in the downport from abaplint 2.120.72 on (abaplint/abaplint#4401, pinned here): it saves and restoressy-subrcandsy-tabixaround the inserted READ. - A time or date assigned to a number keeps its digits in the runtime.
lv_secs = lv_time.(tintoi) is the seconds since midnight on a system, and235930undernpm run unit- the same forint8,pandf, and for adsource (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: adsource counts the days since 01.01.0001 from@abaplint/runtime2.14.2 on (abaplint/transpiler#1977), atsource 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 undernpm 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, butlv_b = lv_a.copies it through a JS double, so20240229235959becomes20240229235958.9986304and...123000becomes...122999.9988736- only values the double happens to hold exactly, a whole hour among them, survive. A system copies the digits. ajson'sto_timestamplreturns its result by such a copy, so itsto_timestampfinds a fraction in|{ lv_timestampl }|and raises Unexpected timestamp format:get_timestamp( )answers 0, and a bound table with aTIMESTAMPcolumn holding23:59:59is refused whole on its way back in (to_abap) undernpm run unitand in the node runtime. Found 2026-10-08 by the date round trip ofz2ui5_cl_ui5_srv_model's test class, which uses a whole hour for that reason. Measured on@abaplint/runtime2.13.99 and 2.14.0 (Packed.set( )hands anotherPackedtothis.set(value.get()), andget( )is aNumber). 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( );CPused as a containment test →CS;IS NOT INITIALon an internal table →lines( … ) > 0; a local class type inCREATE DATA … TYPE→LIKE. - An ABAP name that is a reserved JS word used to break the transpiler. The
importing parameter
withofc_replace_allwas emitted aslet with = …, illegal in strict mode, and the unit job went red. The fix at the time was akeywordsentry innode/setup/abap_transpile.jsonrather 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'sDEFAULT_KEYWORDS, not in a per-project option here. - A
PARTIALLY IMPLEMENTEDinterface in a test double gets no stubs from the transpiler. On a system, calling an interface method the double does not implement raisesCX_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 aTypeError: ... is not a functioninside the method under test instead of that exception (abaplint/transpiler#2024 makes it the exception). Found the day the MCP server'srun_unit_testsfirst ran app-template's starter test in the transpiled backend (2026-09-19): itsltd_clientimplemented the six lifecycle and output methods, andview_display( )calledcheck_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 (_bindmay answer a fixed token,_eventits name), and keepPARTIALLY IMPLEMENTEDonly 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 failingrun_unit_testsis 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_syntaxresolvescl_abap_unit_assertagainst open-abap-core, which has both);EXPORTING/IMPORTING/CHANGINGin a chained calla( )->b( CHANGING … )(the v702 parser accepts it);TYPE int8(7.50); a built-in function as theINSERToperand (read as a method call); a line over 255 characters (the downport joins the operands of a multi-line&&insideVALUE #( )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.mjsnow closes the chain every 80 operands). Gate: this repo — the downport rewritesassert_true/false( x )intoassert_equals( exp = abap_true/abap_false act = x )(assert-boolinnode/setup/downport-fix.mjs, main keeps the idiomatic call), andcheck-702in the same script failsnpm run downporton the other four read from the OUTPUT. Insrc/: assign the object to a variable before a call withCHANGING,TYPE p LENGTH 16 DECIMALS 0for a big integer, the function result to a variable beforeINSERT, a long&&chain as a plain statement before theVALUE #( ). 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 raisedGETWA_NOT_ASSIGNEDincheck_alias_capable;substring( )does not. No script decides that one - read for it where a->*component is passed on. xsdbool, neverboolc— the downport convertsxsdbooltoboolcautomatically, so writingboolcyourself breaks in the other direction. abaplint —prefer_xsdbooldecides it (measured on 2.120.52).- Not every released class is released in ABAP Cloud.
CAST cl_abap_elemdescr( … )->get_ddic_field( )is not, andz2ui5_cl_pop_tablehad to derive the label fromabsolute_nameplus a context helper instead (bc230abe). Framework code reaches system functionality only throughz2ui5_cl_ui5_util_contextfor exactly this reason. - What the downport cannot rewrite - written 7.02-ready in the source.
Giving 15 addons a
702branch (2026-11-02, a publish-702 workflow in each) turned up constructsabaplint --fixleaves 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 ofIS INSTANCE OF(one of these blocks the whole class,Expected ENDMETHOD); anORDER BYin a SELECT that has a WHERE andUP TO;SELECT SINGLE * FROMinstead ofSELECT SINGLE FROM ... FIELDS *; a row loop instead ofMOVE-CORRESPONDING/CORRESPONDING #( )between tables; an assignment plus CLEAR instead ofCORRESPONDING #( ... EXCEPT ); READ TABLE ... ASSIGNING instead ofCLEAR itab[ ... ]-comp;WITH DEFAULT KEYin a dynamicCREATE DATA; a declared work area instead ofINSERT 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 v702check_syntaxonly says"DATA" not found. The cast forCX_SY_ITAB_LINE_NOT_FOUNDthesyfixesscript patches is upstream already (abaplint/abaplint#3735, #3240), andsyfixesonly runs when the--fixcall 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_rootdoes not stop it. A class with a syntax error that is loaded at run time ends the request with the runtime errorSYNTAX_ERROR, a short dump. NoTRYaround 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) leftz2ui5_cl_cockpit_setupwith 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 betterCATCH. The framework no longer calls a monitor unless the installation switches it on (check_monitor_activein the user exit'sset_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_tstmpraised on the system and ran under the transpiler.z2ui5_cl_cockpit_setup=>now_minus_seconds(abap2UI5-addons/admin-cockpit) calledcl_abap_tstmp=>subtractsecs( tstmp = now( ) secs = seconds ), wherenow( )is a method returningtimestampl. 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'scl_abap_tstmptakes 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 typedtimestampl. The fix leaves the class out. It converts withCONVERT 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). Treatcl_abap_tstmplike any API whose type checks only a system runs. If you call it, pass a variable typed exactlytimestamportimestampl, and test on a system, not only under the transpiler. Gate: open. A rule would need the system's own type check ofcl_abap_tstmp, which nobody has measured yet.After
ASSIGN, checkIS ASSIGNED— notsy-subrc. A 7.40 SP7 system ran every abap2UI5 app into an endless loop becausesy-subrcwas still4from an earlierREAD TABLEwhenattri_get_val_reftested it: the dynamicASSIGN (lv_name)sat in a branch that had not been taken, and on that release a successfulASSIGNdid 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-subrcthan the release range this repository ships to, so no test here can reproduce it.But
IS ASSIGNEDis not a blind replacement. A FAILEDASSIGNleaves the field symbol bound to whatever it pointed at before — verified in@abaplint/runtime'sassign, where every failure path setssy-subrc = 4and returns without clearing the target. Inside aLOOP, or anywhere the same symbol was already assigned,IS ASSIGNEDtherefore reads TRUE for a failure, which is worse than the bug it replaces.UNASSIGN <fs>.before theASSIGNwhere 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 theUNASSIGN. 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 inabaplint.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 frozensrc/99, 5 in the mirrorssrc/00/01andsrc/00/02), and none in the packages the ruleset reports on. TheUNASSIGNhalf stays a review point — the rule reports the test, not the missingUNASSIGN.- An object name inside a string literal is not a reference — a rename sweep
will get it wrong and nothing will notice.
#2564renamedz2ui5_cl_exittoz2ui5_cl_ui5_user_exitand carried the rename into the interface literal ofget_user_exit_class, which becameZ2UI5_IF_USER_EXIT— an interface that does not exist; the shipped one isZ2UI5_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 undernpm 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 bynpm run check:dynamic(.github/scripts/dynamic-name-gate.mjs): everyZ2UI5_*name in a string literal must exist undersrc/, with names owned by other repositories listed inEXTERNALwith their reason. After any rename, reread the string literals — and when adding a dynamic lookup for something outside this repository, put it inEXTERNALrather 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 arename_<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.andz2ui5_cl_ui5_util_context=>rtti_get_ref_type_name( li_app )— which the rename rewrites. Gated bynpm run check:rename(.github/scripts/rename-literal-gate.mjs, production code ofsrc/00–src/02), andnpm run renamere-runs it over the renamed tree. - Never "modernize"
WITH DEFAULT KEYtoWITH EMPTY KEYon a table passed to a classic function module. The key is part of the table type; an incompatible one makesCALL FUNCTIONfail at runtime, silently when it sits inside aTRY … CATCH/EXCEPTIONSguard. Concrete case and the reason it must stay: the comment abovelt_implinz2ui5_cl_ui5_util_context=>rtti_get_classes_intf_std, which passes the table toSEO_INTERFACE_IMPLEM_GET_ALL— withEMPTY KEYit 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
FAILEDis neither evaluated nor rolled back marks the transaction for abortion, and every later statement in the same request aborts — the request ends inCX_SADL_DUMP_APPL_MODEL_ERRORand the user sees the SAP 500 page, far away from the statement that caused it.abap2UI5/samples-stackf7f0a02: aDiscardissued for instances that had no draft, deliberately ignoringFAILED, took down the whole data regeneration. Read first, act only on what exists, evaluateFAILED, andROLLBACK ENTITIESwhen it is filled. - RAP action derived types have
%key,%is_draftand%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 theEXECUTEstatements were flagged and theREAD ENTITIESin the same class was not.cebe6bd. DELETE itab INDEX sy-tabixinside aLOOP ATover the SAME table deletes whatever rowsy-tabixnames when the statement runs. The loop handles the deletion of its current row - the next pass is the row after it - butsy-tabixis the loop's index only until another statement in the body writes it: an innerLOOP,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-tabixis positive, nothing dumps: the statement deletes another row and the result looks plausible and is WRONG. Where it is 0 - an innerLOOPthat found nothing - index 0 is a short dump, andTABLE_INVALID_INDEXis the ABAP runtime error's own name for it. (DOsetssy-index, notsy-tabix.) When nothing in the body writessy-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
listCloseround-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/runtime2.13.59'sdeleteInternalraises for index 0 and setssy-subrc = 4for 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 inabap2UI5/samples(z2ui5_cl_smp_context'sfilter_itabanditab_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: infilter_itabthesy-tabixbelonged 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-tabixright after aREAD TABLEis 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 inabaplint.jsoncsince 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 undersrc/01/03/, where abaplint's7bit_asciirule 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 - andto_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 innpm run unitand threez2ui5_cl_ui5_clienttests 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_attrialready did, the delta path now does. From runtime 2.14.3 on (abaplint/transpiler#1986, pinned here) the transpiled runtime clears the target too, sonpm run unitshows it. ASSIGN dref->* TO <fs>with a typed field symbol is a runtime error, not sy-subrc 4. A sorted table assigned to aTYPE STANDARD TABLEfield symbol dumps withASSIGN_TYPE_CONFLICTon a system; the transpiler answers sy-subrc 4, andtest_skip_sorted_tablewas 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-protectedThat 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
INTOa work area, call with the work area, thenMODIFY 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 inabaplint.jsoncsince 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.ABCis the system ID of the Node runtime, so the guard makes the method a silent no-op innpm run unitwhile 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 theskiplist ofnode/setup/abap_transpile.jsonwith a note naming the missing runtime capability; the runner then prints it as skipped instead of pretending it passed. (The remainingsy-sysidguards are all in the frozensrc/99— they are history, not precedent.) - Generated code is not editable — fix the generator.
src/01/03/comes fromnpm run app2abapand a manual fix there is reverted by the next generation, withcheck:app2abapfailing the PR. The same trap outside this repository, without a gate to catch it: inabap2UI5/samples-controls#38an over-length statement was first split by hand in a file produced byscripts/generate-overview.mjs, and the split had to be moved into the generator in a follow-up commit. Before editing anysrc/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>orMETHODS … FOR ENTITY EVENT, so the class pool does not parse and every finding on the file — the structure error, the parser errors, thecheck_syntaxfollow-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_baseis on-premise only and absent fromsteampunk-2305-api, which made the WebSocket handler and its callers report unknown references.abap2UI5/samples-stack468af3dsorted 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, andglobal.noIssuesonly when the error comes from the parser itself and no rule-levelexcludecan reach it. Scope it to the.abapfiles so the.clas.xmlkeeps itsobject_namingandxml_consistencycoverage.
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, thenIF <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-tabixafter 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 SEARCHandDELETE ADJACENT DUPLICATES … COMPARINGon 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 raisesCX_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 (abap2UI57647c26). - 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 … EXCEPTIONSwithoutOTHERS(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 passprevious = 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_xas 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 JOINon 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 insideSELECT … ENDSELECTor 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) insideLOOP … WHEREorFROM/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 … MESSAGEfor a class without IF_T100_MESSAGE.RAISE EXCEPTIONorTHROWin a method or function module that declares classicEXCEPTIONS- 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, sox+6(2)does not activate (abaplint types some of these asd). - 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 KEYa 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:
| Check | Here |
| Empty PROTECTED/PRIVATE SECTION removed | no — AGENTS.md keeps all three sections |
| CATCH cx_root avoided | no — AGENTS.md catches cx_root and re-raises it with previous |
| OPTIONAL and boolean IMPORTING parameters avoided | no 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/FIXME | yes |
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 method | yes |
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, CONDENSE | yes — the downport expands += for 7.02; to_upper( ) and condense( ) exist on 7.02 already |
FINAL( ) for inline declarations never changed | not yet — 7.57; check that the downport handles it first |
A table expression / VALUE #( … OPTIONAL ) instead of READ TABLE + sy-subrc | yes, where not-found is skipped; the exception differs otherwise |
| SORT … BY with ASCENDING/DESCENDING stated | optional |
CLEAR right after DATA, a needless me-> | yes |
| Classes of only static methods, interfaces of only constants, public methods outside an interface | judge per case |
| Class size (methods, attributes per class), statements per WHEN | the method limits in AGENTS.md apply; the rest is a smell |
Magic numbers, sy-uname/sy-sysid compared with a literal, '@00@' icon literals | name them as constants |
| MESSAGE TYPE E/A/X or WRITE in a global class; MESSAGE ID sy-msgid … TYPE sy-msgty | no dialog in classes — raise an exception |
| COLLECT; a table with the implicit default key | avoid (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 uses | remove |
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
.abapfile (check:abapgitcovers 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.
- 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.
- 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.
- 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.
- 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. - If a script cannot decide it, say so in the entry rather than leaving the reader wondering why it is not gated.
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
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.

