Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/check-atc.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
177 changes: 177 additions & 0 deletions scripts/check-atc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env node
/*
* check-atc — two findings a sample can ship that no gate here could see.
*
* 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.
*
* 1. NOWHERE - a `SELECT` with no `WHERE` clause.
*
* 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 <fs>.` 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.
*
* 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.
*
* 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, 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 TEXT_SYMBOL_ARG = /\b\w+\s*=\s*'[^']*'\(\d{3}\)/;

const findings = [];

for (const rel of abapFiles('src')) {
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',
});
}

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;
}
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;
}
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: 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(
'\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 <fs>.` 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: no SELECT without a WHERE, no sy-subrc after a dynamic ASSIGN,\n no text symbol passed to a parameter - OK');
4 changes: 3 additions & 1 deletion src/00/98/z2ui5_cl_smp_app_117.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -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 <view_display>.

IF sy-subrc = 0 AND <view_display> = abap_true.
IF <view_display> IS ASSIGNED AND <view_display> = abap_true.

<view_display> = abap_false.
client->view_display( mo_main_page->stringify( ) ).
Expand Down
2 changes: 1 addition & 1 deletion src/00/98/z2ui5_cl_smp_app_126.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ CLASS z2ui5_cl_smp_app_126 IMPLEMENTATION.

ASSIGN mt_table->* TO <table>.

SELECT * FROM z2ui5_t_01
SELECT * FROM z2ui5_t_01 "#EC CI_NOWHERE
ORDER BY PRIMARY KEY
INTO CORRESPONDING FIELDS OF TABLE @<table>
UP TO 3 ROWS.
Expand Down
4 changes: 3 additions & 1 deletion src/00/98/z2ui5_cl_smp_app_131.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -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 <view_display>.

IF sy-subrc = 0 AND <view_display> = abap_true.
IF <view_display> IS ASSIGNED AND <view_display> = abap_true.

<view_display> = abap_false.
client->view_display( mo_main_page->stringify( ) ).
Expand Down
2 changes: 1 addition & 1 deletion src/00/98/z2ui5_cl_smp_app_184.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ CLASS z2ui5_cl_smp_app_184 IMPLEMENTATION.

ASSIGN mt_table->* TO <table>.

SELECT *
SELECT * "#EC CI_NOWHERE
FROM (mv_table)
ORDER BY PRIMARY KEY
INTO CORRESPONDING FIELDS OF TABLE @<table>
Expand Down
4 changes: 3 additions & 1 deletion src/00/98/z2ui5_cl_smp_app_185.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -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 <view_display>.

IF sy-subrc = 0 AND <view_display> = abap_true.
IF <view_display> IS ASSIGNED AND <view_display> = abap_true.

<view_display> = abap_false.
client->view_display( mo_main_page->stringify( ) ).
Expand Down
2 changes: 1 addition & 1 deletion src/00/98/z2ui5_cl_smp_app_190.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ CLASS z2ui5_cl_smp_app_190 IMPLEMENTATION.

ASSIGN mt_table->* TO <table>.

SELECT *
SELECT * "#EC CI_NOWHERE
FROM (mv_table)
ORDER BY PRIMARY KEY
INTO CORRESPONDING FIELDS OF TABLE @<table>
Expand Down
4 changes: 3 additions & 1 deletion src/00/98/z2ui5_cl_smp_app_191.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -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 <view_display>.

IF sy-subrc = 0 AND <view_display> = abap_true.
IF <view_display> IS ASSIGNED AND <view_display> = abap_true.

<view_display> = abap_false.
client->view_display( mo_main_page->stringify( ) ).
Expand Down
2 changes: 1 addition & 1 deletion src/00/98/z2ui5_cl_smp_app_194.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ CLASS z2ui5_cl_smp_app_194 IMPLEMENTATION.

ASSIGN mt_table->* TO <table>.

SELECT *
SELECT * "#EC CI_NOWHERE
FROM (mv_table)
ORDER BY PRIMARY KEY
INTO CORRESPONDING FIELDS OF TABLE @<table>
Expand Down
4 changes: 3 additions & 1 deletion src/00/98/z2ui5_cl_smp_app_195.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -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 <view_display>.

IF sy-subrc = 0 AND <view_display> = abap_true.
IF <view_display> IS ASSIGNED AND <view_display> = abap_true.

<view_display> = abap_false.
client->view_display( mo_main_page->stringify( ) ).
Expand Down
2 changes: 1 addition & 1 deletion src/00/98/z2ui5_cl_smp_app_199.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ CLASS z2ui5_cl_smp_app_199 IMPLEMENTATION.
CAST cl_abap_tabledescr(
cl_abap_typedescr=>describe_by_data( <table> ) )->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 @<table>
UP TO 2 ROWS.
Expand Down
4 changes: 3 additions & 1 deletion src/00/98/z2ui5_cl_smp_app_211.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -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(<view_display>).

IF sy-subrc = 0 AND <view_display> = abap_true.
IF <view_display> IS ASSIGNED AND <view_display> = abap_true.

<view_display> = abap_false.
client->view_display( mo_main_page->stringify( ) ).
Expand Down
Loading
Loading