LCOV - code coverage report
Current view: top level - src/api - hsd_validation.f90 (source / functions) Coverage Total Hit
Test: coverage.info Lines: 94.2 % 206 194
Test Date: 2026-03-19 13:06:14 Functions: 100.0 % 12 12

            Line data    Source code
       1              : !> HSD validation and verification
       2              : !>
       3              : !> This module provides utilities for validating HSD data, including
       4              : !> required field checking, range validation, and unit conversion support.
       5              : module hsd_validation
       6              :   use hsd_constants, only: dp
       7              :   use hsd_utils, only: to_lower
       8              :   use hsd_error, only: hsd_error_t, make_error, HSD_STAT_OK, HSD_STAT_NOT_FOUND, &
       9              :     HSD_STAT_TYPE_ERROR
      10              :   use hsd_types, only: hsd_node_t, NODE_TYPE_TABLE, NODE_TYPE_VALUE, &
      11              :     VALUE_TYPE_NONE, VALUE_TYPE_STRING, VALUE_TYPE_INTEGER, &
      12              :     VALUE_TYPE_REAL, VALUE_TYPE_LOGICAL, VALUE_TYPE_ARRAY, VALUE_TYPE_COMPLEX
      13              :   use hsd_api, only: hsd_get_child
      14              :   implicit none (type, external)
      15              :   private
      16              : 
      17              :   ! Public procedures
      18              :   public :: hsd_require
      19              :   public :: hsd_validate_range
      20              :   public :: hsd_validate_one_of
      21              :   public :: hsd_get_with_unit
      22              :   public :: hsd_get_array_with_unit
      23              :   public :: hsd_get_matrix_with_unit
      24              :   public :: hsd_node_context
      25              :   public :: hsd_format_error
      26              :   public :: hsd_format_warning
      27              :   public :: hsd_warn_unprocessed
      28              :   public :: MAX_WARNING_LEN
      29              : 
      30              :   !> Maximum length for warning message strings
      31              :   integer, parameter :: MAX_WARNING_LEN = 256
      32              : 
      33              : contains
      34              : 
      35              :   !> Require that a path exists and optionally check its type
      36              :   !>
      37              :   !> If the path doesn't exist or type doesn't match, creates a descriptive error.
      38              :   !>
      39              :   !> Both optional arguments should be passed as keyword arguments for clarity:
      40              :   !>
      41              :   !>   call hsd_require(root, "Driver/MaxSteps", error, &
      42              :   !>     expected_type=FIELD_TYPE_INTEGER, context="load_config")
      43           18 :   subroutine hsd_require(table, path, error, expected_type, context)
      44              :     type(hsd_node_t), intent(in), target :: table
      45              :     character(len=*), intent(in) :: path
      46              :     type(hsd_error_t), allocatable, intent(out) :: error
      47              :     integer, intent(in), optional :: expected_type
      48              :     character(len=*), intent(in), optional :: context
      49              : 
      50              :     type(hsd_node_t), pointer :: child
      51           18 :     integer :: local_stat, actual_type
      52           18 :     character(len=:), allocatable :: ctx_prefix
      53              : 
      54           18 :     call hsd_get_child(table, path, child, local_stat)
      55              : 
      56           18 :     if (present(context)) then
      57            2 :       ctx_prefix = context // ": "
      58              :     else
      59           16 :       ctx_prefix = ""
      60              :     end if
      61              : 
      62           18 :     if (local_stat /= 0 .or. .not. associated(child)) then
      63              :       call make_error(error, HSD_STAT_NOT_FOUND, &
      64            5 :         ctx_prefix // "Required field '" // path // "' not found")
      65            5 :       return
      66              :     end if
      67              : 
      68           13 :     if (present(expected_type)) then
      69           10 :       if (child%node_type == NODE_TYPE_VALUE) then
      70            9 :         actual_type = child%value_type
      71            9 :         if (expected_type /= actual_type) then
      72              :           call make_error(error, HSD_STAT_TYPE_ERROR, &
      73              :             ctx_prefix // "Field '" // path // &
      74              :             & "' has wrong type: expected " // &
      75              :             type_name(expected_type) // ", got " // &
      76            8 :             & type_name(actual_type))
      77              :         end if
      78            1 :       else if (child%node_type == NODE_TYPE_TABLE) then
      79            1 :         if (expected_type /= VALUE_TYPE_NONE) then
      80              :           call make_error(error, HSD_STAT_TYPE_ERROR, &
      81              :             ctx_prefix // "Field '" // path // &
      82              :             & "' is a table, expected value of type " // &
      83            1 :             type_name(expected_type))
      84              :         end if
      85              :       end if
      86              :     end if
      87              : 
      88           18 :   end subroutine hsd_require
      89              : 
      90              :   !> Get a human-readable name for a value type
      91           17 :   pure function type_name(val_type) result(name)
      92              :     integer, intent(in) :: val_type
      93              :     character(len=:), allocatable :: name
      94              : 
      95           18 :     select case (val_type)
      96              :     case (VALUE_TYPE_NONE)
      97            1 :       name = "none"
      98              :     case (VALUE_TYPE_STRING)
      99            8 :       name = "string"
     100              :     case (VALUE_TYPE_INTEGER)
     101            3 :       name = "integer"
     102              :     case (VALUE_TYPE_REAL)
     103            1 :       name = "real"
     104              :     case (VALUE_TYPE_LOGICAL)
     105            1 :       name = "logical"
     106              :     case (VALUE_TYPE_ARRAY)
     107            1 :       name = "array"
     108              :     case (VALUE_TYPE_COMPLEX)
     109            1 :       name = "complex"
     110              :     case default
     111            1 :       name = "unknown"
     112              :     end select
     113              : 
     114           18 :   end function type_name
     115              : 
     116              :   !> Validate that a real value is within a specified range
     117           12 :   subroutine hsd_validate_range(table, path, min_val, max_val, error, context)
     118              :     type(hsd_node_t), intent(in), target :: table
     119              :     character(len=*), intent(in) :: path
     120              :     real(dp), intent(in) :: min_val, max_val
     121              :     type(hsd_error_t), allocatable, intent(out) :: error
     122              :     character(len=*), intent(in), optional :: context
     123              : 
     124              :     type(hsd_node_t), pointer :: child
     125           12 :     real(dp) :: val
     126           12 :     integer :: local_stat
     127              :     character(len=32) :: min_str, max_str, val_str
     128           12 :     character(len=:), allocatable :: ctx_prefix
     129              : 
     130           12 :     call hsd_get_child(table, path, child, local_stat)
     131              : 
     132           12 :     if (present(context)) then
     133            1 :       ctx_prefix = context // ": "
     134              :     else
     135           11 :       ctx_prefix = ""
     136              :     end if
     137              : 
     138           12 :     if (local_stat /= HSD_STAT_OK .or. .not. associated(child)) then
     139              :       call make_error(error, local_stat, &
     140            2 :         ctx_prefix // "Field '" // path // "' not found or invalid type for range validation")
     141            2 :       return
     142              :     end if
     143              : 
     144           10 :     if (child%node_type == NODE_TYPE_VALUE) then
     145            8 :       call child%get_real(val, local_stat)
     146            8 :       if (local_stat /= HSD_STAT_OK) then
     147              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     148              :           ctx_prefix // "Field '" // path // &
     149            1 :           & "' is not a real number")
     150            1 :         return
     151              :       end if
     152              : 
     153            7 :       if (val < min_val .or. val > max_val) then
     154            5 :         write(min_str, '(G0)') min_val
     155            5 :         write(max_str, '(G0)') max_val
     156            5 :         write(val_str, '(G0)') val
     157              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     158              :           ctx_prefix // "Field '" // path // &
     159              :           & "' value " // trim(val_str) // &
     160              :           " is outside valid range [" // trim(min_str) // &
     161            5 :           & ", " // trim(max_str) // "]")
     162              :       end if
     163              :     else
     164              :       call make_error(error, HSD_STAT_TYPE_ERROR, &
     165              :         ctx_prefix // "Field '" // path // &
     166            2 :         & "' is not a value node")
     167              :     end if
     168              : 
     169           29 :   end subroutine hsd_validate_range
     170              : 
     171              :   !> Validate that a string value is one of the allowed choices
     172           18 :   subroutine hsd_validate_one_of(table, path, choices, error, context)
     173              :     type(hsd_node_t), intent(in), target :: table
     174              :     character(len=*), intent(in) :: path
     175              :     character(len=*), intent(in) :: choices(:)
     176              :     type(hsd_error_t), allocatable, intent(out) :: error
     177              :     character(len=*), intent(in), optional :: context
     178              : 
     179              :     type(hsd_node_t), pointer :: child
     180            9 :     character(len=:), allocatable :: val, choices_str, ctx_prefix
     181            9 :     integer :: i, local_stat
     182            9 :     logical :: found
     183              : 
     184            9 :     call hsd_get_child(table, path, child, local_stat)
     185              : 
     186            9 :     if (present(context)) then
     187            1 :       ctx_prefix = context // ": "
     188              :     else
     189            8 :       ctx_prefix = ""
     190              :     end if
     191              : 
     192            9 :     if (local_stat /= HSD_STAT_OK .or. .not. associated(child)) then
     193              :       call make_error(error, local_stat, &
     194            1 :         ctx_prefix // "Field '" // path // "' not found")
     195            1 :       return
     196              :     end if
     197              : 
     198            8 :     if (child%node_type == NODE_TYPE_VALUE) then
     199            6 :       call child%get_string(val, local_stat)
     200            6 :       if (local_stat /= HSD_STAT_OK) then
     201              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     202              :           ctx_prefix // "Field '" // path // &
     203            0 :           & "' is not a string")
     204            0 :         return
     205              :       end if
     206              : 
     207            6 :       found = .false.
     208           16 :       do i = 1, size(choices)
     209           42 :         if (to_lower(val) == to_lower(choices(i))) then
     210            3 :           found = .true.
     211           26 :           exit
     212              :         end if
     213              :       end do
     214              : 
     215            6 :       if (.not. found) then
     216            3 :         choices_str = ""
     217           12 :         do i = 1, size(choices)
     218           15 :           if (i > 1) choices_str = choices_str // ", "
     219              :           choices_str = choices_str // "'" // &
     220           12 :               & trim(choices(i)) // "'"
     221              :         end do
     222              :         call make_error(error, HSD_STAT_TYPE_ERROR, &
     223              :           ctx_prefix // "Field '" // path // &
     224              :           & "' value '" // val // &
     225            3 :           "' is not one of: " // choices_str)
     226              :       end if
     227              :     else
     228              :       call make_error(error, HSD_STAT_TYPE_ERROR, &
     229              :         ctx_prefix // "Field '" // path // &
     230            2 :         & "' is not a value node")
     231              :     end if
     232              : 
     233           21 :   end subroutine hsd_validate_one_of
     234              : 
     235              :   !> Get a real value with automatic unit conversion
     236              :   !>
     237              :   !> The unit is read from the node's attribute and converted to the target unit.
     238              :   !> The converter function takes (value, from_unit, to_unit) and returns the converted value.
     239              :   !>
     240              :   !> Example:
     241              :   !>   For input `Temperature [Kelvin] = 300`, calling
     242              :   !>   `hsd_get_with_unit(root, "Temperature", val, "Celsius", converter)`
     243              :   !>   would call `converter(300.0, "Kelvin", "Celsius")` to get the result.
     244            5 :   subroutine hsd_get_with_unit(table, path, val, target_unit, converter, stat)
     245              :     type(hsd_node_t), intent(in), target :: table
     246              :     character(len=*), intent(in) :: path
     247              :     real(dp), intent(out) :: val
     248              :     character(len=*), intent(in) :: target_unit
     249              :     interface
     250              :       pure function converter(value, from_unit, to_unit) result(converted)
     251              :         import :: dp
     252              :         implicit none (type, external)
     253              :         real(dp), intent(in) :: value
     254              :         character(len=*), intent(in) :: from_unit, to_unit
     255              :         real(dp) :: converted
     256              :       end function converter
     257              :     end interface
     258              :     integer, intent(out), optional :: stat
     259              : 
     260              :     type(hsd_node_t), pointer :: child
     261            5 :     character(len=:), allocatable :: source_unit
     262            5 :     real(dp) :: raw_val
     263            5 :     integer :: local_stat
     264              : 
     265            5 :     val = 0.0_dp
     266              : 
     267            5 :     call hsd_get_child(table, path, child, local_stat)
     268            5 :     if (local_stat /= 0 .or. .not. associated(child)) then
     269            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     270            1 :       return
     271              :     end if
     272              : 
     273            4 :     if (child%node_type == NODE_TYPE_VALUE) then
     274            3 :       call child%get_real(raw_val, local_stat)
     275            3 :       if (local_stat /= HSD_STAT_OK) then
     276            1 :         if (present(stat)) stat = local_stat
     277            1 :         return
     278              :       end if
     279              : 
     280            2 :       if (allocated(child%attrib)) then
     281            1 :         source_unit = child%attrib
     282              :       else
     283            1 :         source_unit = target_unit  ! No conversion needed
     284              :       end if
     285              : 
     286            2 :       val = converter(raw_val, source_unit, target_unit)
     287            2 :       if (present(stat)) stat = HSD_STAT_OK
     288              : 
     289              :     else
     290            1 :       if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     291              :     end if
     292              : 
     293           14 :   end subroutine hsd_get_with_unit
     294              : 
     295              :   !> Get a real array with unit conversion
     296              :   !>
     297              :   !> Reads a real(:) array from the node at `path`, extracts the unit from
     298              :   !> the node's `attrib` field, and applies the `converter` function to each
     299              :   !> element. If no unit attribute is present, assumes `target_unit`.
     300            4 :   subroutine hsd_get_array_with_unit(table, path, val, target_unit, converter, stat)
     301              :     type(hsd_node_t), intent(in), target :: table
     302              :     character(len=*), intent(in) :: path
     303              :     real(dp), allocatable, intent(out) :: val(:)
     304              :     character(len=*), intent(in) :: target_unit
     305              :     interface
     306              :       pure function converter(value, from_unit, to_unit) result(converted)
     307              :         import :: dp
     308              :         implicit none (type, external)
     309              :         real(dp), intent(in) :: value
     310              :         character(len=*), intent(in) :: from_unit, to_unit
     311              :         real(dp) :: converted
     312              :       end function converter
     313              :     end interface
     314              :     integer, intent(out), optional :: stat
     315              : 
     316              :     type(hsd_node_t), pointer :: child
     317            4 :     character(len=:), allocatable :: source_unit
     318            4 :     integer :: local_stat, i
     319              : 
     320            4 :     call hsd_get_child(table, path, child, local_stat)
     321            4 :     if (local_stat /= 0 .or. .not. associated(child)) then
     322            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     323            1 :       allocate(val(0))
     324            1 :       return
     325              :     end if
     326              : 
     327            3 :     if (child%node_type == NODE_TYPE_VALUE) then
     328            2 :       call child%get_real_array(val, local_stat)
     329            2 :       if (local_stat /= HSD_STAT_OK) then
     330            0 :         if (present(stat)) stat = local_stat
     331            0 :         return
     332              :       end if
     333              : 
     334            2 :       if (allocated(child%attrib)) then
     335            1 :         source_unit = child%attrib
     336              :       else
     337            1 :         source_unit = target_unit
     338              :       end if
     339              : 
     340            7 :       do i = 1, size(val)
     341            7 :         val(i) = converter(val(i), source_unit, target_unit)
     342              :       end do
     343              : 
     344            2 :       if (present(stat)) stat = HSD_STAT_OK
     345              : 
     346              :     else
     347            1 :       if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     348            1 :       allocate(val(0))
     349              :     end if
     350              : 
     351            9 :   end subroutine hsd_get_array_with_unit
     352              : 
     353              :   !> Get a real matrix with unit conversion
     354              :   !>
     355              :   !> Reads a real(:,:) matrix from the node at `path`, extracts the unit from
     356              :   !> the node's `attrib` field, and applies the `converter` function to each
     357              :   !> element. If no unit attribute is present, assumes `target_unit`.
     358            3 :   subroutine hsd_get_matrix_with_unit(table, path, val, nrows, ncols, &
     359              :       target_unit, converter, stat)
     360              :     type(hsd_node_t), intent(in), target :: table
     361              :     character(len=*), intent(in) :: path
     362              :     real(dp), allocatable, intent(out) :: val(:,:)
     363              :     integer, intent(out) :: nrows, ncols
     364              :     character(len=*), intent(in) :: target_unit
     365              :     interface
     366              :       pure function converter(value, from_unit, to_unit) result(converted)
     367              :         import :: dp
     368              :         implicit none (type, external)
     369              :         real(dp), intent(in) :: value
     370              :         character(len=*), intent(in) :: from_unit, to_unit
     371              :         real(dp) :: converted
     372              :       end function converter
     373              :     end interface
     374              :     integer, intent(out), optional :: stat
     375              : 
     376              :     type(hsd_node_t), pointer :: child
     377            3 :     character(len=:), allocatable :: source_unit
     378            3 :     integer :: local_stat, i, j
     379              : 
     380            3 :     nrows = 0
     381            3 :     ncols = 0
     382              : 
     383            3 :     call hsd_get_child(table, path, child, local_stat)
     384            3 :     if (local_stat /= 0 .or. .not. associated(child)) then
     385            1 :       if (present(stat)) stat = HSD_STAT_NOT_FOUND
     386            1 :       allocate(val(0, 0))
     387            1 :       return
     388              :     end if
     389              : 
     390            2 :     if (child%node_type == NODE_TYPE_VALUE) then
     391            1 :       call child%get_real_matrix(val, nrows, ncols, local_stat)
     392            1 :       if (local_stat /= HSD_STAT_OK) then
     393            0 :         if (present(stat)) stat = local_stat
     394            0 :         return
     395              :       end if
     396              : 
     397            1 :       if (allocated(child%attrib)) then
     398            1 :         source_unit = child%attrib
     399              :       else
     400            0 :         source_unit = target_unit
     401              :       end if
     402              : 
     403            4 :       do j = 1, ncols
     404           10 :         do i = 1, nrows
     405            6 :           val(i, j) = converter(val(i, j), source_unit, &
     406           15 :               & target_unit)
     407              :         end do
     408              :       end do
     409              : 
     410            1 :       if (present(stat)) stat = HSD_STAT_OK
     411              : 
     412              :     else
     413            1 :       if (present(stat)) stat = HSD_STAT_TYPE_ERROR
     414            1 :       allocate(val(0, 0))
     415              :     end if
     416              : 
     417            7 :   end subroutine hsd_get_matrix_with_unit
     418              : 
     419              : 
     420              :   !> Get context string for an HSD node (for error messages)
     421              :   !>
     422              :   !> Returns "NodeName (line N)" if line info is available, or just "NodeName".
     423              :   !> Returns "" if the node has no name.
     424            6 :   function hsd_node_context(node) result(ctx)
     425              :     type(hsd_node_t), intent(in) :: node
     426              :     character(len=:), allocatable :: ctx
     427              : 
     428              :     character(len=20) :: linebuf
     429              : 
     430            6 :     if (allocated(node%name) .and. node%line > 0) then
     431            3 :       write(linebuf, '(i0)') node%line
     432            3 :       ctx = "'" // node%name // "' (line " // trim(linebuf) // ")"
     433            3 :     else if (allocated(node%name)) then
     434            1 :       ctx = "'" // node%name // "'"
     435              :     else
     436            2 :       ctx = ""
     437              :     end if
     438              : 
     439            3 :   end function hsd_node_context
     440              : 
     441              : 
     442              :   !> Format an error message with node context
     443              :   !>
     444              :   !> Returns "Error in 'NodeName' (line N): <msg>" or just <msg> if no context.
     445              :   !> This is a general-purpose utility for producing user-friendly error messages
     446              :   !> that include the location in the input file where the error occurred.
     447            2 :   subroutine hsd_format_error(node, msg, formatted)
     448              :     type(hsd_node_t), intent(in) :: node
     449              :     character(len=*), intent(in) :: msg
     450              :     character(len=:), allocatable, intent(out) :: formatted
     451              : 
     452            2 :     character(len=:), allocatable :: ctx
     453              : 
     454            2 :     ctx = hsd_node_context(node)
     455            2 :     if (len(ctx) > 0) then
     456            1 :       formatted = "Error in " // ctx // ": " // msg
     457              :     else
     458            1 :       formatted = msg
     459              :     end if
     460              : 
     461            8 :   end subroutine hsd_format_error
     462              : 
     463              : 
     464              :   !> Format a warning message with node context
     465              :   !>
     466              :   !> Returns "Warning in 'NodeName' (line N): <msg>" or just <msg> if no context.
     467            2 :   subroutine hsd_format_warning(node, msg, formatted)
     468              :     type(hsd_node_t), intent(in) :: node
     469              :     character(len=*), intent(in) :: msg
     470              :     character(len=:), allocatable, intent(out) :: formatted
     471              : 
     472            2 :     character(len=:), allocatable :: ctx
     473              : 
     474            2 :     ctx = hsd_node_context(node)
     475            2 :     if (len(ctx) > 0) then
     476            1 :       formatted = "Warning in " // ctx // ": " // msg
     477              :     else
     478            1 :       formatted = msg
     479              :     end if
     480              : 
     481            4 :   end subroutine hsd_format_warning
     482              : 
     483              : 
     484              :   !> Walk a table's children and collect warnings for unprocessed nodes.
     485              :   !>
     486              :   !> Any non-"#text" child that has `processed == .false.` and `line > 0`
     487              :   !> (i.e., it came from the parsed input, not programmatic defaults) is
     488              :   !> reported. Processed table children are recursed into so that deeply
     489              :   !> nested unprocessed nodes are also caught.
     490              :   !>
     491              :   !> Usage:
     492              :   !> ```fortran
     493              :   !> character(len=MAX_WARNING_LEN), allocatable :: warnings(:)
     494              :   !> call hsd_warn_unprocessed(root, warnings)
     495              :   !> do i = 1, size(warnings)
     496              :   !>   write(*, '(A)') trim(warnings(i))
     497              :   !> end do
     498              :   !> ```
     499            2 :   subroutine hsd_warn_unprocessed(root, warnings)
     500              :     type(hsd_node_t), intent(in) :: root
     501              :     character(len=MAX_WARNING_LEN), allocatable, intent(out) :: warnings(:)
     502              : 
     503            2 :     character(len=MAX_WARNING_LEN), allocatable :: tmp(:)
     504            2 :     integer :: nwarn
     505              : 
     506            2 :     allocate(tmp(32))
     507            2 :     nwarn = 0
     508            2 :     call collect_unprocessed_(root, tmp, nwarn)
     509            2 :     allocate(warnings(nwarn))
     510           43 :     if (nwarn > 0) warnings(:) = tmp(1:nwarn)
     511              : 
     512            4 :   end subroutine hsd_warn_unprocessed
     513              : 
     514              : 
     515              :   !> Recursive helper: collect unprocessed node warnings into a growable array.
     516            0 :   recursive subroutine collect_unprocessed_(node, buf, nwarn)
     517              :     type(hsd_node_t), intent(in) :: node
     518              :     character(len=MAX_WARNING_LEN), allocatable, intent(inout) :: buf(:)
     519              :     integer, intent(inout) :: nwarn
     520              : 
     521            2 :     integer :: ii
     522              :     type(hsd_node_t), pointer :: child
     523            2 :     character(len=MAX_WARNING_LEN), allocatable :: tmp(:)
     524              :     character(len=20) :: linebuf
     525              : 
     526           44 :     do ii = 1, node%num_children
     527           42 :       call node%get_child(ii, child)
     528           42 :       if (.not. associated(child)) cycle
     529              : 
     530              :       ! Skip anonymous or #text value nodes
     531           42 :       if (.not. allocated(child%name)) cycle
     532           42 :       if (child%name == "#text") cycle
     533              : 
     534           42 :       if (.not. child%processed .and. child%line > 0) then
     535              :         ! Grow buffer if needed
     536           41 :         if (nwarn >= size(buf)) then
     537            1 :           allocate(tmp(size(buf) * 2))
     538           33 :           tmp(1:nwarn) = buf(1:nwarn)
     539            1 :           call move_alloc(tmp, buf)
     540              :         end if
     541           41 :         nwarn = nwarn + 1
     542           41 :         if (child%line > 0) then
     543           41 :           write(linebuf, '(i0)') child%line
     544            0 :           buf(nwarn) = "Unprocessed input: '" // trim(child%name) &
     545           41 :               & // "' (line " // trim(linebuf) // ")"
     546              :         else
     547            0 :           buf(nwarn) = "Unprocessed input: '" // trim(child%name) // "'"
     548              :         end if
     549              :       end if
     550              : 
     551              :       ! Recurse into processed table children
     552           44 :       if (child%node_type == NODE_TYPE_TABLE) then
     553            0 :         if (child%processed) &
     554            0 :             & call collect_unprocessed_(child, buf, nwarn)
     555              :       end if
     556              :     end do
     557              : 
     558            4 :   end subroutine collect_unprocessed_
     559              : 
     560           15 : end module hsd_validation
        

Generated by: LCOV version 2.0-1