From 01dfdee30dd753cee55f376ef10ccbfe02d7f052 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 09:55:42 +0000 Subject: [PATCH 1/5] Check the field symbol, not sy-subrc, after a dynamic ASSIGN On some releases a SUCCESSFUL dynamic `ASSIGN` does not reset `sy-subrc` (abap2UI5 #1937), so `IF sy-subrc = 0` after one can read FALSE for an assignment that worked and TRUE for one that did not. `IS ASSIGNED` is the check that holds on all of them. Eleven sites across eight sample apps tested sy-subrc instead - among them the sub-app view handover (`MV_VIEW_DISPLAY`, `VIEW_PARENT`), which is exactly the failure the comment above one of them already describes for a different cause. Two need more than the drop-in: - app_502's node builder assigns inside `DO 6 TIMES`. A failed assign leaves the previous round's binding in place, so `IS ASSIGNED` would read TRUE for the failure and build onto the wrong node - `UNASSIGN` first. - app_212 dereferenced `mt_table->*` and `ms_table_row->*` and then USED both without ever checking: reading `[ index ]` or a component of an unassigned field symbol is a short dump, not a catchable exception, so the popup would take the roundtrip down with it. Both are checked now, and the row structure - which does not change per field - is read once before the loop instead of on every iteration. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UxxNLTJFLpefuUNnRKqC61 --- src/00/98/z2ui5_cl_smp_app_117.clas.abap | 4 +++- src/00/98/z2ui5_cl_smp_app_131.clas.abap | 4 +++- src/00/98/z2ui5_cl_smp_app_185.clas.abap | 4 +++- src/00/98/z2ui5_cl_smp_app_191.clas.abap | 4 +++- src/00/98/z2ui5_cl_smp_app_195.clas.abap | 4 +++- src/00/98/z2ui5_cl_smp_app_211.clas.abap | 4 +++- src/00/98/z2ui5_cl_smp_app_212.clas.abap | 17 +++++++++++++++-- src/00/98/z2ui5_cl_smp_app_338.clas.abap | 4 +++- src/01/z2ui5_cl_smp_app_104.clas.abap | 6 ++++-- src/01/z2ui5_cl_smp_app_461.clas.abap | 6 ++++-- src/01/z2ui5_cl_smp_app_502.clas.abap | 10 ++++++++-- 11 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/00/98/z2ui5_cl_smp_app_117.clas.abap b/src/00/98/z2ui5_cl_smp_app_117.clas.abap index 003be0f9..dcaf4efb 100644 --- a/src/00/98/z2ui5_cl_smp_app_117.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_117.clas.abap @@ -145,9 +145,11 @@ CLASS z2ui5_cl_smp_app_117 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO . - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/00/98/z2ui5_cl_smp_app_131.clas.abap b/src/00/98/z2ui5_cl_smp_app_131.clas.abap index 1c333807..562af91f 100644 --- a/src/00/98/z2ui5_cl_smp_app_131.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_131.clas.abap @@ -148,9 +148,11 @@ CLASS z2ui5_cl_smp_app_131 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO . - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/00/98/z2ui5_cl_smp_app_185.clas.abap b/src/00/98/z2ui5_cl_smp_app_185.clas.abap index 41fb2980..8fcb9440 100644 --- a/src/00/98/z2ui5_cl_smp_app_185.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_185.clas.abap @@ -132,9 +132,11 @@ CLASS z2ui5_cl_smp_app_185 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO . - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/00/98/z2ui5_cl_smp_app_191.clas.abap b/src/00/98/z2ui5_cl_smp_app_191.clas.abap index 95addf5d..8231f755 100644 --- a/src/00/98/z2ui5_cl_smp_app_191.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_191.clas.abap @@ -133,9 +133,11 @@ CLASS z2ui5_cl_smp_app_191 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO . - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/00/98/z2ui5_cl_smp_app_195.clas.abap b/src/00/98/z2ui5_cl_smp_app_195.clas.abap index 86532ac5..a9ad077d 100644 --- a/src/00/98/z2ui5_cl_smp_app_195.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_195.clas.abap @@ -132,9 +132,11 @@ CLASS z2ui5_cl_smp_app_195 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO . - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/00/98/z2ui5_cl_smp_app_211.clas.abap b/src/00/98/z2ui5_cl_smp_app_211.clas.abap index bbc7581f..58db00e0 100644 --- a/src/00/98/z2ui5_cl_smp_app_211.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_211.clas.abap @@ -159,9 +159,11 @@ CLASS z2ui5_cl_smp_app_211 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO FIELD-SYMBOL(). - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/00/98/z2ui5_cl_smp_app_212.clas.abap b/src/00/98/z2ui5_cl_smp_app_212.clas.abap index c568c9a7..0c188dc9 100644 --- a/src/00/98/z2ui5_cl_smp_app_212.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_212.clas.abap @@ -82,11 +82,25 @@ CLASS z2ui5_cl_smp_app_212 IMPLEMENTATION. FIELD-SYMBOLS TYPE STANDARD TABLE. FIELD-SYMBOLS TYPE any. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937). Both references are also + " checked before they are USED: reading [ ] or a component of an + " UNASSIGNED field symbol is a short dump, not an exception - and the + " popup would take the whole roundtrip down with it ASSIGN mt_table->* TO . + IF IS NOT ASSIGNED. + RETURN. + ENDIF. + + " the row structure does not change per field - read once, before the loop + ASSIGN ms_table_row->* TO . + IF IS NOT ASSIGNED. + RETURN. + ENDIF. ASSIGN [ index ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. + IF IS NOT ASSIGNED. RETURN. ENDIF. @@ -98,7 +112,6 @@ CLASS z2ui5_cl_smp_app_212 IMPLEMENTATION. CONTINUE. ENDIF. - ASSIGN ms_table_row->* TO . ASSIGN COMPONENT lv_field OF STRUCTURE TO FIELD-SYMBOL(). IF sy-subrc <> 0. diff --git a/src/00/98/z2ui5_cl_smp_app_338.clas.abap b/src/00/98/z2ui5_cl_smp_app_338.clas.abap index 780f8bc3..b421f5db 100644 --- a/src/00/98/z2ui5_cl_smp_app_338.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_338.clas.abap @@ -147,9 +147,11 @@ CLASS z2ui5_cl_smp_app_338 IMPLEMENTATION. RETURN. ENDTRY. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN mo_app->(`MV_VIEW_DISPLAY`) TO . - IF sy-subrc = 0 AND = abap_true. + IF IS ASSIGNED AND = abap_true. = abap_false. client->view_display( mo_main_page->stringify( ) ). diff --git a/src/01/z2ui5_cl_smp_app_104.clas.abap b/src/01/z2ui5_cl_smp_app_104.clas.abap index 0e02ee35..9338c09e 100644 --- a/src/01/z2ui5_cl_smp_app_104.clas.abap +++ b/src/01/z2ui5_cl_smp_app_104.clas.abap @@ -44,9 +44,11 @@ CLASS z2ui5_cl_smp_app_104 IMPLEMENTATION. IF app_sub IS BOUND. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN app_sub->(`VIEW_PARENT`) TO FIELD-SYMBOL(). - IF sy-subrc <> 0. + IF IS NOT ASSIGNED. RETURN. ENDIF. @@ -65,7 +67,7 @@ CLASS z2ui5_cl_smp_app_104 IMPLEMENTATION. ASSIGN app_sub->(`VIEW_PARENT`) TO FIELD-SYMBOL(). - IF sy-subrc <> 0. + IF IS NOT ASSIGNED. RETURN. ENDIF. diff --git a/src/01/z2ui5_cl_smp_app_461.clas.abap b/src/01/z2ui5_cl_smp_app_461.clas.abap index 9f8b87e9..ea31db76 100644 --- a/src/01/z2ui5_cl_smp_app_461.clas.abap +++ b/src/01/z2ui5_cl_smp_app_461.clas.abap @@ -81,12 +81,14 @@ CLASS z2ui5_cl_smp_app_461 IMPLEMENTATION. IF lv_from_root = lv_to_root. RETURN. ENDIF. + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN t_nodes[ lv_from_root ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. + IF IS NOT ASSIGNED. RETURN. ENDIF. ASSIGN t_nodes[ lv_to_root ] TO FIELD-SYMBOL(). - IF sy-subrc <> 0. + IF IS NOT ASSIGNED. RETURN. ENDIF. DELETE -nodes INDEX lv_from_child. diff --git a/src/01/z2ui5_cl_smp_app_502.clas.abap b/src/01/z2ui5_cl_smp_app_502.clas.abap index 4cdac6cb..4c5f081f 100644 --- a/src/01/z2ui5_cl_smp_app_502.clas.abap +++ b/src/01/z2ui5_cl_smp_app_502.clas.abap @@ -305,8 +305,10 @@ CLASS z2ui5_cl_smp_app_502 IMPLEMENTATION. " stops at five and writes an ellipsis where it stopped - without the " limit a structure that points at itself would never end DATA(tree) = get_tree( ). + " IS ASSIGNED, not sy-subrc: a SUCCESSFUL dynamic ASSIGN does not reset + " sy-subrc on every release (abap2UI5 #1937) ASSIGN tree->* TO . - IF sy-subrc = 0. + IF IS ASSIGNED. client->message_box_display( ). ENDIF. @@ -701,8 +703,12 @@ CLASS z2ui5_cl_smp_app_502 IMPLEMENTATION. " built from the bottom up, so every node holds the one below it DO 6 TIMES. CREATE DATA result TYPE ty_s_node. + " UNASSIGN before the ASSIGN because this is a loop: a failed assign + " leaves the previous round's binding in place, and IS ASSIGNED would + " then read TRUE for the failure and build onto the WRONG node + UNASSIGN . ASSIGN result->* TO . - IF sy-subrc <> 0. + IF IS NOT ASSIGNED. RETURN. ENDIF. From fbd692a748e65f11eeb1f9f72c0f767cf1130722 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 12:09:44 +0000 Subject: [PATCH 2/5] Say where a SELECT reads a whole table, and gate that it keeps saying it The extended program check (SLIN / ATC) runs in the systems these samples are INSTALLED on and nowhere here - abaplint does not model it. So a sample can pass every gate in this repository and still light up a customer's ATC run, which is worse than a missing sample: it is the code somebody copied because it was published as the way to do the thing. Twenty-one `SELECT` statements read a small demo table with no `WHERE`, which is exactly what they are demonstrating - and none of them said so. The check wants `"#EC CI_NOWHERE` on the statement; the framework's own z2ui5_cl_ui5_srv_draft=>count_entries_total is the precedent, reading the whole draft table on purpose and carrying the pseudo-comment for it. `npm run check:atc` holds it. It decides one finding, the one this repository actually carried, and its scan is line-based on purpose: a SELECT runs from the keyword to the first line ending in a period and carries no string literal that could hide one, so a full statement splitter would buy nothing and cost a reader twenty lines. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UxxNLTJFLpefuUNnRKqC61 --- package.json | 3 +- scripts/check-atc.mjs | 80 ++++++++++++++++++++++++ src/00/98/z2ui5_cl_smp_app_126.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_184.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_190.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_194.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_199.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_212.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_328.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_331.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_332.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_334.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_335.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_337.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_339.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_342.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_343.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_344.clas.abap | 4 +- src/00/98/z2ui5_cl_smp_app_345.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_347.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_348.clas.abap | 2 +- src/00/98/z2ui5_cl_smp_app_349.clas.abap | 2 +- 22 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 scripts/check-atc.mjs diff --git a/package.json b/package.json index 98ea792e..4cc09573 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,12 @@ "check:prose": "node scripts/check-prose-names.mjs", "check:docs-links": "node scripts/check-docs-links.mjs", "check:app-rules": "node scripts/check-app-rules.mjs", + "check:atc": "node scripts/check-atc.mjs", "check:pin": "node scripts/check-framework-pin.mjs", "rename": "abaplint .github/abaplint/rename_test.jsonc --rename", "syfixes": "find . -type f -name '*.abap' -exec sed -i -e 's/ RAISE EXCEPTION TYPE cx_sy_itab_line_not_found/ ASSERT 1 = 0/g' {} + ", "downport": "rm -rf src/00 && abaplint --fix .github/abaplint/abap_702.jsonc && npm run syfixes && node scripts/generate-samples-md.mjs", - "check": "npm run check:pin && npm run lint && npm run check:cloud && npm run check:abap2ui5 && npm run check:agents && npm run check:strip && npm run check:orphans && npm run check:keywords && npm run check:launchpad && npm run check:catalogue && npm run check:derived && npm run check:prose && npm run check:docs-links && npm run check:app-rules && npm run rename" + "check": "npm run check:pin && npm run lint && npm run check:cloud && npm run check:abap2ui5 && npm run check:agents && npm run check:strip && npm run check:orphans && npm run check:keywords && npm run check:launchpad && npm run check:catalogue && npm run check:derived && npm run check:prose && npm run check:docs-links && npm run check:app-rules && npm run check:atc && npm run rename" }, "repository": { "type": "git", diff --git a/scripts/check-atc.mjs b/scripts/check-atc.mjs new file mode 100644 index 00000000..bd27e1a0 --- /dev/null +++ b/scripts/check-atc.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/* + * check-atc — the extended program check (SLIN / ATC) runs in the systems the + * samples are INSTALLED on, and nowhere here. + * + * abaplint does not model it, so a sample can pass every gate in this + * repository and still light up a customer's ATC run - and a sample that does + * is worse than a missing sample: it is the code somebody copied because it + * was published as the way to do the thing. + * + * ONE finding, the one this repository actually carried: a `SELECT` with no + * `WHERE` clause. The check wants the pseudo-comment `"#EC CI_NOWHERE` on the + * statement, and the framework's own + * z2ui5_cl_ui5_srv_draft=>count_entries_total is the precedent - it reads the + * whole draft table on purpose and says so. Twenty-one statements here read a + * small demo table without a WHERE, which is exactly what they are + * demonstrating; none of them said so, and they were annotated in one pass + * (2026-09-16) by the same scan this gate runs. + * + * The scan is line-based on purpose. A SELECT runs from the keyword to the + * first line that ENDS in a period, and a SELECT carries no string literal + * that could hide one - so the two things a real statement splitter buys + * (literals, embedded templates) buy nothing here, and a reader can see in + * twenty lines what this decides. + * + * Run: npm run check:atc + */ +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const ROOT = new URL('..', import.meta.url).pathname; + +function abapFiles(dir, out = []) { + for (const entry of readdirSync(join(ROOT, dir), { withFileTypes: true })) { + const rel = `${dir}/${entry.name}`; + if (entry.isDirectory()) abapFiles(rel, out); + else if (rel.endsWith('.abap')) out.push(rel); + } + return out; +} + +/** The code half of a line: the trailing `"` comment cut off. */ +const code = (line) => { + const at = line.indexOf('"'); + return at === -1 ? line : line.slice(0, at); +}; + +const findings = []; + +for (const rel of abapFiles('src')) { + const lines = readFileSync(join(ROOT, rel), 'utf8').split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + if (/^\s*\*/.test(lines[i])) continue; + if (!/^\s*SELECT\b/i.test(code(lines[i]))) continue; + // collect the statement: to the first line whose CODE ends in a period + const text = []; + let j = i; + for (; j < lines.length; j++) { + text.push(lines[j]); + if (code(lines[j]).trimEnd().endsWith('.')) break; + } + const whole = text.join('\n'); + i = j; + if (/\bWHERE\b/i.test(whole.replace(/"[^\n]*/g, ''))) continue; + if (/#EC\s+CI_NOWHERE/i.test(whole)) continue; + findings.push(`${rel}:${i + 1 - (text.length - 1)} SELECT without WHERE and without "#EC CI_NOWHERE`); + } +} + +if (findings.length > 0) { + console.error('check-atc: the extended program check would report these.\n'); + for (const f of findings) console.error(` ${f}`); + console.error( + '\nA sample that reads a whole table on purpose says so with the pseudo-comment,\n' + + 'on the first line of the statement - see any annotated SELECT under src/00/98.', + ); + process.exit(1); +} + +console.log(`check-atc: ${findings.length} finding(s) - every SELECT without a WHERE says so`); diff --git a/src/00/98/z2ui5_cl_smp_app_126.clas.abap b/src/00/98/z2ui5_cl_smp_app_126.clas.abap index d9f9e546..d800fb43 100644 --- a/src/00/98/z2ui5_cl_smp_app_126.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_126.clas.abap @@ -99,7 +99,7 @@ CLASS z2ui5_cl_smp_app_126 IMPLEMENTATION. ASSIGN mt_table->* TO . - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
UP TO 3 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_184.clas.abap b/src/00/98/z2ui5_cl_smp_app_184.clas.abap index f8320c30..085fb723 100644 --- a/src/00/98/z2ui5_cl_smp_app_184.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_184.clas.abap @@ -149,7 +149,7 @@ CLASS z2ui5_cl_smp_app_184 IMPLEMENTATION. ASSIGN mt_table->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (mv_table) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_190.clas.abap b/src/00/98/z2ui5_cl_smp_app_190.clas.abap index 68c3d188..fa10a0f0 100644 --- a/src/00/98/z2ui5_cl_smp_app_190.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_190.clas.abap @@ -159,7 +159,7 @@ CLASS z2ui5_cl_smp_app_190 IMPLEMENTATION. ASSIGN mt_table->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (mv_table) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_194.clas.abap b/src/00/98/z2ui5_cl_smp_app_194.clas.abap index a27e230a..79312dcc 100644 --- a/src/00/98/z2ui5_cl_smp_app_194.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_194.clas.abap @@ -197,7 +197,7 @@ CLASS z2ui5_cl_smp_app_194 IMPLEMENTATION. ASSIGN mt_table->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (mv_table) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_199.clas.abap b/src/00/98/z2ui5_cl_smp_app_199.clas.abap index 1f1269f5..f2c1f41f 100644 --- a/src/00/98/z2ui5_cl_smp_app_199.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_199.clas.abap @@ -136,7 +136,7 @@ CLASS z2ui5_cl_smp_app_199 IMPLEMENTATION. CAST cl_abap_tabledescr( cl_abap_typedescr=>describe_by_data(
) )->get_table_line_type( ) )->get_components( ). - SELECT id, id_prev FROM z2ui5_t_01 + SELECT id, id_prev FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
UP TO 2 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_212.clas.abap b/src/00/98/z2ui5_cl_smp_app_212.clas.abap index 0c188dc9..757ad80e 100644 --- a/src/00/98/z2ui5_cl_smp_app_212.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_212.clas.abap @@ -299,7 +299,7 @@ CLASS z2ui5_cl_smp_app_212 IMPLEMENTATION. ASSIGN mt_table->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (mv_table) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_328.clas.abap b/src/00/98/z2ui5_cl_smp_app_328.clas.abap index 977190eb..c17249bb 100644 --- a/src/00/98/z2ui5_cl_smp_app_328.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_328.clas.abap @@ -148,7 +148,7 @@ CLASS z2ui5_cl_smp_app_328 IMPLEMENTATION. ASSIGN mt_table->* TO
. - SELECT id FROM z2ui5_t_01 + SELECT id FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
UP TO 4 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_331.clas.abap b/src/00/98/z2ui5_cl_smp_app_331.clas.abap index d407803a..8984d689 100644 --- a/src/00/98/z2ui5_cl_smp_app_331.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_331.clas.abap @@ -88,7 +88,7 @@ CLASS z2ui5_cl_smp_app_331 IMPLEMENTATION. " any single row will do here, but it has to be the SAME one on every " roundtrip - SELECT SINGLE without a full key leaves that to the database - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO TABLE @DATA(lt_data) UP TO 1 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_332.clas.abap b/src/00/98/z2ui5_cl_smp_app_332.clas.abap index 1d521c12..d8f67e0c 100644 --- a/src/00/98/z2ui5_cl_smp_app_332.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_332.clas.abap @@ -102,7 +102,7 @@ CLASS z2ui5_cl_smp_app_332 IMPLEMENTATION. " any single row will do here, but it has to be the SAME one on every " roundtrip - SELECT SINGLE without a full key leaves that to the database - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO TABLE @DATA(lt_data) UP TO 1 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_334.clas.abap b/src/00/98/z2ui5_cl_smp_app_334.clas.abap index 80741561..86ceba95 100644 --- a/src/00/98/z2ui5_cl_smp_app_334.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_334.clas.abap @@ -117,7 +117,7 @@ CLASS z2ui5_cl_smp_app_334 IMPLEMENTATION. " any single row will do here, but it has to be the SAME one on every " roundtrip - SELECT SINGLE without a full key leaves that to the database - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO TABLE @DATA(lt_data) UP TO 1 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_335.clas.abap b/src/00/98/z2ui5_cl_smp_app_335.clas.abap index c3b56050..1f3a741b 100644 --- a/src/00/98/z2ui5_cl_smp_app_335.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_335.clas.abap @@ -147,7 +147,7 @@ CLASS z2ui5_cl_smp_app_335 IMPLEMENTATION. " any single row will do here, but it has to be the SAME one on every " roundtrip - SELECT SINGLE without a full key leaves that to the database - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO TABLE @DATA(lt_data) UP TO 1 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_337.clas.abap b/src/00/98/z2ui5_cl_smp_app_337.clas.abap index f0b6e2ea..f5543da2 100644 --- a/src/00/98/z2ui5_cl_smp_app_337.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_337.clas.abap @@ -147,7 +147,7 @@ CLASS z2ui5_cl_smp_app_337 IMPLEMENTATION. METHOD get_data. - SELECT id, + SELECT id, "#EC CI_NOWHERE id_prev, id_prev_app, id_prev_app_stack, diff --git a/src/00/98/z2ui5_cl_smp_app_339.clas.abap b/src/00/98/z2ui5_cl_smp_app_339.clas.abap index 7a0405d4..a4811bfc 100644 --- a/src/00/98/z2ui5_cl_smp_app_339.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_339.clas.abap @@ -219,7 +219,7 @@ CLASS z2ui5_cl_smp_app_339 IMPLEMENTATION. ASSIGN mt_table->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (mv_table) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_342.clas.abap b/src/00/98/z2ui5_cl_smp_app_342.clas.abap index 8097157f..eab62e3a 100644 --- a/src/00/98/z2ui5_cl_smp_app_342.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_342.clas.abap @@ -223,7 +223,7 @@ CLASS z2ui5_cl_smp_app_342 IMPLEMENTATION. ASSIGN mt_data->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (mv_table) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_343.clas.abap b/src/00/98/z2ui5_cl_smp_app_343.clas.abap index 3672da6b..c6040bb3 100644 --- a/src/00/98/z2ui5_cl_smp_app_343.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_343.clas.abap @@ -66,7 +66,7 @@ CLASS Z2UI5_CL_SMP_APP_343 IMPLEMENTATION. CREATE DATA mt_data1 TYPE HANDLE new_table_desc. ASSIGN mt_data1->* TO . - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO TABLE @ UP TO 5 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_344.clas.abap b/src/00/98/z2ui5_cl_smp_app_344.clas.abap index c1866fed..06d663cb 100644 --- a/src/00/98/z2ui5_cl_smp_app_344.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_344.clas.abap @@ -176,7 +176,7 @@ CLASS z2ui5_cl_smp_app_344 IMPLEMENTATION. ASSIGN mt_data->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (iv_tabname) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
@@ -207,7 +207,7 @@ CLASS z2ui5_cl_smp_app_344 IMPLEMENTATION. ASSIGN mt_data2->* TO
. - SELECT * + SELECT * "#EC CI_NOWHERE FROM (iv_tabname) ORDER BY PRIMARY KEY INTO CORRESPONDING FIELDS OF TABLE @
diff --git a/src/00/98/z2ui5_cl_smp_app_345.clas.abap b/src/00/98/z2ui5_cl_smp_app_345.clas.abap index f40a4cdc..dd624a61 100644 --- a/src/00/98/z2ui5_cl_smp_app_345.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_345.clas.abap @@ -75,7 +75,7 @@ CLASS Z2UI5_CL_SMP_APP_345 IMPLEMENTATION. CREATE DATA mt_data1 TYPE HANDLE new_table_desc. ASSIGN mt_data1->* TO . - SELECT * FROM z2ui5_t_01 + SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE ORDER BY PRIMARY KEY INTO TABLE @ UP TO 5 ROWS. diff --git a/src/00/98/z2ui5_cl_smp_app_347.clas.abap b/src/00/98/z2ui5_cl_smp_app_347.clas.abap index fe9a81f0..138a6798 100644 --- a/src/00/98/z2ui5_cl_smp_app_347.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_347.clas.abap @@ -132,7 +132,7 @@ CLASS z2ui5_cl_smp_app_347 IMPLEMENTATION. METHOD get_data. - SELECT id, + SELECT id, "#EC CI_NOWHERE id_prev, id_prev_app, id_prev_app_stack, diff --git a/src/00/98/z2ui5_cl_smp_app_348.clas.abap b/src/00/98/z2ui5_cl_smp_app_348.clas.abap index e93dc07b..5e1ba8b8 100644 --- a/src/00/98/z2ui5_cl_smp_app_348.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_348.clas.abap @@ -116,7 +116,7 @@ CLASS z2ui5_cl_smp_app_348 IMPLEMENTATION. " any single row will do here, but it has to be the SAME one on every " roundtrip - SELECT SINGLE without a full key leaves that to the database - SELECT id, + SELECT id, "#EC CI_NOWHERE id_prev, id_prev_app, id_prev_app_stack, diff --git a/src/00/98/z2ui5_cl_smp_app_349.clas.abap b/src/00/98/z2ui5_cl_smp_app_349.clas.abap index ffc38e07..59d243e5 100644 --- a/src/00/98/z2ui5_cl_smp_app_349.clas.abap +++ b/src/00/98/z2ui5_cl_smp_app_349.clas.abap @@ -155,7 +155,7 @@ CLASS z2ui5_cl_smp_app_349 IMPLEMENTATION. METHOD get_data. - SELECT id, + SELECT id, "#EC CI_NOWHERE id_prev, id_prev_app, id_prev_app_stack, From 06650eda1b7aa82228ab1d8a41d589259e9770de Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 12:28:00 +0000 Subject: [PATCH 3/5] Gate #1937 here too: no sy-subrc after a plain dynamic ASSIGN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate this repository gained for `SELECT` without `WHERE` grows the second finding the same sweep produced. On some releases a SUCCESSFUL dynamic `ASSIGN` does not reset `sy-subrc`, so a test on it reads FALSE for an assignment that worked and TRUE for one that did not (abap2UI5 #1937). Fourteen of those shipped here - among them the sub-app view handover (`MV_VIEW_DISPLAY`, `VIEW_PARENT`), and app_502's node builder, which assigns inside a `DO` where a stale binding builds onto the wrong node. `ASSIGN COMPONENT … OF STRUCTURE` is the negative and is never reported: there `sy-subrc` distinguishes "component not found" and IS the documented check, so reporting it is how a cleanup turns a wrong-branch bug into a silently-taken one. That is also why the scan now reads ABAP STATEMENTS rather than lines - a multi-line `ASSIGN COMPONENT` looks like a plain `ASSIGN` to a line-based scan, which would report exactly the shape that must not be reported. Verified by re-introducing one of each: the gate names both, with the file, the line and the fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UxxNLTJFLpefuUNnRKqC61 --- scripts/check-atc.mjs | 157 ++++++++++++++++++++++++++++++------------ 1 file changed, 113 insertions(+), 44 deletions(-) diff --git a/scripts/check-atc.mjs b/scripts/check-atc.mjs index bd27e1a0..00c66251 100644 --- a/scripts/check-atc.mjs +++ b/scripts/check-atc.mjs @@ -1,27 +1,41 @@ #!/usr/bin/env node /* - * check-atc — the extended program check (SLIN / ATC) runs in the systems the - * samples are INSTALLED on, and nowhere here. + * check-atc — two findings a sample can ship that no gate here could see. * - * abaplint does not model it, so a sample can pass every gate in this - * repository and still light up a customer's ATC run - and a sample that does - * is worse than a missing sample: it is the code somebody copied because it - * was published as the way to do the thing. + * Both come from the same place: abaplint models neither, so a sample passes + * every gate in this repository and the problem surfaces on the system + * somebody installed it on. That is worse than a missing sample - it is the + * code they copied because it was published as the way to do the thing. * - * ONE finding, the one this repository actually carried: a `SELECT` with no - * `WHERE` clause. The check wants the pseudo-comment `"#EC CI_NOWHERE` on the - * statement, and the framework's own - * z2ui5_cl_ui5_srv_draft=>count_entries_total is the precedent - it reads the - * whole draft table on purpose and says so. Twenty-one statements here read a - * small demo table without a WHERE, which is exactly what they are - * demonstrating; none of them said so, and they were annotated in one pass - * (2026-09-16) by the same scan this gate runs. + * 1. NOWHERE - a `SELECT` with no `WHERE` clause. * - * The scan is line-based on purpose. A SELECT runs from the keyword to the - * first line that ENDS in a period, and a SELECT carries no string literal - * that could hide one - so the two things a real statement splitter buys - * (literals, embedded templates) buy nothing here, and a reader can see in - * twenty lines what this decides. + * The extended program check (SLIN / ATC) runs in those systems and + * nowhere here; it wants the pseudo-comment `"#EC CI_NOWHERE` on the + * statement. The framework's own z2ui5_cl_ui5_srv_draft=>count_entries_total + * is the precedent - it reads the whole draft table on purpose and says so. + * Twenty-one statements here read a small demo table with no WHERE, which is + * exactly what they demonstrate; none said so until 2026-09-16. + * + * 2. SUBRC_AFTER_ASSIGN - `sy-subrc` read after a plain dynamic `ASSIGN`. + * + * On some releases a SUCCESSFUL assign does not reset `sy-subrc`, so the + * test reads FALSE for an assignment that worked and TRUE for one that did + * not (abap2UI5 #1937). `IS [NOT] ASSIGNED` is the check that holds on all + * of them - and inside a LOOP, `UNASSIGN .` has to come FIRST, because + * a failed assign leaves the previous round's binding in place and + * `IS ASSIGNED` would then read TRUE for the failure. Fourteen of these + * shipped here, among them the sub-app view handover (`MV_VIEW_DISPLAY`, + * `VIEW_PARENT`) - and app_502's node builder, which assigns inside a + * `DO`, where a stale binding builds onto the wrong node. + * + * `ASSIGN COMPONENT … OF STRUCTURE` is the NEGATIVE and must never be + * reported: there `sy-subrc` distinguishes "component not found" and IS + * the documented check. Reporting it is how a cleanup turns a + * wrong-branch bug into a silently-taken one. + * + * The scan reads ABAP STATEMENTS, not lines - a multi-line `ASSIGN COMPONENT` + * looks like a plain `ASSIGN` to a line-based scan, which would report the one + * shape that must not be reported. * * Run: npm run check:atc */ @@ -39,42 +53,97 @@ function abapFiles(dir, out = []) { return out; } -/** The code half of a line: the trailing `"` comment cut off. */ -const code = (line) => { - const at = line.indexOf('"'); - return at === -1 ? line : line.slice(0, at); -}; +/* The code half of a line: the trailing `"` comment cut off, outside string + * literals so a `"` in a `'…'` or `|…|` stays text. */ +function code(line) { + let quote = null; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (quote) { + if (ch === quote) quote = null; + continue; + } + if (ch === "'" || ch === '|') quote = ch; + else if (ch === '"') return line.slice(0, i); + } + return line; +} + +/* Statements, with the line they start on and their raw text (the raw half + * keeps the pseudo-comments rule 1 decides on). */ +function statements(source) { + const out = []; + let raw = []; + let start = 0; + source.split(/\r?\n/).forEach((line, i) => { + if (/^\s*\*/.test(line)) return; + const c = code(line); + if (raw.length === 0) { + if (c.trim() === '') return; + start = i + 1; + } + raw.push(line); + if (c.trimEnd().endsWith('.') || c.trimEnd().endsWith(':')) { + out.push({ start, raw: raw.join('\n'), code: raw.map(code).join(' ').replace(/\s+/g, ' ').trim() }); + raw = []; + } + }); + if (raw.length > 0) out.push({ start, raw: raw.join('\n'), code: raw.map(code).join(' ').replace(/\s+/g, ' ').trim() }); + return out; +} + +/* Statements that write sy-subrc and so end an ASSIGN's claim on it. + * Deliberately short and deliberately over-broad: one missing from the list + * makes the scan report MORE, which a reader catches, rather than less. */ +const SETS_SUBRC = + /^(read\s+table|select|loop\s+at|find|replace|call\s+function|call\s+method|delete|insert|modify|append|split|open\s+dataset|authority-check|import|export|describe|search|get\s+parameter|set\s+parameter)\b/i; const findings = []; for (const rel of abapFiles('src')) { - const lines = readFileSync(join(ROOT, rel), 'utf8').split(/\r?\n/); - for (let i = 0; i < lines.length; i++) { - if (/^\s*\*/.test(lines[i])) continue; - if (!/^\s*SELECT\b/i.test(code(lines[i]))) continue; - // collect the statement: to the first line whose CODE ends in a period - const text = []; - let j = i; - for (; j < lines.length; j++) { - text.push(lines[j]); - if (code(lines[j]).trimEnd().endsWith('.')) break; + let claim = null; + for (const st of statements(readFileSync(join(ROOT, rel), 'utf8'))) { + const { code: c, raw, start } = st; + + if (/^SELECT\b/i.test(c) && !/\bWHERE\b/i.test(c) && !/#EC\s+CI_NOWHERE/i.test(raw)) { + findings.push({ + rule: 'nowhere', + at: `${rel}:${start}`, + message: 'SELECT without a WHERE clause - the extended check wants "#EC CI_NOWHERE on the statement', + }); + } + + if (/^ASSIGN\b/i.test(c)) { + claim = /^ASSIGN\s+COMPONENT\b/i.test(c) ? 'component' : 'plain'; + continue; + } + if (/\bsy-subrc\b/i.test(c)) { + if (claim === 'plain') { + findings.push({ + rule: 'subrc_after_assign', + at: `${rel}:${start}`, + message: `${c.slice(0, 70)} - a successful dynamic ASSIGN does not reset sy-subrc on every release (#1937); use IS [NOT] ASSIGNED`, + }); + } + claim = null; + continue; } - const whole = text.join('\n'); - i = j; - if (/\bWHERE\b/i.test(whole.replace(/"[^\n]*/g, ''))) continue; - if (/#EC\s+CI_NOWHERE/i.test(whole)) continue; - findings.push(`${rel}:${i + 1 - (text.length - 1)} SELECT without WHERE and without "#EC CI_NOWHERE`); + if (SETS_SUBRC.test(c) || /\bEXCEPTIONS\b/i.test(c)) claim = null; + if (/^(METHOD|ENDMETHOD|FORM|ENDFORM)\b/i.test(c)) claim = null; } } if (findings.length > 0) { - console.error('check-atc: the extended program check would report these.\n'); - for (const f of findings) console.error(` ${f}`); + console.error('check-atc: these fire on the system a sample is installed on.\n'); + for (const f of findings) console.error(` [${f.rule}] ${f.at}\n ${f.message}`); console.error( - '\nA sample that reads a whole table on purpose says so with the pseudo-comment,\n' + - 'on the first line of the statement - see any annotated SELECT under src/00/98.', + '\nnowhere - a full read on purpose says so, on the first line of the\n' + + ' statement; see any annotated SELECT under src/.\n' + + 'subrc_after_assign - IS [NOT] ASSIGNED, and `UNASSIGN .` BEFORE the ASSIGN\n' + + ' when it sits in a loop or the symbol was assigned earlier.\n' + + ' ASSIGN COMPONENT is not this finding and is never reported.', ); process.exit(1); } -console.log(`check-atc: ${findings.length} finding(s) - every SELECT without a WHERE says so`); +console.log('check-atc: no SELECT without a WHERE, no sy-subrc after a dynamic ASSIGN - OK'); From a1e8b2dab54b137309efa698a869013bebbd25a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 13:31:10 +0000 Subject: [PATCH 4/5] A text symbol is a character literal - read it into a variable first A Code Inspector SYNTAX_CHECK on a real system reported app 519's view_display: `'Your name'(001) is not type-compatible with formal parameter "V"` - a SYNTAX_ERROR of the whole class, on the sample whose subject IS translatable texts, while abaplint, the transpiler and the unit suite were all green on it. None of them models the compatibility rule for a text-symbol literal: it is a CHARACTER literal, and the view builder's `v` is TYPE string. The three symbols are read into variables and the variables are passed. That is not just the repair, it is the better teaching shape - an app that uses a text twice would write it the same way - and the comment now says why, and says what needs no variable: inside a string template (which on_event( ) does five lines up) an embedded expression is a general expression position. `check:atc` grows the rule as its third. A PARAMETER binding only, decided by whether the binding sits inside an open paren: `lv_x = 'y'(001).` is an assignment and a plain conversion, correct on every release. Verified by putting the symbol back: the gate names the file, the line and the fix. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UxxNLTJFLpefuUNnRKqC61 --- scripts/check-atc.mjs | 30 ++++++++++++++++++++++++++- src/01/z2ui5_cl_smp_app_519.clas.abap | 24 ++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/scripts/check-atc.mjs b/scripts/check-atc.mjs index 00c66251..83d5bf53 100644 --- a/scripts/check-atc.mjs +++ b/scripts/check-atc.mjs @@ -33,6 +33,17 @@ * the documented check. Reporting it is how a cleanup turns a * wrong-branch bug into a silently-taken one. * + * 3. TEXT_SYMBOL_ARG - a text symbol (`'text'(001)`) handed to a PARAMETER. + * + * It is a CHARACTER literal, so a formal parameter typed `string` - the + * view builder's `v`, for one - answers `'...'(001) is not type-compatible + * with formal parameter V`, a SYNTAX_ERROR of the whole class. Three of these shipped here, in app 519 - the sample whose subject IS + * translatable texts, so the one place it was certain to appear. + * + * A PARAMETER binding only: `lv_x = 'y'(001).` is an assignment and a plain + * conversion, and a symbol inside a string template sits in a general + * expression position. Read it into a variable and pass that. + * * The scan reads ABAP STATEMENTS, not lines - a multi-line `ASSIGN COMPONENT` * looks like a plain `ASSIGN` to a line-based scan, which would report the one * shape that must not be reported. @@ -98,6 +109,8 @@ function statements(source) { const SETS_SUBRC = /^(read\s+table|select|loop\s+at|find|replace|call\s+function|call\s+method|delete|insert|modify|append|split|open\s+dataset|authority-check|import|export|describe|search|get\s+parameter|set\s+parameter)\b/i; +const TEXT_SYMBOL_ARG = /\b\w+\s*=\s*'[^']*'\(\d{3}\)/; + const findings = []; for (const rel of abapFiles('src')) { @@ -113,6 +126,19 @@ for (const rel of abapFiles('src')) { }); } + const symbolAt = c.search(TEXT_SYMBOL_ARG); + if (symbolAt !== -1) { + const head = c.slice(0, symbolAt); + const depth = (head.match(/\(/g) ?? []).length - (head.match(/\)/g) ?? []).length; + if (depth > 0) { + findings.push({ + rule: 'text_symbol_arg', + at: `${rel}:${start}`, + message: "a text symbol is a CHARACTER literal - a parameter typed `string` answers \"not type-compatible with formal parameter\"; read it into a variable and pass that", + }); + } + } + if (/^ASSIGN\b/i.test(c)) { claim = /^ASSIGN\s+COMPONENT\b/i.test(c) ? 'component' : 'plain'; continue; @@ -139,6 +165,8 @@ if (findings.length > 0) { console.error( '\nnowhere - a full read on purpose says so, on the first line of the\n' + ' statement; see any annotated SELECT under src/.\n' + + 'text_symbol_arg - read the symbol into a variable and pass the variable;\n' + + ' an assignment and a string template need nothing.\n' + 'subrc_after_assign - IS [NOT] ASSIGNED, and `UNASSIGN .` BEFORE the ASSIGN\n' + ' when it sits in a loop or the symbol was assigned earlier.\n' + ' ASSIGN COMPONENT is not this finding and is never reported.', @@ -146,4 +174,4 @@ if (findings.length > 0) { process.exit(1); } -console.log('check-atc: no SELECT without a WHERE, no sy-subrc after a dynamic ASSIGN - OK'); +console.log('check-atc: no SELECT without a WHERE, no sy-subrc after a dynamic ASSIGN,\n no text symbol passed to a parameter - OK'); diff --git a/src/01/z2ui5_cl_smp_app_519.clas.abap b/src/01/z2ui5_cl_smp_app_519.clas.abap index d8ab4c71..c34af6f8 100644 --- a/src/01/z2ui5_cl_smp_app_519.clas.abap +++ b/src/01/z2ui5_cl_smp_app_519.clas.abap @@ -46,6 +46,11 @@ CLASS z2ui5_cl_smp_app_519 IMPLEMENTATION. METHOD view_display. + " the three text symbols this app is about - see the block below + DATA lv_name_label TYPE string. + DATA lv_placeholder TYPE string. + DATA lv_greet TYPE string. + DATA(view) = z2ui5_cl_ui5_view_builder=>factory( )->ele( n = `View` ns = `mvc` )->a( n = `displayBlock` v = `true` @@ -70,22 +75,35 @@ CLASS z2ui5_cl_smp_app_519 IMPLEMENTATION. " Every text below comes out of the class's text pool (Goto > Text " Elements in SE24/ADT). The literal in the source is the fallback and the " maintenance text; what renders is the entry for the logon language. + " + " Read into a VARIABLE first, and that is part of the lesson: a text + " symbol is a CHARACTER literal, while the builder's v is TYPE string. + " Handing one straight to v answers `'...'(001) is not type-compatible + " with formal parameter V` on a system - a SYNTAX_ERROR of the class, + " although abaplint, the transpiler and the unit suite are all green on + " it. The assignment below is a plain conversion and is allowed on every + " release. Inside a string template ( see on_event( ) ) the symbol needs + " no variable: an embedded expression is a general expression position. + lv_name_label = 'Your name'(001). + lv_placeholder = 'Type a name here'(002). + lv_greet = 'Greet'(003). + page->ele( `VBox` )->a( n = `class` v = `sapUiSmallMargin` )->tag( `Label` - )->a( n = `text` v = 'Your name'(001) + )->a( n = `text` v = lv_name_label )->a( n = `labelFor` v = `nameInput` )->tag( `Input` )->a( n = `id` v = `nameInput` )->a( n = `value` v = client->_bind( name ) - )->a( n = `placeholder` v = 'Type a name here'(002) + )->a( n = `placeholder` v = lv_placeholder )->a( n = `width` v = `20rem` )->tag( `Button` )->a( n = `press` v = client->_event( `GREET` ) - )->a( n = `text` v = 'Greet'(003) + )->a( n = `text` v = lv_greet )->a( n = `type` v = `Emphasized` )->a( n = `class` v = `sapUiSmallMarginTop` ). From 82bade4c03eeab245e9f0e74237e6cc1e2b69029 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 16 Sep 2026 13:49:50 +0000 Subject: [PATCH 5/5] Give check-atc a CI job, so a pull request can go red on it The gate was only in the local `npm run check` chain, and this repository runs one workflow per gate for a reason its own check-abapdoc header states: a gate that exists only in the chain cannot turn a pull request red. Same shape as check-keywords - plain node, no dependencies, a few seconds - and the header names what each of the three rules found here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UxxNLTJFLpefuUNnRKqC61 --- .github/workflows/check-atc.yaml | 46 ++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/check-atc.yaml diff --git a/.github/workflows/check-atc.yaml b/.github/workflows/check-atc.yaml new file mode 100644 index 00000000..2bc76dad --- /dev/null +++ b/.github/workflows/check-atc.yaml @@ -0,0 +1,46 @@ +name: check-atc + +# The extended program check (SLIN / ATC) runs in the systems these samples are +# INSTALLED on and nowhere here - abaplint models none of its findings. So a +# sample passes every other gate in this repository and the problem surfaces on +# somebody's system, which is worse than a missing sample: it is the code they +# copied because it was published as the way to do the thing. +# +# Three findings, each named by the rule that decides it: +# +# nowhere 21 SELECT statements read a small demo table with no +# WHERE and none of them said so +# subrc_after_assign 14 sy-subrc tests sat after a dynamic ASSIGN, among them +# the sub-app view handover and a node builder inside a DO +# text_symbol_arg three text symbols went straight to the view builder's +# `v`, which is TYPE string - a SYNTAX_ERROR of the class, +# reported from a system on 2026-09-16 +# +# A JOB of its own, not a step of `npm run check`: this repository runs one +# workflow per gate, because a gate that only exists in the local chain cannot +# turn a pull request red - the same reason check-abapdoc has one. +# +# Plain node, no dependencies, so it stays a few seconds. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: check-atc-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-atc: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22' + - run: node scripts/check-atc.mjs