In the preparation phase of our S/4 migration we wanted to identify unused custom code. While there are several different reports from SAP, none of them fulfilled our requirements, so we created an own report to detect unused code.

First of all you need to have SCMON enabled for a rather long period (SAP suggests to have it active for at least 13 months). We created a snapshot of the SCMON via SUSG and exported the snapshot to our development system. The usage data is the basis of our report. We combined this with a remote existence check (to check if the object is also in our productive system).

1. Get all relevant objects to check

This select will get us all objects to check along with needed meta data:

1
2
3
4
5
6
7
8
9
10
11
12
13
" get the relevant programs/classes/function groups - Local system
SELECT DISTINCT object, tadir~obj_name, progdir~subc AS psubc, progdir~cdat AS pcdat, tcode
  FROM tadir
  LEFT OUTER JOIN progdir
    ON progdir~name EQ tadir~obj_name
  LEFT OUTER JOIN tstc
    ON tstc~pgmna EQ tadir~obj_name
  INTO TABLE @DATA(lt_objname)
  WHERE object IN @s_objtyp
    AND tadir~obj_name IN @s_objnam
    AND tadir~delflag EQ @abap_false
    AND tadir~genflag EQ @abap_false
    AND tadir~devclass IN @s_devc.

2. Check in productive system

If we have a remote RFC system, we will check those objects for existence in the destination:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
METHOD get_remote_objects.

  DATA: lf_where_clause TYPE string,
        lf_message      TYPE char1024.

  DATA: lt_options TYPE TABLE OF rfc_db_opt,
        lt_temp    TYPE TABLE OF rfc_db_opt,
        lt_fields  TYPE TABLE OF rfc_db_fld,
        lt_data    TYPE TABLE OF t_remote_tadir.

  APPEND ` delflag EQ '' ` TO lt_options.
  APPEND ` AND genflag EQ '' ` TO lt_options.

  " object type
  IF s_objtyp[] IS NOT INITIAL.
    lt_temp = morph_selopt_to_rfc( if_fieldname = 'object' it_selopt = s_objtyp[] ).
    IF lt_temp IS NOT INITIAL.
      APPEND ` AND  ` TO lt_options.
      APPEND LINES OF lt_temp TO lt_options.
    ENDIF.
  ENDIF.

  " object name
  IF s_objnam[] IS NOT INITIAL.
    lt_temp = morph_selopt_to_rfc( if_fieldname = 'obj_name' it_selopt = s_objnam[] ).
    IF lt_temp IS NOT INITIAL.
      APPEND ` AND  ` TO lt_options.
      APPEND LINES OF lt_temp TO lt_options.
    ENDIF.
  ENDIF.

  " package
  IF s_devc[] IS NOT INITIAL.
    lt_temp = morph_selopt_to_rfc( if_fieldname = 'devclass' it_selopt = s_devc[] ).
    IF lt_temp IS NOT INITIAL.
      APPEND ` AND  ` TO lt_options.
      APPEND LINES OF lt_temp TO lt_options.
    ENDIF.
  ENDIF.

  APPEND VALUE #( fieldname = 'OBJECT' ) TO lt_fields.
  APPEND VALUE #( fieldname = 'OBJ_NAME' ) TO lt_fields.

  " get objects
  CALL FUNCTION 'RFC_READ_TABLE'
    DESTINATION p_rfcdes
    EXPORTING
      query_table           = 'TADIR'
    TABLES
      options               = lt_options
      fields                = lt_fields
      data                  = lt_data
    EXCEPTIONS
      system_failure        = 1 MESSAGE lf_message
      communication_failure = 2 MESSAGE lf_message
      OTHERS                = 3.

  LOOP AT lt_data ASSIGNING FIELD-SYMBOL(<s_data>).
    INSERT <s_data> INTO TABLE gt_remote_objects.
  ENDLOOP.

ENDMETHOD.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
METHOD morph_selopt_to_rfc.

  DATA: lf_where TYPE string,
        lf_bit   TYPE so_text,
        lf_len   TYPE int4.

  DATA lt_selopt TYPE TABLE OF ddshselopt.

  LOOP AT it_selopt ASSIGNING FIELD-SYMBOL(<s_selopt>).
    DATA(ls_selopt) = CORRESPONDING ddshselopt( <s_selopt> ).
    ls_selopt-shlpfield = if_fieldname.
    APPEND ls_selopt TO lt_selopt.
  ENDLOOP.

  CALL FUNCTION 'F4_CONV_SELOPT_TO_WHERECLAUSE'
    IMPORTING
      where_clause = lf_where
    TABLES
      selopt_tab   = lt_selopt.

  CHECK lf_where IS NOT INITIAL.

  DO.
    IF lf_where IS INITIAL.
      EXIT.
    ENDIF.

    CLEAR lf_len.
    lf_len = strlen( lf_where ).

    IF lf_len <= 72.
      APPEND lf_where TO rt_result.
      EXIT.
    ELSE.
      lf_bit = lf_where.
      lf_len = 71.
      DO 71 TIMES.
        IF lf_bit+lf_len(1) = ' '.
          EXIT.
        ENDIF.
        lf_len = lf_len - 1.
      ENDDO.

      APPEND lf_bit(lf_len) TO rt_result.
      lf_where = lf_where+lf_len.
    ENDIF.
  ENDDO.

ENDMETHOD.

3. Get SCMON data and create the result

In the last step we get the SCMON data and build up a result for the ALV grid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
LOOP AT lt_objname ASSIGNING FIELD-SYMBOL(<s_objname>).
  DATA(lf_remote) = abap_false.

  " CHECK if available in remote
  READ TABLE gt_remote_objects WITH KEY object = <s_objname>-object
                                        obj_name = <s_objname>-obj_name
                                TRANSPORTING NO FIELDS.
  IF sy-subrc EQ 0.
    lf_remote = abap_true.

  ENDIF.

  CASE <s_objname>-object.
    WHEN 'PROG'.
      " Report/Include
      CHECK <s_objname>-psubc IN s_psubc.
      CHECK <s_objname>-pcdat IN s_pcdat.

      IF <s_objname>-psubc EQ '1'.
        " Program will be used as is
        LOOP AT get_usage_data_prog( <s_objname>-obj_name ) ASSIGNING FIELD-SYMBOL(<s_outtab>).
          <s_outtab>-cdat = <s_objname>-pcdat.
          <s_outtab>-remote = lf_remote.
          <s_outtab>-tcode = <s_objname>-tcode.

          SELECT SINGLE text
            FROM trdirt
            INTO @<s_outtab>-title
            WHERE name EQ @<s_objname>-obj_name
              AND sprsl EQ @sy-langu.
          IF sy-subrc NE 0.
            SELECT SINGLE text
              FROM trdirt
              INTO @<s_outtab>-title
              WHERE name EQ @<s_objname>-obj_name.

          ENDIF.

          APPEND <s_outtab> TO gt_outtab.

        ENDLOOP.

      ELSE.
        " Include will be "extracted" to the main program
        LOOP AT get_usage_data_include( <s_objname>-obj_name ) ASSIGNING <s_outtab>.
          <s_outtab>-cdat = <s_objname>-pcdat.
          <s_outtab>-remote = lf_remote.
          <s_outtab>-tcode = <s_objname>-tcode.
          APPEND <s_outtab> TO gt_outtab.

        ENDLOOP.

      ENDIF.

    WHEN 'CLAS'.
      " Class
      LOOP AT get_usage_data_clas( <s_objname>-obj_name ) ASSIGNING <s_outtab>.
        <s_outtab>-cdat = <s_objname>-pcdat.
        <s_outtab>-remote = lf_remote.
        APPEND <s_outtab> TO gt_outtab.

      ENDLOOP.

    WHEN 'FUGR'.
      " Function group
      LOOP AT get_usage_data_fugr( <s_objname>-obj_name ) ASSIGNING <s_outtab>.
        <s_outtab>-cdat = <s_objname>-pcdat.
        <s_outtab>-remote = lf_remote.
        APPEND <s_outtab> TO gt_outtab.

      ENDLOOP.

    WHEN OTHERS.
      CONTINUE.

  ENDCASE.

ENDLOOP.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
METHOD get_usage_data_prog.

  DATA(lt_data) = get_usage_data_objects(
                    iv_name = iv_name
                    iv_type = 'PROG'
                    it_subobjects = VALUE #( ( type = 'PROG' name = iv_name ) ) ).

  CHECK lt_data IS NOT INITIAL.

  SORT lt_data BY last_used DESCENDING.
  READ TABLE lt_data INTO DATA(ls_data) INDEX 1.

  APPEND ls_data TO rt_data.

ENDMETHOD.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
METHOD get_usage_data_include.

  DATA: lt_subobjects TYPE tt_subobject.

  SELECT master, include
    FROM wbcrossi
    INTO TABLE @DATA(lt_wbcrossi)
    WHERE name EQ @iv_name.

  LOOP AT lt_wbcrossi ASSIGNING FIELD-SYMBOL(<s_wbcrossi>).
    IF <s_wbcrossi>-include EQ <s_wbcrossi>-master.
      " Include and master the same -> just add
      APPEND VALUE #( type = 'PROG' name = <s_wbcrossi>-master ) TO lt_subobjects.

    ELSE.
      " have a look if the include is part of a function module (User exit)
      DATA(lf_funcname) = VALUE rs38l_fnam( ).
      DATA(lf_group) = VALUE rs38l_area( ).
      DATA(lf_include) = CONV progname( <s_wbcrossi>-include ).

      CALL FUNCTION 'FUNCTION_INCLUDE_INFO'
        CHANGING
          funcname = lf_funcname
          group    = lf_group
          include  = lf_include
        EXCEPTIONS
          OTHERS   = 6.
      IF sy-subrc EQ 0.
        APPEND VALUE #( type = 'FUGS' name = lf_group subtype = 'FUNC' subname = lf_funcname ) TO lt_subobjects.

      ELSE.
        " Just add
        APPEND VALUE #( type = 'PROG' name = <s_wbcrossi>-master ) TO lt_subobjects.

      ENDIF.

    ENDIF.

  ENDLOOP.

  DATA(lt_data) = get_usage_data_objects( iv_name = iv_name iv_type = 'PROG' it_subobjects = lt_subobjects ).

  CHECK lt_data IS NOT INITIAL.

  SORT lt_data BY last_used DESCENDING.
  READ TABLE lt_data INTO DATA(ls_data) INDEX 1.

  APPEND ls_data TO rt_data.

ENDMETHOD.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
METHOD get_usage_data_clas.

  DATA: lt_subobjects TYPE tt_subobject.

  SELECT methodname
    FROM tmdir
    INTO TABLE @DATA(lt_methods)
    WHERE classname EQ @iv_name
      AND methodname NE @space.

  LOOP AT lt_methods ASSIGNING FIELD-SYMBOL(<s_method>).
    APPEND VALUE #( type = 'CLAS' name = iv_name subtype = 'METH' subname = <s_method>-methodname ) TO lt_subobjects.

  ENDLOOP.

  APPEND LINES OF get_usage_data_objects( iv_name = iv_name iv_type = 'CLAS' it_subobjects = lt_subobjects ) TO rt_data.

ENDMETHOD.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
METHOD get_usage_data_fugr.

  DATA: lt_subobjects TYPE tt_subobject.

  DATA(lf_groupname) = |SAPL{ iv_name }|.

  SELECT funcname
    FROM tfdir
    INTO TABLE @DATA(lt_functions)
    WHERE pname EQ @lf_groupname.

  LOOP AT lt_functions ASSIGNING FIELD-SYMBOL(<s_function>).
    APPEND VALUE #( type = 'FUGR' name = iv_name subtype = 'FUNC' subname = <s_function>-funcname ) TO lt_subobjects.

  ENDLOOP.

  APPEND LINES OF get_usage_data_objects( iv_name = iv_name iv_type = 'FUGR' it_subobjects = lt_subobjects ) TO rt_data.

ENDMETHOD.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
METHOD get_usage_data_objects.

  DATA: lr_subtype TYPE RANGE OF scmon_proctype,
        lr_subname TYPE RANGE OF scmon_procname.

  LOOP AT it_subobjects ASSIGNING FIELD-SYMBOL(<s_subobject>).
    DATA(ls_outtab) = VALUE t_outtab( ).

    lr_subtype = VALUE #( ).
    lr_subname = VALUE #( ).

    CASE iv_type.
      WHEN 'PROG'.
        ls_outtab-type = 'PROG'.
        ls_outtab-parent = <s_subobject>-name.
        ls_outtab-name = iv_name.

      WHEN 'CLAS'.
        ls_outtab-type = 'METH'.
        ls_outtab-parent = <s_subobject>-name.
        ls_outtab-name = <s_subobject>-subname.
        APPEND VALUE #( sign = 'I' option = 'EQ' low = <s_subobject>-subtype ) TO lr_subtype.
        APPEND VALUE #( sign = 'I' option = 'EQ' low = <s_subobject>-subname ) TO lr_subname.

      WHEN 'FUGR'.
        ls_outtab-type = 'FUNC'.
        ls_outtab-parent = <s_subobject>-name.
        ls_outtab-name = <s_subobject>-subname.
        APPEND VALUE #( sign = 'I' option = 'EQ' low = <s_subobject>-subtype ) TO lr_subtype.
        APPEND VALUE #( sign = 'I' option = 'EQ' low = <s_subobject>-subname ) TO lr_subname.

    ENDCASE.

    IF ls_outtab-parent(4) EQ 'SAPL'.
      ls_outtab-parent = ls_outtab-parent+4.
    ENDIF.

    SELECT SINGLE devclass
      FROM tadir
      INTO @ls_outtab-package
      WHERE obj_name EQ @ls_outtab-parent
        AND object IN ('PROG', 'FUGR', 'FUGS', 'CLAS').

    SELECT MAX( last_used ), MAX( counter )
      FROM susg_v_data
      INTO (@ls_outtab-last_used, @ls_outtab-executions)
      WHERE usgid    EQ @gf_usgid
        AND obj_type EQ @<s_subobject>-type
        AND obj_name EQ @<s_subobject>-name
        AND sub_type IN @lr_subtype
        AND sub_name IN @lr_subname.

    APPEND ls_outtab TO rt_data.

  ENDLOOP.

ENDMETHOD.

Using this report we were able to identify lots of unsed programs (and even programs which never arrived on our productive server).