cobol-parser 2.0.0__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. {cobol_parser-2.0.0/cobol_parser.egg-info → cobol_parser-3.0.0}/PKG-INFO +2 -2
  2. {cobol_parser-2.0.0 → cobol_parser-3.0.0/cobol_parser.egg-info}/PKG-INFO +2 -2
  3. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/__init__.py +1 -1
  4. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/codebase.py +75 -23
  5. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/const_fold.py +274 -27
  6. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/lint.py +81 -9
  7. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/move_type_checker.py +297 -13
  8. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/cli/parse.py +111 -7
  9. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/compare.py +59 -5
  10. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/decoding.py +39 -8
  11. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/layout.py +155 -0
  12. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/values.py +44 -5
  13. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/views.py +250 -54
  14. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/copy_statement_parser.py +5 -1
  15. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/path_resolver.py +65 -38
  16. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/pyproject.toml +2 -2
  17. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/LICENSE +0 -0
  18. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/MANIFEST.in +0 -0
  19. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/README.md +0 -0
  20. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobol_parser.egg-info/SOURCES.txt +0 -0
  21. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobol_parser.egg-info/dependency_links.txt +0 -0
  22. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobol_parser.egg-info/entry_points.txt +0 -0
  23. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobol_parser.egg-info/requires.txt +0 -0
  24. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobol_parser.egg-info/top_level.txt +0 -0
  25. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/__main__.py +0 -0
  26. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_data_fast.c +0 -0
  27. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_data_fast.pyx +0 -0
  28. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_errors.py +0 -0
  29. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_lexer_fast.c +0 -0
  30. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_lexer_fast.pyx +0 -0
  31. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_lines.py +0 -0
  32. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_literals.py +0 -0
  33. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_preprocess_fast.c +0 -0
  34. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_preprocess_fast.pyx +0 -0
  35. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_source_directive.py +0 -0
  36. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/_storage.py +0 -0
  37. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/__init__.py +0 -0
  38. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/_files.py +0 -0
  39. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/_names.py +0 -0
  40. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/_procedures.py +0 -0
  41. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/_sql_flow.py +0 -0
  42. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/ast_diff.py +0 -0
  43. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/boundary_inventory.py +0 -0
  44. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/business_rules.py +0 -0
  45. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/complexity.py +0 -0
  46. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/constraints.py +0 -0
  47. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/crud_matrix.py +0 -0
  48. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/dclgen_check.py +0 -0
  49. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/dead_code.py +0 -0
  50. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/domains.py +0 -0
  51. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/field_lineage.py +0 -0
  52. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/host_var_resolver.py +0 -0
  53. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/ims_segment_analyzer.py +0 -0
  54. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/io_coverage_reporter.py +0 -0
  55. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/io_diagnostics.py +0 -0
  56. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/io_logger.py +0 -0
  57. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/io_sequence.py +0 -0
  58. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/language_env_analyzer.py +0 -0
  59. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/lineage_graph.py +0 -0
  60. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/lineage_jcl.py +0 -0
  61. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/linkage_checker.py +0 -0
  62. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/__init__.py +0 -0
  63. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/analyzer.py +0 -0
  64. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/condition_text.py +0 -0
  65. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/cycles.py +0 -0
  66. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/edges.py +0 -0
  67. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/graph.py +0 -0
  68. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/models.py +0 -0
  69. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/scanner.py +0 -0
  70. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_flow/statement_text.py +0 -0
  71. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/program_signature.py +0 -0
  72. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/recursion_risk.py +0 -0
  73. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/redundancy.py +0 -0
  74. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/sql_cics_lint.py +0 -0
  75. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/technical_debt.py +0 -0
  76. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/unused_variables.py +0 -0
  77. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/value_tracker.py +0 -0
  78. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/analysis/xref.py +0 -0
  79. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/batch.py +0 -0
  80. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/cli/__init__.py +0 -0
  81. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/cli/_project_config.py +0 -0
  82. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/cli/coverage.py +0 -0
  83. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/config/__init__.py +0 -0
  84. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/config/_yaml.py +0 -0
  85. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/config/project.py +0 -0
  86. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/core/__init__.py +0 -0
  87. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/core/ast_factory.py +0 -0
  88. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/core/exceptions.py +0 -0
  89. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/core/interfaces.py +0 -0
  90. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/core/registry.py +0 -0
  91. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/core/token_stream.py +0 -0
  92. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/dialects/__init__.py +0 -0
  93. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/dialects/ibm_enterprise.py +0 -0
  94. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/dialects/le_signatures.py +0 -0
  95. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/__init__.py +0 -0
  96. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/copybook_data.py +0 -0
  97. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/__init__.py +0 -0
  98. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/bundle.py +0 -0
  99. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/copybook.py +0 -0
  100. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/dataset.py +0 -0
  101. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/editing.py +0 -0
  102. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/encoding.py +0 -0
  103. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/errors.py +0 -0
  104. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/layout_offsets.py +0 -0
  105. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/options.py +0 -0
  106. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/program_layout.py +0 -0
  107. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/data/storage.py +0 -0
  108. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/flowchart.py +0 -0
  109. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/program_fixtures.py +0 -0
  110. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/scenarios.py +0 -0
  111. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/generators/verification.py +0 -0
  112. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/jcl/__init__.py +0 -0
  113. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/jcl/bindings.py +0 -0
  114. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/jcl/models.py +0 -0
  115. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/jcl/parser.py +0 -0
  116. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/lexer.py +0 -0
  117. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/__init__.py +0 -0
  118. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/ast_base.py +0 -0
  119. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/data_items.py +0 -0
  120. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/program.py +0 -0
  121. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/sql_statements.py +0 -0
  122. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/__init__.py +0 -0
  123. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/arithmetic.py +0 -0
  124. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/base.py +0 -0
  125. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/call.py +0 -0
  126. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/communication.py +0 -0
  127. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/control.py +0 -0
  128. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/data.py +0 -0
  129. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/io.py +0 -0
  130. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/legacy.py +0 -0
  131. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/misc.py +0 -0
  132. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/models/statements/report_writer.py +0 -0
  133. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parser.py +0 -0
  134. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/__init__.py +0 -0
  135. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/__init__.py +0 -0
  136. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/base.py +0 -0
  137. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/data.py +0 -0
  138. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/data_clauses.py +0 -0
  139. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/data_file_description.py +0 -0
  140. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/data_report_description.py +0 -0
  141. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/data_special_entries.py +0 -0
  142. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/environment.py +0 -0
  143. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/environment_sections.py +0 -0
  144. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/environment_select.py +0 -0
  145. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/factory.py +0 -0
  146. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/identification.py +0 -0
  147. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/procedure.py +0 -0
  148. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/procedure_boundaries.py +0 -0
  149. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/procedure_recovery.py +0 -0
  150. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/divisions/procedure_registry.py +0 -0
  151. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/__init__.py +0 -0
  152. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/_conditions.py +0 -0
  153. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/_utils.py +0 -0
  154. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/accept_parser.py +0 -0
  155. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/allocate_free_parsers.py +0 -0
  156. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/arithmetic_parsers.py +0 -0
  157. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/call_parser.py +0 -0
  158. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/close_parser.py +0 -0
  159. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/communication_parsers.py +0 -0
  160. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/delete_parser.py +0 -0
  161. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/display_parser.py +0 -0
  162. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/entry_parser.py +0 -0
  163. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/evaluate_parser.py +0 -0
  164. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/exception_parsers.py +0 -0
  165. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/go_parser.py +0 -0
  166. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/ibm_exec_statements.py +0 -0
  167. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/if_parser.py +0 -0
  168. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/initialize_inspect_parsers.py +0 -0
  169. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/invoke_parser.py +0 -0
  170. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/legacy_verb_parsers.py +0 -0
  171. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/move_parser.py +0 -0
  172. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/open_parser.py +0 -0
  173. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/perform_parser.py +0 -0
  174. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/read_parser.py +0 -0
  175. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/report_writer_parsers.py +0 -0
  176. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/rewrite_parser.py +0 -0
  177. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/search_parser.py +0 -0
  178. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/set_parser.py +0 -0
  179. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/simple_statement_parsers.py +0 -0
  180. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/sort_merge_parsers.py +0 -0
  181. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/start_parser.py +0 -0
  182. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/stop_parser.py +0 -0
  183. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/string_unstring_parsers.py +0 -0
  184. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/unlock_parser.py +0 -0
  185. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/write_parser.py +0 -0
  186. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/parsers/statements/xml_json_parser.py +0 -0
  187. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/plugins/__init__.py +0 -0
  188. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/plugins/core.py +0 -0
  189. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/__init__.py +0 -0
  190. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/_normalize.py +0 -0
  191. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/copybook_expander.py +0 -0
  192. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/models.py +0 -0
  193. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/replace_statement.py +0 -0
  194. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/preprocess/source_format.py +0 -0
  195. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/profiling.py +0 -0
  196. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/py.typed +0 -0
  197. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/cobolparser/scan.py +0 -0
  198. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/setup.cfg +0 -0
  199. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/setup.py +0 -0
  200. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/tests/test_all_samples.py +0 -0
  201. {cobol_parser-2.0.0 → cobol_parser-3.0.0}/tests/test_okf_staleness_map.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cobol-parser
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: A comprehensive COBOL parser for Python with IBM Enterprise COBOL focus
5
- Author: Samuel Dion
5
+ Author-email: Samuel Dion <sam94dion@gmail.com>
6
6
  License: AGPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/Landparty/cobol-parser
8
8
  Project-URL: Repository, https://github.com/Landparty/cobol-parser
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cobol-parser
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: A comprehensive COBOL parser for Python with IBM Enterprise COBOL focus
5
- Author: Samuel Dion
5
+ Author-email: Samuel Dion <sam94dion@gmail.com>
6
6
  License: AGPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/Landparty/cobol-parser
8
8
  Project-URL: Repository, https://github.com/Landparty/cobol-parser
@@ -231,7 +231,7 @@ from .scan import (
231
231
  CobolParseError = ParseError
232
232
 
233
233
  # Version info
234
- __version__ = "2.0.0" # x-release-please-version
234
+ __version__ = "3.0.0" # x-release-please-version
235
235
  __author__ = "Samuel Dion"
236
236
  __license__ = "AGPL-3.0-or-later"
237
237
 
@@ -14,7 +14,13 @@ and aggregates the results into a single graph with two kinds of edge:
14
14
  targets, each marked ``resolved=False`` so the edge set is never mistaken
15
15
  for an exhaustive one.
16
16
  - **copy edges** -- ``program A copies member X``, and ``member X copies
17
- member Y`` for nested copybooks. These are collected by re-scanning each
17
+ member Y`` for nested copybooks. A DB2 ``EXEC SQL INCLUDE X END-EXEC`` is
18
+ a copy edge too, with the same ``COPY`` label and the same unresolved-member
19
+ reporting: it is how SQLCA and DCLGEN members are normally pulled in, so
20
+ leaving it out made "what uses this DCLGEN?" answer nothing (#295). The
21
+ output does not say which of the two produced an edge. An INCLUDE of
22
+ ``SQLCA`` or ``SQLDA`` that is not on the path is not reported unresolved,
23
+ since the precompiler supplies those. These are collected by re-scanning each
18
24
  source with :func:`~cobolparser.preprocess.copy_statement_parser.extract_copy_statements`
19
25
  rather than from an expanded parse, because expansion *consumes* the COPY
20
26
  statement and discards exactly the edge we are after. Members are resolved
@@ -84,6 +90,13 @@ __all__ = [
84
90
  "ProgramNode",
85
91
  ]
86
92
 
93
+ #: Members the DB2 precompiler supplies itself. An ``EXEC SQL INCLUDE`` of one
94
+ #: that is not on the copybook path is normal, not a gap: it still gets its
95
+ #: edge, but no ``unresolved`` note. This is narrower than the copybook
96
+ #: expander, which lets *every* unresolvable INCLUDE through silently: here a
97
+ #: missing DCLGEN is still a gap worth reporting, and so is a COPY of either.
98
+ _PRECOMPILER_MEMBERS = frozenset({"SQLCA", "SQLDA"})
99
+
87
100
  #: File extensions :meth:`CodebaseGraphBuilder.build_from_directory` treats as
88
101
  #: COBOL program source. Matched case-insensitively.
89
102
  DEFAULT_PROGRAM_EXTENSIONS: tuple[str, ...] = (".cbl", ".cob", ".cobol")
@@ -607,8 +620,13 @@ class CodebaseGraphBuilder:
607
620
  )
608
621
  # member -> resolved file path (None when it could not be resolved)
609
622
  self._member_paths: dict[str, str | None] = {}
610
- # member -> its own direct COPY member names (sorted)
611
- self._member_copies: dict[str, list[str]] = {}
623
+ # member -> why it could not be resolved (only for failed members)
624
+ self._member_failures: dict[str, str] = {}
625
+ # (member, referrer) pairs already reported unresolved this build, so
626
+ # a COPY and an INCLUDE of one missing member give one note, not two
627
+ self._reported_failures: set[tuple[str, str]] = set()
628
+ # member -> its own direct (member name, is EXEC SQL INCLUDE), sorted
629
+ self._member_copies: dict[str, list[tuple[str, bool]]] = {}
612
630
 
613
631
  # -- public API -------------------------------------------------------
614
632
 
@@ -617,6 +635,8 @@ class CodebaseGraphBuilder:
617
635
  # Per-build caches: a copybook edited between two builds on one
618
636
  # long-lived builder must not serve the previous build's chain.
619
637
  self._member_paths = {}
638
+ self._member_failures = {}
639
+ self._reported_failures = set()
620
640
  self._member_copies = {}
621
641
  files = self._normalize_paths(paths)
622
642
 
@@ -923,7 +943,7 @@ class CodebaseGraphBuilder:
923
943
 
924
944
  warnings: list[str] = []
925
945
  statements = extract_copy_statements(
926
- source, source_file=path, warnings=warnings
946
+ source, source_file=path, warnings=warnings, sql_include=True
927
947
  )
928
948
  for warning in warnings:
929
949
  unresolved.append(f"{path}: COPY statement not usable: {warning}")
@@ -934,7 +954,11 @@ class CodebaseGraphBuilder:
934
954
  if not member:
935
955
  continue
936
956
  member_path = self._resolve_member(
937
- member, statement.library_qualifier, path, unresolved
957
+ member,
958
+ statement.library_qualifier,
959
+ path,
960
+ unresolved,
961
+ sql_include=statement.sql_include,
938
962
  )
939
963
  copy_edges.append(
940
964
  CopyEdge(program=owner, member=member, path=member_path, nested=False)
@@ -949,27 +973,48 @@ class CodebaseGraphBuilder:
949
973
  library: str | None,
950
974
  referrer: str,
951
975
  unresolved: list[str],
976
+ *,
977
+ sql_include: bool = False,
952
978
  ) -> str | None:
953
- """Resolve *member* to a file, caching the answer (and the failure)."""
979
+ """Resolve *member* to a file, caching the answer (and the failure).
980
+
981
+ A failure is reported for every reference that is not an INCLUDE of
982
+ a precompiler-supplied member (:data:`_PRECOMPILER_MEMBERS`), so a
983
+ quiet ``INCLUDE SQLCA`` seen first cannot silence a later ``COPY
984
+ SQLCA``. One note per (member, referrer): whichever of COPY / INCLUDE
985
+ is seen first names it.
986
+ """
954
987
  if self._resolver is None:
955
988
  return None
956
- if member in self._member_paths:
957
- return self._member_paths[member]
958
- try:
959
- resolved = self._resolver.resolve(member, library)
960
- except Exception as exc: # noqa: BLE001 -- an unresolvable member is a
961
- # finding, not a crash: record why and carry on with the rest.
962
- self._member_paths[member] = None
989
+ if member not in self._member_paths:
990
+ try:
991
+ self._member_paths[member] = self._resolver.resolve(member, library)
992
+ except Exception as exc: # noqa: BLE001 -- an unresolvable member
993
+ # is a finding, not a crash: record why and carry on.
994
+ self._member_paths[member] = None
995
+ self._member_failures[member] = f"{type(exc).__name__}: {exc}"
996
+ resolved = self._member_paths[member]
997
+ if (
998
+ resolved is None
999
+ and not (sql_include and member in _PRECOMPILER_MEMBERS)
1000
+ and (member, referrer) not in self._reported_failures
1001
+ ):
1002
+ self._reported_failures.add((member, referrer))
1003
+ kind = "EXEC SQL INCLUDE" if sql_include else "COPY"
963
1004
  unresolved.append(
964
- f"COPY member {member} (referenced by {referrer}) could not be "
965
- f"resolved on the copybook search path: {type(exc).__name__}: {exc}"
1005
+ f"{kind} member {member} (referenced by {referrer}) could not be "
1006
+ "resolved on the copybook search path: "
1007
+ f"{self._member_failures[member]}"
966
1008
  )
967
- return None
968
- self._member_paths[member] = resolved
969
1009
  return resolved
970
1010
 
971
- def _member_direct_copies(self, member: str, unresolved: list[str]) -> list[str]:
972
- """The member names *member* itself COPYs (empty when unresolved)."""
1011
+ def _member_direct_copies(
1012
+ self, member: str, unresolved: list[str]
1013
+ ) -> list[tuple[str, bool]]:
1014
+ """What *member* itself COPYs or INCLUDEs (empty when unresolved).
1015
+
1016
+ Each entry is ``(member name, is EXEC SQL INCLUDE)``.
1017
+ """
973
1018
  if member in self._member_copies:
974
1019
  return self._member_copies[member]
975
1020
  self._member_copies[member] = []
@@ -985,13 +1030,13 @@ class CodebaseGraphBuilder:
985
1030
  return []
986
1031
  warnings: list[str] = []
987
1032
  statements = extract_copy_statements(
988
- source, source_file=member_path, warnings=warnings
1033
+ source, source_file=member_path, warnings=warnings, sql_include=True
989
1034
  )
990
1035
  for warning in warnings:
991
1036
  unresolved.append(f"{member_path}: COPY statement not usable: {warning}")
992
1037
  names = sorted(
993
1038
  {
994
- _normalize_member(s.member_name)
1039
+ (_normalize_member(s.member_name), s.sql_include)
995
1040
  for s in statements
996
1041
  if _normalize_member(s.member_name)
997
1042
  }
@@ -1007,8 +1052,15 @@ class CodebaseGraphBuilder:
1007
1052
  unresolved: list[str],
1008
1053
  ) -> None:
1009
1054
  """Emit nested member->member edges below *member*, cycle-safe."""
1010
- for nested in self._member_direct_copies(member, unresolved):
1011
- nested_path = self._resolve_member(nested, None, member, unresolved)
1055
+ seen: set[str] = set()
1056
+ for nested, sql_include in self._member_direct_copies(member, unresolved):
1057
+ nested_path = self._resolve_member(
1058
+ nested, None, member, unresolved, sql_include=sql_include
1059
+ )
1060
+ if nested in seen:
1061
+ # COPY and INCLUDE of one member: one edge, as for a program.
1062
+ continue
1063
+ seen.add(nested)
1012
1064
  copy_edges.append(
1013
1065
  CopyEdge(program=member, member=nested, path=nested_path, nested=True)
1014
1066
  )
@@ -43,6 +43,11 @@ than guessing:
43
43
  coercion. Real COBOL truncation rules for an overflowing MOVE are
44
44
  well-defined, but replicating them exactly is out of scope for v1; an
45
45
  overflowing result is poisoned instead of guessed.
46
+ * Every DISPLAY/WRITE in the program is a site: one inside an arm, loop or
47
+ handler records "unknown", and one the walk never reached (past a GO TO,
48
+ DECLARATIVES, dead code) vetoes the field at the end. A GO TO / EXIT
49
+ PARAGRAPH / NEXT SENTENCE the walk cannot follow puts it in "uncertain"
50
+ mode, where every later write poisons instead of folding.
46
51
  * Only a bare ``DISPLAY``/``WRITE`` operand that resolves directly to an
47
52
  elementary field is an observation point -- a whole-record WRITE's
48
53
  individual member fields are not (yet) walked, so some real
@@ -78,11 +83,19 @@ from ..models.statements.control import (
78
83
  GoStatement,
79
84
  IfStatement,
80
85
  PerformStatement,
86
+ SearchStatement,
81
87
  StopStatement,
82
88
  )
83
89
  from ..models.statements.data import DisplayStatement, MoveStatement
84
90
  from ..models.statements.io import IOAcceptStatement, IOReadStatement, IOWriteStatement
85
- from ..models.statements.misc import ExitStatement, GobackStatement
91
+ from ..models.statements.misc import (
92
+ ExitStatement,
93
+ GobackStatement,
94
+ InvokeStatement,
95
+ MergeStatement,
96
+ NextSentenceStatement,
97
+ SortStatement,
98
+ )
86
99
 
87
100
  if TYPE_CHECKING:
88
101
  # Same shape as constraints.py's own guard: cobolparser.generators
@@ -113,6 +126,13 @@ _SUFFIX_RE = re.compile(r"\s*\(.*$", re.DOTALL)
113
126
  #: answer.
114
127
  _MAX_WALK_STEPS = 50_000
115
128
 
129
+ #: `ordered_names` key prefix for a SECTION header (a marker, never a
130
+ #: paragraph) and for a paragraph with no name (statements written straight
131
+ #: under a SECTION header, or straight under PROCEDURE DIVISION). Neither
132
+ #: can collide with a COBOL name.
133
+ _SECTION_KEY = "\x00SECTION:"
134
+ _UNNAMED_KEY = "\x00UNNAMED:"
135
+
116
136
  _TEXT_CATEGORIES = frozenset(
117
137
  {FieldCategory.ALPHANUMERIC, FieldCategory.ALPHABETIC, FieldCategory.NATIONAL}
118
138
  )
@@ -197,7 +217,7 @@ class _LayoutFacts:
197
217
  share bytes through a REDEFINES chain, and which are JUSTIFIED. Built
198
218
  once per index from the raw `CobolDataItem` trees and cached weakly."""
199
219
 
200
- __slots__ = ("groups", "leaves", "aliases", "justified", "renames")
220
+ __slots__ = ("groups", "leaves", "aliases", "justified", "renames", "shared")
201
221
 
202
222
  def __init__(self) -> None:
203
223
  #: UPPERCASED bare group/record name -> qualified paths of every
@@ -213,6 +233,10 @@ class _LayoutFacts:
213
233
  #: covers. A 66 is not a `CobolDataItem` (the index never resolves
214
234
  #: it), so without this a write through the alias poisoned nothing.
215
235
  self.renames: dict[str, list[str]] = {}
236
+ #: Leaves under a GLOBAL or EXTERNAL item: storage another program
237
+ #: (a contained program, or any program declaring the same EXTERNAL
238
+ #: name) can write, so every CALL / INVOKE may change them.
239
+ self.shared: set[str] = set()
216
240
 
217
241
  def leaves_under(self, qpath: str) -> list[str]:
218
242
  prefix = qpath + "."
@@ -333,18 +357,28 @@ def _data_children(item: CobolDataItem) -> list[CobolDataItem]:
333
357
 
334
358
 
335
359
  def _collect_facts(
336
- item: CobolDataItem, path: tuple[str, ...], facts: _LayoutFacts
360
+ item: CobolDataItem,
361
+ path: tuple[str, ...],
362
+ facts: _LayoutFacts,
363
+ shared: bool = False,
337
364
  ) -> None:
338
365
  qpath = (*path, item.name)
339
366
  qname = ".".join(qpath)
367
+ shared = (
368
+ shared
369
+ or bool(getattr(item, "is_global", False))
370
+ or bool(getattr(item, "is_external", False))
371
+ )
340
372
  children = _data_children(item)
341
373
  if children:
342
374
  facts.groups.setdefault(item.name.upper(), []).append(qname)
343
375
  for child in children:
344
- _collect_facts(child, qpath, facts)
376
+ _collect_facts(child, qpath, facts, shared)
345
377
  _link_redefines(children, qpath, facts)
346
378
  return
347
379
  facts.leaves.append(qname)
380
+ if shared:
381
+ facts.shared.add(qname)
348
382
  if getattr(item, "justified", False):
349
383
  facts.justified.add(qname)
350
384
 
@@ -543,6 +577,21 @@ class ConstantFolder:
543
577
  # observed: no reported value could be shown to be the only one.
544
578
  return ConstantFoldResult(known={})
545
579
 
580
+ # A DISPLAY/WRITE site the walk never reached (code after a GO TO,
581
+ # code only a GO TO reaches, a DECLARATIVES procedure, ...) may emit
582
+ # any value at all, so a field shown there is not knowable either --
583
+ # "every site that observed it" must be every site, not just the
584
+ # walked ones.
585
+ for site in _observation_sites(ast):
586
+ if id(site) in state.visited_sites:
587
+ continue
588
+ for operand in _site_operands(site):
589
+ resolved = _safe_resolve(index, operand)
590
+ if resolved is not None:
591
+ state.observed.setdefault(resolved.spec.qualified_name, []).append(
592
+ None
593
+ )
594
+
546
595
  # A field is reported only when every DISPLAY/WRITE site that
547
596
  # observed it saw the same known value -- the value the program
548
597
  # actually emitted there, not whatever it held when the walk ended.
@@ -563,12 +612,64 @@ def _fall_through_chain(
563
612
  ordered_names: list[str],
564
613
  by_name: dict[str, CobolParagraph],
565
614
  ) -> list[CobolParagraph]:
566
- """*entry* followed by every paragraph after it in source order."""
567
- name = (entry.paragraph_name or "").upper()
568
- if name not in by_name or by_name[name] is not entry:
615
+ """*entry* followed by every paragraph after it in source order (across
616
+ SECTION boundaries: control falls into the next section too)."""
617
+ start = next(
618
+ (i for i, key in enumerate(ordered_names) if by_name.get(key) is entry),
619
+ None,
620
+ )
621
+ if start is None:
569
622
  return [entry]
570
- start = ordered_names.index(name)
571
- return [by_name[following] for following in ordered_names[start:]]
623
+ return [by_name[key] for key in ordered_names[start:] if key in by_name]
624
+
625
+
626
+ def _observation_sites(ast: CobolProgram) -> list[Any]:
627
+ """Every DISPLAY and WRITE statement of the PROCEDURE DIVISION, nested
628
+ ones (IF/EVALUATE/SEARCH arms, inline PERFORM bodies, handlers)
629
+ included."""
630
+ procedure = ast.get_division("PROCEDURE")
631
+ if procedure is None:
632
+ return []
633
+ pending: list[list[Any]] = [list(p.statements) for p in procedure.paragraphs]
634
+ for section in procedure._sections.values():
635
+ pending.extend(list(p.statements) for p in section.paragraphs)
636
+ sites: list[Any] = []
637
+ seen: set[int] = set()
638
+ while pending:
639
+ for stmt in pending.pop():
640
+ if id(stmt) in seen:
641
+ continue
642
+ seen.add(id(stmt))
643
+ if isinstance(stmt, (DisplayStatement, IOWriteStatement)):
644
+ sites.append(stmt)
645
+ pending.extend(_nested_statement_lists(stmt))
646
+ return sites
647
+
648
+
649
+ def _site_operands(stmt: Any) -> list[str]:
650
+ """The operands `_dispatch_one` marks observed for a DISPLAY/WRITE."""
651
+ if isinstance(stmt, DisplayStatement):
652
+ return list(stmt.display_items)
653
+ if isinstance(stmt, IOWriteStatement) and stmt.record_name:
654
+ return [stmt.record_name]
655
+ return []
656
+
657
+
658
+ def _transfer_kind(stmt: Any) -> str | None:
659
+ """``"jump"`` for a GO TO (control goes to code this walk does not
660
+ follow), ``"skip"`` for a transfer that only skips forward inside the
661
+ current paragraph / section / inline PERFORM / sentence, else None."""
662
+ if isinstance(stmt, GoStatement):
663
+ return "jump"
664
+ if isinstance(stmt, NextSentenceStatement):
665
+ return "skip"
666
+ if isinstance(stmt, ExitStatement) and (stmt.exit_type or "") in {
667
+ "PARAGRAPH",
668
+ "SECTION",
669
+ "PERFORM",
670
+ }:
671
+ return "skip"
672
+ return None
572
673
 
573
674
 
574
675
  # -- walk state ---------------------------------------------------------------
@@ -584,6 +685,8 @@ class _WalkState:
584
685
  "accepted",
585
686
  "steps",
586
687
  "exhausted",
688
+ "visited_sites",
689
+ "uncertain",
587
690
  )
588
691
 
589
692
  def __init__(
@@ -610,6 +713,15 @@ class _WalkState:
610
713
  self.steps = 0
611
714
  #: Set when the budget ran out; `fold()` then reports nothing.
612
715
  self.exhausted = False
716
+ #: ``id()`` of every DISPLAY/WRITE the walk reached (main line or
717
+ #: nested), so `fold()` can tell which sites it never saw.
718
+ self.visited_sites: set[int] = set()
719
+ #: Set once a branch arm / loop body / handler could GO TO, EXIT
720
+ #: PARAGRAPH/SECTION/PERFORM or NEXT SENTENCE (or the main line does
721
+ #: an EXIT PARAGRAPH/... itself): from then on the walk cannot tell
722
+ #: whether a later statement really ran, so every write it meets
723
+ #: poisons instead of folding.
724
+ self.uncertain = False
613
725
 
614
726
 
615
727
  # -- entry point / paragraph ordering -----------------------------------------
@@ -657,11 +769,15 @@ def _ordered_paragraphs(
657
769
  by_name: dict[str, CobolParagraph] = {}
658
770
 
659
771
  def register(para: CobolParagraph) -> None:
660
- if para.paragraph_name:
661
- name = para.paragraph_name.upper()
662
- if name not in by_name:
663
- ordered.append(name)
664
- by_name[name] = para
772
+ # A paragraph with no name (statements straight under a SECTION
773
+ # header) still runs when control falls into it or its section is
774
+ # PERFORMed, so it gets a key no PERFORM can name.
775
+ name = (para.paragraph_name or "").upper()
776
+ if not name:
777
+ name = f"{_UNNAMED_KEY}{len(ordered)}"
778
+ if name not in by_name:
779
+ ordered.append(name)
780
+ by_name[name] = para
665
781
 
666
782
  top_level_items: list[Any] = sorted(
667
783
  [*procedure._sections.values(), *procedure.paragraphs],
@@ -671,6 +787,10 @@ def _ordered_paragraphs(
671
787
  if isinstance(item, CobolParagraph):
672
788
  register(item)
673
789
  else:
790
+ # A marker (absent from `by_name`) so `PERFORM SEC-A` / `THRU
791
+ # SEC-B` can find where a section's paragraphs start and end.
792
+ section_name = (getattr(item, "section_name", "") or "").upper()
793
+ ordered.append(f"{_SECTION_KEY}{section_name}")
674
794
  for para in item.paragraphs:
675
795
  register(para)
676
796
  return ordered, by_name
@@ -761,6 +881,27 @@ def _dispatch_one(
761
881
  ) -> tuple[str | None, list[Any]] | None:
762
882
  """Per-statement-type fold dispatch for `_walk_one` (which additionally
763
883
  poisons conditional handler bodies around whatever happens here)."""
884
+ if state.uncertain and isinstance(
885
+ stmt,
886
+ (
887
+ MoveStatement,
888
+ ArithmeticStatement,
889
+ ComputeStatement,
890
+ IOReadStatement,
891
+ IOAcceptStatement,
892
+ ),
893
+ ):
894
+ # Past a possible skip/jump: this write may or may not have run.
895
+ _poison_written_names(
896
+ [stmt], index, state.env, by_name, ordered_names, state=state
897
+ )
898
+ return None
899
+ if _transfer_kind(stmt) == "skip":
900
+ # EXIT PARAGRAPH / SECTION / PERFORM, NEXT SENTENCE on the main
901
+ # line: the statements after it are skipped, which this straight-line
902
+ # walk does not model.
903
+ state.uncertain = True
904
+ return None
764
905
  if isinstance(stmt, MoveStatement):
765
906
  _fold_move(stmt, index, state.env)
766
907
  elif isinstance(stmt, AddStatement):
@@ -774,10 +915,14 @@ def _dispatch_one(
774
915
  elif isinstance(stmt, ComputeStatement):
775
916
  _fold_compute(stmt, index, state.env)
776
917
  elif isinstance(stmt, DisplayStatement):
918
+ state.visited_sites.add(id(stmt))
777
919
  for item in stmt.display_items:
778
920
  _mark_observed(item, index, state)
779
921
  elif isinstance(stmt, IOWriteStatement):
780
- if stmt.record_name and stmt.from_clause:
922
+ state.visited_sites.add(id(stmt))
923
+ if stmt.record_name and stmt.from_clause and state.uncertain:
924
+ _poison_names([stmt.record_name], index, state.env)
925
+ elif stmt.record_name and stmt.from_clause:
781
926
  # `WRITE rec FROM id` is `MOVE id TO rec` and then the WRITE. A
782
927
  # group record target is poisoned whole by `_apply_move_target`.
783
928
  source = _resolve_move_source(stmt.from_clause, index, state.env)
@@ -832,7 +977,11 @@ def _dispatch_one(
832
977
  # values with seedable IN ones, and the raw AST does not carry
833
978
  # resolved direction to tell them apart (only
834
979
  # `BoundaryInventoryAnalyzer`'s own richer analysis does).
835
- _poison_statement_writes(stmt, index, state.env, state=state)
980
+ # Nested bodies (SEARCH WHEN, ...) and out-of-line procedures
981
+ # (SORT/MERGE INPUT/OUTPUT PROCEDURE) are poisoned the same way.
982
+ _poison_written_names(
983
+ [stmt], index, state.env, by_name, ordered_names, state=state
984
+ )
836
985
  return None
837
986
 
838
987
 
@@ -943,6 +1092,11 @@ def _poison_statement_writes(
943
1092
  base, _ = _base_name(stmt.target)
944
1093
  state.accepted.add(base.upper())
945
1094
  _poison_names(_write_targets(stmt), index, env)
1095
+ if isinstance(stmt, (CallStatement, InvokeStatement)):
1096
+ # The called program may write any GLOBAL item (a contained program
1097
+ # sees them by name) and any EXTERNAL one, USING list or not.
1098
+ for leaf in _facts(index).shared:
1099
+ _drop(leaf, index, env)
946
1100
 
947
1101
 
948
1102
  def _mark_observed(operand: str, index: ProgramLayoutIndex, state: _WalkState) -> None:
@@ -977,6 +1131,9 @@ def _walk_perform(
977
1131
  body = stmt.inline_statements or _perform_body_statements(
978
1132
  stmt, by_name, ordered_names
979
1133
  )
1134
+ if body is None:
1135
+ state.env.clear() # an unknown target could have written anything
1136
+ return None
980
1137
  _poison_written_names(
981
1138
  body, index, state.env, by_name, ordered_names, state=state
982
1139
  )
@@ -989,25 +1146,60 @@ def _walk_perform(
989
1146
  return None
990
1147
  target = stmt.target_paragraph.upper()
991
1148
  body = _perform_body_statements(stmt, by_name, ordered_names)
1149
+ if body is None:
1150
+ state.env.clear() # an unknown target could have written anything
1151
+ return None
992
1152
  return (target, body)
993
1153
 
994
1154
 
1155
+ def _procedure_span(name: str, ordered_names: list[str], *, last: bool) -> int | None:
1156
+ """Index in *ordered_names* where the paragraph or SECTION *name* starts
1157
+ (or, with *last*, ends). A paragraph name wins over a section name."""
1158
+ if name in ordered_names:
1159
+ return ordered_names.index(name)
1160
+ marker = f"{_SECTION_KEY}{name}"
1161
+ if marker not in ordered_names:
1162
+ return None
1163
+ start = ordered_names.index(marker)
1164
+ if not last:
1165
+ return start
1166
+ end = start
1167
+ for idx in range(start + 1, len(ordered_names)):
1168
+ if ordered_names[idx].startswith(_SECTION_KEY):
1169
+ break
1170
+ end = idx
1171
+ return end
1172
+
1173
+
995
1174
  def _perform_body_statements(
996
1175
  stmt: PerformStatement,
997
1176
  by_name: dict[str, CobolParagraph],
998
1177
  ordered_names: list[str],
999
- ) -> list[Any]:
1178
+ ) -> list[Any] | None:
1179
+ """The statements a PERFORM of a paragraph, a SECTION or a THRU range
1180
+ executes, or None when the target cannot be found -- the caller must
1181
+ then assume the PERFORM could have written anything."""
1000
1182
  if not stmt.target_paragraph:
1001
- return []
1002
- start = stmt.target_paragraph.upper()
1003
- end = (stmt.thru_paragraph or stmt.target_paragraph).upper()
1004
- if start not in ordered_names or end not in ordered_names:
1005
- para = by_name.get(start)
1006
- return list(para.statements) if para else []
1007
- start_idx = ordered_names.index(start)
1008
- end_idx = ordered_names.index(end)
1009
- if end_idx < start_idx:
1010
- return [] # malformed/unexpected THRU order: nothing to walk, not a guess
1183
+ return None
1184
+ return _range_statements(
1185
+ stmt.target_paragraph, stmt.thru_paragraph, by_name, ordered_names
1186
+ )
1187
+
1188
+
1189
+ def _range_statements(
1190
+ first: str,
1191
+ thru: str | None,
1192
+ by_name: dict[str, CobolParagraph],
1193
+ ordered_names: list[str],
1194
+ ) -> list[Any] | None:
1195
+ """The statements of procedure *first* (THRU *thru*), or None when it
1196
+ cannot be found."""
1197
+ start = first.upper()
1198
+ end = (thru or first).upper()
1199
+ start_idx = _procedure_span(start, ordered_names, last=False)
1200
+ end_idx = _procedure_span(end, ordered_names, last=True)
1201
+ if start_idx is None or end_idx is None or end_idx < start_idx:
1202
+ return None # unknown target / malformed THRU order: not a guess
1011
1203
  statements: list[Any] = []
1012
1204
  for name in ordered_names[start_idx : end_idx + 1]:
1013
1205
  para = by_name.get(name)
@@ -1089,9 +1281,31 @@ def _nested_statement_lists(stmt: Any) -> list[list[Any]]:
1089
1281
  lists.append(wc.statements)
1090
1282
  if stmt.default_when:
1091
1283
  lists.append(stmt.default_when.statements)
1284
+ elif isinstance(stmt, SearchStatement):
1285
+ # SEARCH's WHEN clauses are dicts: {"condition", "statements"}.
1286
+ for clause in stmt.when_clauses:
1287
+ body = clause.get("statements") if isinstance(clause, dict) else None
1288
+ if isinstance(body, list):
1289
+ lists.append(body)
1092
1290
  return lists
1093
1291
 
1094
1292
 
1293
+ def _procedure_ranges(stmt: Any) -> list[tuple[str, str | None]]:
1294
+ """Out-of-line procedures a non-PERFORM statement runs: a SORT/MERGE
1295
+ INPUT / OUTPUT PROCEDURE (optionally THRU)."""
1296
+ ranges: list[tuple[str, str | None]] = []
1297
+ if isinstance(stmt, (SortStatement, MergeStatement)):
1298
+ for first_attr, thru_attr in (
1299
+ ("input_procedure", "input_procedure_thru"),
1300
+ ("output_procedure", "output_procedure_thru"),
1301
+ ):
1302
+ first = getattr(stmt, first_attr, None)
1303
+ if isinstance(first, str) and first:
1304
+ thru = getattr(stmt, thru_attr, None)
1305
+ ranges.append((first, thru if isinstance(thru, str) else None))
1306
+ return ranges
1307
+
1308
+
1095
1309
  def _poison_written_names(
1096
1310
  statements: list[Any],
1097
1311
  index: ProgramLayoutIndex,
@@ -1135,10 +1349,43 @@ def _poison_written_names(
1135
1349
  if not target or target in visiting:
1136
1350
  continue # non-looping PERFORM cycle: stop rather than recurse forever
1137
1351
  body = _perform_body_statements(stmt, by_name, ordered_names)
1352
+ if body is None:
1353
+ env.clear() # an unknown target could have written anything
1354
+ continue
1138
1355
  visiting.add(target)
1139
1356
  frames.append((iter(body), target))
1140
1357
  continue
1358
+ if state is not None:
1359
+ if isinstance(stmt, (DisplayStatement, IOWriteStatement)):
1360
+ # A site inside a block that may run zero, one or many times:
1361
+ # what it shows is not knowable, so it vetoes the field.
1362
+ state.visited_sites.add(id(stmt))
1363
+ for operand in _site_operands(stmt):
1364
+ resolved = _safe_resolve(index, operand)
1365
+ if resolved is not None:
1366
+ state.observed.setdefault(
1367
+ resolved.spec.qualified_name, []
1368
+ ).append(None)
1369
+ transfer = _transfer_kind(stmt)
1370
+ if transfer is not None:
1371
+ # The block may leave the straight line: later statements may
1372
+ # not run, and a GO TO runs code this walk never sees.
1373
+ state.uncertain = True
1374
+ if transfer == "jump":
1375
+ env.clear()
1141
1376
  _poison_statement_writes(stmt, index, env, state=state)
1377
+ for first, thru in _procedure_ranges(stmt):
1378
+ # A SORT/MERGE INPUT/OUTPUT PROCEDURE runs once per record: a
1379
+ # loop body, poisoned like one.
1380
+ key = first.upper()
1381
+ if key in visiting:
1382
+ continue
1383
+ body = _range_statements(first, thru, by_name, ordered_names)
1384
+ if body is None:
1385
+ env.clear()
1386
+ continue
1387
+ visiting.add(key)
1388
+ frames.append((iter(body), key))
1142
1389
  for nested in _nested_statement_lists(stmt):
1143
1390
  _poison_written_names(
1144
1391
  nested, index, env, by_name, ordered_names, visiting, state=state