At a guess, many of you will have had reasons to use the SAP standard transaction CODE_SCANNER to search through ABAP code in order to find a specified string. I hadn’t known about this old transaction until happening upon the blog post by arghadip kar in 2021 and have been using it regularly since then. It’s a great way to quickly search through all ABAP-code in Z* packages (just not in enhancements unfortunately).
We are currently in the process to identify Z-code which is still using checks on SY-UNAME instead of proper authority-checks or other “sub-optimal” checking logic based on the content of SY-UNAME. We want to use the results to show a warning message whenever impacted ABAP code is opened, also asking to get it fixed as quickly as possible. Our initial idea was to simply run CODE_SCANNER, dump the results into a spreadsheet and then fill a Z-table with the program names in order to trigger the message via available exits in SE38 and SE37 (yes, we are still mostly working in the GUI and not Eclipse).
Unfortunately, the CODE_SCANNER results were not quite up to the task as they included a lot of false hits, where SY-UNAME is for example used in field assignements, a perfectly legitimate use of course:
Many hits were also in Includes belonging to a function module. There, it’s rather unlikely that the code will be directly accessed via SE38 based on the include name and much more likely to go via SE37 and the function module name. I thought that it would also be nice to show at least one of the master programs an include was used in and to provide the short descriptions for the program and/or function module as additional information.
Long story short, I suggested going against our coding guidelines – which I’m responsible for! – and to create a Z-copy of the CODE_SCANNER program in order to make it better fit our needs. After that was approved, I went to work and will use the rest of the blog post to highlight some of the logic I added to the copied code.
Additional fields for scan results
TYPES: BEGIN OF t_str_lines,
devclass LIKE tadir-devclass,
progname LIKE rs38m-programm,
inakt LIKE zbc_inaktiv-inakt, "001+
linno LIKE trans_err-line, "sy-tabix,
line LIKE abapsource-line,
pgmid LIKE tadir-pgmid, "001+
object LIKE tadir-object, "001+
master LIKE d010inc-master, "001+
repti LIKE rs38m-repti, "001+
funcname LIKE tfdir-funcname, "001+
funcdesc LIKE tftit-stext, "001+
sysid LIKE sy-sysid, "001+
END OF t_str_lines.
DATA: BEGIN OF g_tab_lines OCCURS 0,
devclass LIKE tadir-devclass,
progname LIKE rs38m-programm,
inakt LIKE zbc_inaktiv-inakt, "001+
linno LIKE trans_err-line, "sy-tabix,
line LIKE abapsource-line,
pgmid LIKE tadir-pgmid, "001+
object LIKE tadir-object, "001+
master LIKE d010inc-master, "001+
repti LIKE rs38m-repti, "001+
funcname LIKE tfdir-funcname, "001+
funcdesc LIKE tftit-stext, "001+
sysid LIKE sy-sysid, "001+
END OF g_tab_lines.
* "001 Begin
TYPES: BEGIN OF ty_objects,
mandt TYPE mandt,
obj_name TYPE sobj_name,
field TYPE z_field,
credat TYPE creationdt,
created_by TYPE uname,
ignore_obj TYPE check_1,
comments TYPE z_comment,
END OF ty_objects.
DATA: g_tab_objects TYPE SORTED TABLE OF ty_objects
WITH UNIQUE KEY obj_name field.
* "001 End
Filling the fields – logic called after scan complete and before results are displayed
"**************end package structure explosion
"Process packages
l_tabix = 0.
LOOP AT l_tab_tadir INTO l_str_tadir.
l_tabix = l_tabix + 1.
l_devclass = l_str_tadir-obj_name.
PERFORM scan_devc USING l_devclass l_tabix l_cnt p_lrng.
ENDLOOP.
"Process local package $TMP
IF l_flg_process_tmp = con_true.
l_tabix = l_tabix + 1.
PERFORM scan_devc USING c_devc_tmp l_tabix l_cnt p_lrng.
ENDIF.
"Get addition data from D010INC for master program "001+
"and TFDIR for function module
PERFORM get_additional_data.
"If requested refresh table ZBC_OBJECTS_WARN "001+
IF p_del EQ abap_true OR
p_ins EQ abap_true.
PERFORM update_objects_table.
ENDIF.
"Display scan result data
PERFORM scan_result_display.
ENDFORM. "process_devc
*&---------------------------------------------------------------------*
*& Form get_additional_data
*&---------------------------------------------------------------------*
FORM get_additional_data.
"Determine distinct programs/includes
DATA(distinct_prognames) = g_tab_lines[].
SORT distinct_prognames BY progname.
DELETE ADJACENT DUPLICATES FROM distinct_prognames COMPARING progname.
IF distinct_prognames[] IS NOT INITIAL.
SELECT include, master
FROM d010inc
INTO TABLE @DATA(master_programs)
FOR ALL ENTRIES IN @distinct_prognames
WHERE include EQ @distinct_prognames-progname.
DATA(cnt_master_programs) = lines( master_programs ).
SORT master_programs BY include master.
SELECT repid, inakt
FROM zbc_inaktiv
INTO TABLE @DATA(inactive_programs)
FOR ALL ENTRIES IN @distinct_prognames
WHERE repid EQ @distinct_prognames-progname.
DATA(cnt_inactive_programs) = lines( inactive_programs ).
SORT inactive_programs BY repid.
IF cnt_master_programs GT 0 OR cnt_inactive_programs GT 0.
LOOP AT g_tab_lines[] INTO DATA(line).
READ TABLE master_programs INTO DATA(master_program)
WITH KEY include = line-progname
BINARY SEARCH.
IF sy-subrc EQ 0.
line-master = master_program-master.
"Determine title of master program
PERFORM determine_program_title USING line-master
CHANGING line-repti.
MODIFY g_tab_lines[] FROM line TRANSPORTING master repti.
ELSE.
PERFORM determine_program_title USING line-progname
CHANGING line-repti.
MODIFY g_tab_lines[] FROM line TRANSPORTING repti.
ENDIF.
READ TABLE inactive_programs INTO DATA(inactive_program)
WITH KEY repid = line-progname
BINARY SEARCH.
IF sy-subrc EQ 0.
line-inakt = inactive_program-inakt.
ELSE.
clear line-inakt.
ENDIF.
MODIFY g_tab_lines[] FROM line TRANSPORTING inakt.
ENDLOOP.
ENDIF.
ENDIF.
"Determine distinct includes to determine function modules
DATA(distinct_includes) = g_tab_lines[].
SORT distinct_includes BY progname.
DELETE distinct_includes WHERE master EQ space.
DELETE ADJACENT DUPLICATES FROM distinct_includes COMPARING progname.
IF distinct_includes[] IS NOT INITIAL.
SELECT d~pname, d~include, d~funcname, t~stext AS funcdesc
FROM tfdir AS d
LEFT OUTER JOIN tftit AS t
ON d~funcname EQ t~funcname
AND ( t~spras EQ 'D' OR
t~spras EQ 'E')
INTO TABLE @DATA(function_modules)
FOR ALL ENTRIES IN @distinct_includes
WHERE d~pname EQ @distinct_includes-master.
IF sy-subrc EQ 0.
SORT function_modules BY pname include.
LOOP AT g_tab_lines[] INTO DATA(line_for_fm).
DATA(num_of_char) = numofchar( line_for_fm-progname ).
DATA(pos) = num_of_char - 2.
DATA(include_no) = line_for_fm-progname+pos(2).
READ TABLE function_modules INTO DATA(function_module)
WITH KEY pname = line_for_fm-master
include = include_no
BINARY SEARCH.
IF sy-subrc EQ 0.
line_for_fm-funcname = function_module-funcname.
line_for_fm-funcdesc = function_module-funcdesc.
MODIFY g_tab_lines[] FROM line_for_fm
TRANSPORTING funcname funcdesc.
ENDIF.
ENDLOOP.
ENDIF.
ENDIF.
g_cnt_hits = lines( g_tab_lines ).
ENDFORM.
Add program title
*&---------------------------------------------------------------------*
*& Form determine_program_title
*&---------------------------------------------------------------------*
FORM determine_program_title USING u_progname TYPE programm
CHANGING c_repti LIKE rs38m-repti.
TYPES: ty_text LIKE textpool.
DATA: prog_texts TYPE ty_text OCCURS 0 WITH HEADER LINE.
CLEAR: prog_texts,
c_repti.
REFRESH prog_texts.
READ TEXTPOOL u_progname INTO prog_texts LANGUAGE 'E'.
IF sy-subrc EQ 0.
READ TABLE prog_texts WITH KEY id = 'R'.
IF sy-subrc EQ 0.
ELSE.
prog_texts-entry = ' '.
ENDIF.
ELSE.
"2nd try with German
READ TEXTPOOL u_progname INTO prog_texts LANGUAGE 'D'.
IF sy-subrc EQ 0.
READ TABLE prog_texts WITH KEY id = 'R'.
IF sy-subrc EQ 0.
ELSE.
prog_texts-entry = ' '.
ENDIF.
ENDIF.
ENDIF.
c_repti = prog_texts-entry.
ENDFORM.
Eliminate more false hits
Switched from three paramaters on the selection screen to one select-option:
SELECTION-SCREEN: BEGIN OF BLOCK b WITH FRAME TITLE TEXT-002.
SELECT-OPTIONS: s_excl FOR zmmllakte_text-text NO INTERVALS. "001+
SELECTION-SCREEN: SKIP.
PARAMETERS: p_lrng(2) TYPE n OBLIGATORY DEFAULT '01'.
SELECTION-SCREEN: SKIP.
PARAMETERS: p_excomm AS CHECKBOX DEFAULT con_false,
p_nohits AS CHECKBOX DEFAULT con_false,
p_edit AS CHECKBOX DEFAULT space.
SELECTION-SCREEN: END OF BLOCK b.
Adapted scan-logic to make use of new select-option:
*&---------------------------------------------------------------------*
*& Form scan_prog
*&---------------------------------------------------------------------*
FORM scan_prog USING i_devclass TYPE devclass
i_objname TYPE sobj_name
i_cnt_line TYPE n
i_pgmid TYPE tadir-pgmid
i_object TYPE tadir-object
CHANGING i_tab_source TYPE t_tab_long_lines.
DATA: l_str_source TYPE t_abapsource_long,
* l_line TYPE sytabix,
* l_out_progname TYPE xfeld, "EC NEEDED
l_flg_found TYPE xfeld,
l_flg_write TYPE xfeld,
l_cnt_line TYPE i,
* l_modulo TYPE i,
l_str_lines TYPE t_str_lines.
* Initialization
* CLEAR l_out_progname.
CLEAR l_flg_found.
g_line_object = i_objname.
l_cnt_line = 1000.
CLEAR l_str_lines.
l_str_lines-devclass = i_devclass.
l_str_lines-progname = i_objname.
l_str_lines-object = i_object. "001+
l_str_lines-pgmid = i_pgmid. "001+
l_str_lines-sysid = sy-sysid. "001+
"Search source for selection criteria
LOOP AT i_tab_source INTO l_str_source.
g_line_number = sy-tabix.
CLEAR l_flg_write.
IF l_str_source-line CS p_strg1 AND
( p_strg2 IS INITIAL OR
l_str_source-line CS p_strg2 ).
* "001 Begin
"Search string is found in line of code
"Check if none of the search terms to exclude is found. This is
"to avoid too many false hits and was changed from several
"parameter fields to a select-option so that it can be easily
"maintained in a variant regardless of how many terms should
"be excluded.
DATA(cnt_excl) = lines( s_excl ).
LOOP AT s_excl INTO DATA(excluded).
IF NOT l_str_source-line CS excluded-low.
cnt_excl = cnt_excl - 1.
ENDIF.
ENDLOOP.
IF cnt_excl LE 0 AND
( p_excomm IS INITIAL OR
l_str_source-line(1) <> '*' ).
l_flg_write = con_true.
l_cnt_line = 0.
ENDIF.
* "001 End
ENDIF.
IF l_flg_write = con_true OR l_cnt_line < i_cnt_line.
l_cnt_line = l_cnt_line + 1.
l_flg_found = con_true.
l_str_lines-linno = g_line_number.
l_str_lines-line = l_str_source-line.
APPEND l_str_lines TO g_tab_lines.
ENDIF.
ENDLOOP.
* No hits found
IF p_nohits = con_true AND l_flg_found IS INITIAL.
l_str_lines-linno = 1.
l_str_lines-line = 'No Hits'(014).
APPEND l_str_lines TO g_tab_lines.
ENDIF.
ENDFORM. " scan_prog
Saving the scan results in a Z-table
While working on the logic to add the fields, we decided to also add an option to the program to directly store the scan results in a new Z-table. That would spare us the tedious task to get and “massage” the results in a spreadsheet and then – with another program – upload them into the table.
New definition:
* "001 Begin
TYPES: BEGIN OF ty_objects,
mandt TYPE mandt,
obj_name TYPE sobj_name,
field TYPE z_field,
credat TYPE creationdt,
created_by TYPE uname,
ignore_obj TYPE check_1,
comments TYPE z_comment,
END OF ty_objects.
DATA: g_tab_objects TYPE SORTED TABLE OF ty_objects
WITH UNIQUE KEY obj_name field.
* "001 End
New block on the selection-screen:
SELECTION-SCREEN BEGIN OF BLOCK d WITH FRAME TITLE TEXT-s04.
PARAMETERS: p_del AS CHECKBOX DEFAULT con_false MODIF ID tab,
p_ins AS CHECKBOX DEFAULT con_false MODIF ID tab,
p_field TYPE z_field MODIF ID tab.
SELECTION-SCREEN END OF BLOCK d.
Refresh Z-Table:
*&---------------------------------------------------------------------*
*& Form update_objects_table
*&---------------------------------------------------------------------*
FORM update_objects_table.
DATA answer(1) TYPE c.
"Only proceed if no restrictions on objectname is entered and a term
"is specified in P_FIELD
IF s_rest[] IS INITIAL AND
p_field IS NOT INITIAL.
IF sy-batch EQ abap_false.
CALL FUNCTION 'POPUP_TO_CONFIRM'
EXPORTING
text_question = 'Update table ZBC_OBJECTS_WARN?'(a01)
IMPORTING
answer = answer.
"Only continue if update confirmed
CHECK answer EQ '1'.
ENDIF.
LOOP AT g_tab_lines INTO DATA(line).
IF line_exists( g_tab_objects[ obj_name = line-progname
field = p_field ] ).
"Don't do anything if line already exists
ELSE.
"Add progname to internal table of objects
g_tab_objects[] = VALUE #( BASE g_tab_objects
( mandt = sy-mandt
obj_name = line-progname
field = p_field
credat = sy-datum
created_by = sy-uname ) ).
ENDIF.
IF line-master IS NOT INITIAL.
IF line_exists( g_tab_objects[ obj_name = line-master
field = p_field ] ).
"Don't do anything if line already exists
ELSE.
"Add progname to internal table of objects
g_tab_objects[] = VALUE #( BASE g_tab_objects
( mandt = sy-mandt
obj_name = line-master
field = p_field
credat = sy-datum
created_by = sy-uname ) ).
ENDIF.
ENDIF.
IF line-funcname IS NOT INITIAL.
IF line_exists( g_tab_objects[ obj_name = line-funcname
field = p_field ] ).
"Don't do anything if line already exists
ELSE.
"Add progname to internal table of objects
g_tab_objects[] = VALUE #( BASE g_tab_objects
( mandt = sy-mandt
obj_name = line-funcname
field = p_field
credat = sy-datum
created_by = sy-uname ) ).
ENDIF.
ENDIF.
ENDLOOP.
IF p_del EQ abap_true.
DELETE FROM zbc_objects_warn
WHERE field EQ p_field
AND ignore_obj EQ space.
g_cnt_del = sy-dbcnt.
IF sy-subrc EQ 0.
MESSAGE i000(38) WITH sy-dbcnt
' entries deleted from ZBC_OBJECTS_WARN'(a02).
ENDIF.
ENDIF.
IF p_ins EQ abap_true.
INSERT zbc_objects_warn
FROM TABLE g_tab_objects ACCEPTING DUPLICATE KEYS.
g_cnt_ins = sy-dbcnt.
IF sy-subrc EQ 0.
MESSAGE i000(38) WITH sy-dbcnt
' entries inserted into ZBC_OBJECTS_WARN'(a03).
ENDIF.
ENDIF.
ENDIF.
ENDFORM.
We plan to set up a job refreshing the Z-table in each relevant system once per month. Over time – as code gets fixed – we hope to see a decline in table entries, but only time will tell how effective this will be!
The code I now have is working well on a NW750-system with SP25 and EHP8 and scans our many Z-objects in about 5 minutes.
Additional musings
I was thinking about utilizing REGEX to more easily eliminate false hits. Right now, only exact hits are excluded, so I’d theoretically have to add multiple entries in the select-options to find all versions for “field_a = sy-uname” regardless of the number of spaces there are before and after “=”. I briefly tried but didn’t get it to work and abandoned the effort in the interest of time. So, if anybody has suggestions of how to improve the exclusion logic with the help of – simple – REGEX – I’m all ears!