pyvbaanalysis 1.4.2__tar.gz → 2.1.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 (183) hide show
  1. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/CHANGELOG.md +149 -0
  2. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/PKG-INFO +19 -11
  3. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/README.md +16 -8
  4. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/docs/api-reference.md +6 -3
  5. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/docs/diagnostics-catalogue.md +3 -2
  6. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/docs/usage.md +43 -10
  7. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyproject.toml +1 -1
  8. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/__init__.py +13 -2
  9. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/cli.py +6 -4
  10. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/conditional/conditional_compilation.py +80 -3
  11. pyvbaanalysis-2.1.0/pyvbaanalysis/data/access_host_model.json +1 -0
  12. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/data/diagnostic_influence_audit.json +14 -0
  13. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/data/event_definitions.json +1 -1
  14. pyvbaanalysis-2.1.0/pyvbaanalysis/data/excel_host_model.json +1 -0
  15. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/data/manifest.json +12 -10
  16. pyvbaanalysis-2.1.0/pyvbaanalysis/data/powerpoint_host_model.json +1 -0
  17. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/data/rule_metadata.json +11 -0
  18. pyvbaanalysis-2.1.0/pyvbaanalysis/data/vba_runtime_tables.json +1 -0
  19. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/data/vbe_oracle_cases.json +64 -0
  20. pyvbaanalysis-2.1.0/pyvbaanalysis/data/word_host_model.json +1 -0
  21. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/analyze_module.py +39 -10
  22. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/context.py +9 -0
  23. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/registry.py +30 -8
  24. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/assignments.py +91 -15
  25. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/control_flow.py +54 -20
  26. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/declarations.py +28 -6
  27. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/duplicates.py +190 -58
  28. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/late_binding.py +5 -3
  29. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/module_kind.py +10 -3
  30. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/undeclared.py +8 -5
  31. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/walker.py +18 -1
  32. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/host/__init__.py +17 -1
  33. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/host/host_model.py +72 -29
  34. pyvbaanalysis-2.1.0/pyvbaanalysis/host/host_registry.py +115 -0
  35. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/nodes.py +6 -0
  36. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/parse_module.py +25 -1
  37. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/project.py +11 -0
  38. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/reader/__init__.py +14 -1
  39. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/reader/vbe_module.py +27 -7
  40. pyvbaanalysis-2.1.0/pyvbaanalysis/reader/workbook.py +230 -0
  41. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/symbols/project_index.py +11 -0
  42. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_cli.py +1 -1
  43. pyvbaanalysis-2.1.0/tests/test_conditional_arms.py +212 -0
  44. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_evidence.py +2 -2
  45. pyvbaanalysis-2.1.0/tests/test_host_seam.py +469 -0
  46. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rule_metadata.py +5 -5
  47. pyvbaanalysis-2.1.0/tests/test_upstream_sync_6_1_2.py +169 -0
  48. pyvbaanalysis-2.1.0/tools/extract_host_model.mjs +31 -0
  49. pyvbaanalysis-1.4.2/pyvbaanalysis/data/excel_host_model.json +0 -1
  50. pyvbaanalysis-1.4.2/pyvbaanalysis/data/vba_runtime_tables.json +0 -1
  51. pyvbaanalysis-1.4.2/pyvbaanalysis/reader/workbook.py +0 -106
  52. pyvbaanalysis-1.4.2/tools/extract_host_model.mjs +0 -12
  53. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/.gitattributes +0 -0
  54. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/.github/dependabot.yml +0 -0
  55. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/.github/workflows/ci.yml +0 -0
  56. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/.github/workflows/publish.yml +0 -0
  57. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/.gitignore +0 -0
  58. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/CONTRIBUTING.md +0 -0
  59. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/LICENSE +0 -0
  60. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/__main__.py +0 -0
  61. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/call/__init__.py +0 -0
  62. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/call/call_context.py +0 -0
  63. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/completion/__init__.py +0 -0
  64. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/completion/cursor_context.py +0 -0
  65. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/completion/event_handlers.py +0 -0
  66. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/completion/member_access.py +0 -0
  67. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/completion/type_completion.py +0 -0
  68. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/conditional/__init__.py +0 -0
  69. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/constants/__init__.py +0 -0
  70. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/constants/integer_constant_expression.py +0 -0
  71. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/__init__.py +0 -0
  72. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/argument_inference.py +0 -0
  73. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/call_extraction.py +0 -0
  74. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/callable_signatures.py +0 -0
  75. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/const_expr.py +0 -0
  76. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/dataflow.py +0 -0
  77. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/exprwalk.py +0 -0
  78. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/inline_suppression.py +0 -0
  79. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/model.py +0 -0
  80. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rule_metadata.py +0 -0
  81. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/__init__.py +0 -0
  82. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/argument_shape.py +0 -0
  83. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/argument_types.py +0 -0
  84. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/arrays.py +0 -0
  85. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/binary_operand_scalar.py +0 -0
  86. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/call_arity.py +0 -0
  87. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/expressions.py +0 -0
  88. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/lexical.py +0 -0
  89. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/numeric_literals.py +0 -0
  90. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/object_state.py +0 -0
  91. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/parameter_defaults.py +0 -0
  92. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/runtime_values.py +0 -0
  93. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/shared.py +0 -0
  94. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/diagnostics/rules/type_of_is.py +0 -0
  95. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/evidence.py +0 -0
  96. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/flow/__init__.py +0 -0
  97. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/flow/procedure_labels.py +0 -0
  98. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/flow/procedure_unstructured.py +0 -0
  99. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/identity_cache.py +0 -0
  100. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/__init__.py +0 -0
  101. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/keyword_table.py +0 -0
  102. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/stripped_lines.py +0 -0
  103. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/token_helpers.py +0 -0
  104. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/token_kinds.py +0 -0
  105. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/tokenize.py +0 -0
  106. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/lexer/trivia.py +0 -0
  107. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/__init__.py +0 -0
  108. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/fixed_length_string.py +0 -0
  109. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/parse_expression.py +0 -0
  110. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/parser_state.py +0 -0
  111. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/parser/type_declaration_suffix.py +0 -0
  112. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/py.typed +0 -0
  113. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/reader/loose_file.py +0 -0
  114. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/runtime/__init__.py +0 -0
  115. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/runtime/vba_runtime.py +0 -0
  116. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/symbols/__init__.py +0 -0
  117. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/symbols/build_module_symbols.py +0 -0
  118. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/symbols/name_resolution.py +0 -0
  119. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/symbols/symbol_model.py +0 -0
  120. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/types/__init__.py +0 -0
  121. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/types/type_inference.py +0 -0
  122. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/pyvbaanalysis/types/type_names.py +0 -0
  123. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/conftest.py +0 -0
  124. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/oracle_support.py +0 -0
  125. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_conditional_compilation.py +0 -0
  126. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_dataflow.py +0 -0
  127. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_diagnostics_engine.py +0 -0
  128. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_host_model.py +0 -0
  129. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_inline_suppression.py +0 -0
  130. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_integer_constant_expression.py +0 -0
  131. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_language_matrix.py +0 -0
  132. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_lexer.py +0 -0
  133. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_member_access_chains.py +0 -0
  134. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_parser.py +0 -0
  135. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_project.py +0 -0
  136. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_project_index.py +0 -0
  137. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_public_api.py +0 -0
  138. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_reader.py +0 -0
  139. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_real_world_false_positives.py +0 -0
  140. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_registry_parity.py +0 -0
  141. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_ambiguous_enum_member.py +0 -0
  142. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_argument_shape.py +0 -0
  143. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_argument_types.py +0 -0
  144. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_arrays.py +0 -0
  145. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_assignments.py +0 -0
  146. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_assignments_types.py +0 -0
  147. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_binary_operand_scalar.py +0 -0
  148. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_call_arity.py +0 -0
  149. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_control_flow.py +0 -0
  150. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_declarations.py +0 -0
  151. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_division_by_zero.py +0 -0
  152. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_duplicates.py +0 -0
  153. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_event_handler_module_scope.py +0 -0
  154. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_expression_calls.py +0 -0
  155. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_expressions.py +0 -0
  156. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_invalid_as_type_names.py +0 -0
  157. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_is_operator_non_object.py +0 -0
  158. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_late_binding.py +0 -0
  159. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_lexical.py +0 -0
  160. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_member_not_found.py +0 -0
  161. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_module_kind.py +0 -0
  162. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_numeric_literals.py +0 -0
  163. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_object_state.py +0 -0
  164. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_parameter_defaults.py +0 -0
  165. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_property_setter_value_parameters.py +0 -0
  166. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_runtime_values.py +0 -0
  167. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_scalar_and_foreach.py +0 -0
  168. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_type_of_is.py +0 -0
  169. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_typeof_is_always_false.py +0 -0
  170. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_rules_undeclared.py +0 -0
  171. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_runtime_tables.py +0 -0
  172. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_statement_token_derivation.py +0 -0
  173. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_stripped_lines.py +0 -0
  174. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_symbols.py +0 -0
  175. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_token_helpers.py +0 -0
  176. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_type_completion.py +0 -0
  177. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tests/test_type_names.py +0 -0
  178. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tools/extract_event_definitions.mjs +0 -0
  179. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tools/extract_rule_metadata.mjs +0 -0
  180. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tools/extract_runtime_tables.mjs +0 -0
  181. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tools/generate_diagnostics_catalogue.py +0 -0
  182. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tools/generate_manifest.py +0 -0
  183. {pyvbaanalysis-1.4.2 → pyvbaanalysis-2.1.0}/tools/oracle/build_combining_mark_probe.py +0 -0
@@ -5,6 +5,155 @@ All notable changes to pyVBAanalysis are recorded here. The format follows
5
5
  follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html): a minor version
6
6
  per milestone.
7
7
 
8
+ ## 2.1.0 - 2026-09-05
9
+
10
+ Sync to XLIDE 6.1.2, the analyzer's first re-pin since 3.1.4. Verified by a
11
+ differential against the upstream analyzer over the whole oracle corpus: 418 of
12
+ 418 cases identical, in both directions.
13
+
14
+ ### Added
15
+
16
+ * `ambiguousProjectProcedure`: VBA is content for two modules to export the same
17
+ public procedure name, but it refuses to compile an UNQUALIFIED call to that
18
+ name from a module declaring neither. The finding sits at the call site, not
19
+ the declarations, because a project that exports a name twice and always
20
+ qualifies its calls is legal VBA and common. Silent when the call is
21
+ qualified, when the calling module declares the name itself, when a local or
22
+ parameter shadows it, or when only one module exports it.
23
+ * `ConditionalActivityTracker.mutually_exclusive` and `in_same_branch`, the
24
+ primitive the arm-aware rules below need, plus
25
+ `ProjectIndex.implemented_interface_names()` and the
26
+ `AnalyzeModuleOptions.implemented_interfaces` option it feeds.
27
+
28
+ ### Fixed
29
+
30
+ * Rules no longer pair declarations from mutually exclusive `#If` arms. Only one
31
+ arm of a chain is ever built, so two declarations in different arms are
32
+ alternatives rather than duplicates. With a compiler constant the analyzer
33
+ cannot evaluate, the ordinary `#If VBA7 / #Else` idiom reported
34
+ `duplicate-procedure` and `duplicate-module-variable` on legal code. The same
35
+ reasoning now covers duplicate declarations and labels, undefined labels,
36
+ `For`/`Next` pairing, `Else` branch order, `Option` placement, and `Implements`
37
+ placement (XLIDE issue #58).
38
+ * A `#Const` directive may precede `Option Explicit`. A conditional-compilation
39
+ directive is not a declaration, and the live VBE compiles it there
40
+ (oracle case `const_directive_before_option_explicit_compile`).
41
+ * Three gaps in return-assignment detection, each of which the widened rule below
42
+ would otherwise have turned into a false positive:
43
+ assigning a field of the returned value (`MsToSystemTime.wYear = ...`) is a
44
+ return assignment; so is an assignment inside a single-line `If`, whose
45
+ branches the statement walk never entered; and a name that SPELLS a keyword is
46
+ still an assignment target, so `Function Read()` assigning `Read = True` was
47
+ read as never assigning its return. Measured on one real workbook these three
48
+ accounted for nine false findings.
49
+
50
+ ### Changed
51
+
52
+ * `missingReturnAssignment` now covers every Function and Property Get, not only
53
+ untyped ones, so a typed Function that never assigns its return reports. An
54
+ empty member of a module some other module declares with `Implements` stays
55
+ silent: that is a contract for an implementer to fill in, not unfinished code.
56
+ A body whose work is to raise stays silent too. **This reports on code that was
57
+ previously quiet**; the rule is warning severity and can be turned off through
58
+ `severity_overrides`.
59
+ * The vendored host models grew with upstream's move to the whole documented
60
+ object model: Excel 229 to 651 types, Word 364 to 627, PowerPoint 201 to 464,
61
+ Access 188 to 451. The wheel grows from 0.94 MB to 1.84 MB; the models still
62
+ load lazily, so import cost is unchanged.
63
+ * Data re-pinned to XLIDE 6.1.2: 418 oracle cases (was 415), 122 audited codes
64
+ (was 121), 119 rules (was 118).
65
+
66
+ ### Known limits
67
+
68
+ * Upstream's form-control member work is not ported, and the oracle corpus has no
69
+ UserForm designer cases, so that area is untested here rather than verified.
70
+
71
+ ## 2.0.0 - 2026-08-19
72
+
73
+ Multi-host analysis: VBA is now measured against the object model of the Office
74
+ host it actually belongs to. Ports the XLIDE host-model seam and the Word,
75
+ PowerPoint and Access models (xlide_vscode 790e6ea and e56098b, its issues #24
76
+ and #25).
77
+
78
+ ### Added
79
+
80
+ * A host seam on the analysis entry points. `analyze_project` and
81
+ `analyze_module_options_for` take a `host` token (`"excel"`, `"word"`,
82
+ `"powerpoint"`, `"access"`, ...), and `AnalyzeModuleOptions` carries `host`
83
+ alongside the existing `host_model`. The semantics are deliberately
84
+ asymmetric: absent means Excel, so every existing caller is unchanged, while a
85
+ NAMED host with no model asserts no host knowledge rather than falling back to
86
+ Excel's. An explicit `host_model` still outranks the token.
87
+ * Word, PowerPoint and Access object models, vendored as
88
+ `data/{word,powerpoint,access}_host_model.json` and extracted mechanically by
89
+ `tools/extract_host_model.mjs` from the generated XLIDE host modules, the same
90
+ pipeline that has always produced the Excel model. 364, 201 and 188
91
+ member-bearing types; 3,742, 1,480 and 1,116 enum constants. Every type is
92
+ non-exhaustive by construction, so absence never becomes a finding.
93
+ * `analyze_office_file(path)` and `read_office_modules(path)` read any container
94
+ pyOpenVBA opens: Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`), Word (`.docm`,
95
+ `.dotm`, `.doc`), PowerPoint (`.pptm`, `.potm`) and Access (`.accdb`, `.mdb`,
96
+ read-only). The extension selects the host, so a Word document is analyzed
97
+ against Word's model without the caller saying anything. The CLI accepts them
98
+ all on the same footing.
99
+ * `host_token_for_file_name` and `host_object_model_for_token` on the public API.
100
+
101
+ ### Fixed
102
+
103
+ * Word VBA no longer false-positives against Excel's object model. Analyzing a
104
+ Word module as a project previously reported four findings on legal code:
105
+ `ActiveDocument` twice and `wdOrientPortrait` as undeclared variables, and
106
+ `Selection.TypeText` as `member-not-found` against `Excel.Range`. Under the
107
+ Word host it reports none.
108
+ * Host metadata is per-model throughout, closing the Excel defaults that
109
+ remained under the seam. Application-member injection is keyed by model
110
+ (`Volatile` is a known bare call under Excel and unknown under Word), and the
111
+ four rules that consulted host metadata with a bare Excel default now take the
112
+ caller's model: undeclared variables, unknown call, ambiguous enum references,
113
+ and late-bound Friend members. `Me` types host-correctly in a document module:
114
+ `ThisWorkbook` is `Excel.Workbook` under Excel, `ThisDocument` is
115
+ `Word.Document` under Word, and nothing is asserted anywhere else.
116
+ * Word's `ThisDocument` classifies as a document module. It declares
117
+ `VB_Base = "1Normal.ThisDocument"`, naming no CLSID, so GUID matching missed
118
+ it; the host-generic signature is the `VB_PredeclaredId` + `VB_Exposed` pair
119
+ that Office gives every document module. A `.bas` extension or the container's
120
+ own standard-module flag still outranks that signature, since either states
121
+ outright that the module is standard. Differentialled across 116 modules in 16
122
+ real workbooks: nothing reclassified.
123
+ * A named host with no object model no longer reports what it cannot know. An
124
+ empty model is not uniformly quieter than Excel's: member lookups go silent
125
+ because no type resolves, but the rules that ask whether a bare name is legal
126
+ would answer no for every global the host injects, turning an Outlook
127
+ project's own surface into a wall of `undeclared-variable` findings. The four
128
+ rules that need host knowledge now stay silent under an unmodelled host, for
129
+ the same reason they already do on a partial project view.
130
+ * The host-model memos no longer key on a bare `id(model)`. Three caches
131
+ (`_host_model_index`, `_host_constant_index`, and the host-member name set)
132
+ used a plain `dict[int, ...]` that kept no reference to the model, so entries
133
+ accumulated one per model object ever seen and a collected model's id could be
134
+ recycled by a later one, serving one model's index for another. They now use
135
+ the repo's `IdentityLru`, which holds its keys alive and stays bounded. Latent
136
+ before this release, since only the permanently-live Excel singleton was ever
137
+ passed; reachable now that callers choose models.
138
+
139
+ ### Changed
140
+
141
+ * `analyze_workbook` and `read_workbook_modules` keep their Excel-only contract
142
+ unchanged. Handed a container the generic reader could open, they now name
143
+ `analyze_office_file` in the error rather than only rejecting the extension.
144
+ * README, the usage guide, the API reference and agent.md describe the four
145
+ hosts; agent.md's pyOpenVBA floor was stale at 3.0.1 and now reads 3.4.0.
146
+
147
+ ### Known limits
148
+
149
+ * Legacy `.ppt` is not readable. pyOpenVBA 3.4.0 lists it but reads it as a
150
+ plain CFB, while the VBA project lives in a zlib-compressed CFB inside an
151
+ `ExOleObjStg` record, so every open fails on the missing `dir` stream. The
152
+ extension is rejected up front rather than failing later with a parse error
153
+ that reads like file corruption. Reported as pyOpenVBA issue #17.
154
+ * Access is read-only, following pyOpenVBA: Access executes compiled p-code, so
155
+ a source write would silently change nothing.
156
+
8
157
  ## 1.4.2 - 2026-08-03
9
158
 
10
159
  ### Fixed
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pyvbaanalysis
3
- Version: 1.4.2
3
+ Version: 2.1.0
4
4
  Summary: Pure-Python static analysis for Excel VBA
5
5
  Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyVBAanalysis
6
6
  Project-URL: Repository, https://github.com/WilliamSmithEdward/pyVBAanalysis
@@ -24,16 +24,18 @@ Requires-Dist: twine>=5; extra == 'build'
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: mypy==2.3.0; extra == 'dev'
26
26
  Requires-Dist: pytest==9.1.1; extra == 'dev'
27
- Requires-Dist: ruff==0.16.0; extra == 'dev'
27
+ Requires-Dist: ruff==0.16.2; extra == 'dev'
28
28
  Description-Content-Type: text/markdown
29
29
 
30
30
  # pyVBAanalysis
31
31
 
32
- Static analysis for Excel VBA. It reads your macros and reports likely bugs and the
33
- errors the VBA compiler would catch, without opening Excel or running any code.
32
+ Static analysis for Office VBA. It reads your macros and reports likely bugs and the
33
+ errors the VBA compiler would catch, without opening Office or running any code.
34
34
 
35
- Point it at a workbook or a set of exported module files, and it returns the
36
- problems it finds, each with the exact line and a plain explanation.
35
+ Point it at an Excel workbook, a Word document, a PowerPoint deck, an Access
36
+ database, or a set of exported module files, and it returns the problems it finds,
37
+ each with the exact line and a plain explanation. Each file is measured against its
38
+ own host's object model, so Word code is never judged by Excel's surface.
37
39
 
38
40
  ## What it checks
39
41
 
@@ -63,9 +65,9 @@ Python 3.10 or later. Nothing else to set up.
63
65
  Analyze a workbook:
64
66
 
65
67
  ```python
66
- from pyvbaanalysis import analyze_workbook
68
+ from pyvbaanalysis import analyze_office_file
67
69
 
68
- for module, problems in analyze_workbook("Budget.xlsm").items():
70
+ for module, problems in analyze_office_file("Budget.xlsm").items():
69
71
  for p in problems:
70
72
  print(module, p.severity.value, p.code, p.message)
71
73
  ```
@@ -105,8 +107,14 @@ clean, so it drops into a CI check.
105
107
 
106
108
  ## Scope
107
109
 
108
- This analyzes Excel VBA. It does not run macros and does not need Excel installed.
109
- Word and PowerPoint are not supported.
110
+ This analyzes the VBA inside Office files. It does not run macros and does not need
111
+ Office installed.
112
+
113
+ Each file is measured against its own host's object model, so a Word document is
114
+ never judged by Excel's surface. Readable containers: Excel (.xlsm, .xlsb, .xlam,
115
+ .xls), Word (.docm, .dotm, .doc), PowerPoint (.pptm, .potm) and Access (.accdb,
116
+ .mdb, read-only). Legacy .ppt is not readable yet, because its VBA project sits in
117
+ a compressed record the reader does not open.
110
118
 
111
119
  ## Documentation
112
120
 
@@ -1,10 +1,12 @@
1
1
  # pyVBAanalysis
2
2
 
3
- Static analysis for Excel VBA. It reads your macros and reports likely bugs and the
4
- errors the VBA compiler would catch, without opening Excel or running any code.
3
+ Static analysis for Office VBA. It reads your macros and reports likely bugs and the
4
+ errors the VBA compiler would catch, without opening Office or running any code.
5
5
 
6
- Point it at a workbook or a set of exported module files, and it returns the
7
- problems it finds, each with the exact line and a plain explanation.
6
+ Point it at an Excel workbook, a Word document, a PowerPoint deck, an Access
7
+ database, or a set of exported module files, and it returns the problems it finds,
8
+ each with the exact line and a plain explanation. Each file is measured against its
9
+ own host's object model, so Word code is never judged by Excel's surface.
8
10
 
9
11
  ## What it checks
10
12
 
@@ -34,9 +36,9 @@ Python 3.10 or later. Nothing else to set up.
34
36
  Analyze a workbook:
35
37
 
36
38
  ```python
37
- from pyvbaanalysis import analyze_workbook
39
+ from pyvbaanalysis import analyze_office_file
38
40
 
39
- for module, problems in analyze_workbook("Budget.xlsm").items():
41
+ for module, problems in analyze_office_file("Budget.xlsm").items():
40
42
  for p in problems:
41
43
  print(module, p.severity.value, p.code, p.message)
42
44
  ```
@@ -76,8 +78,14 @@ clean, so it drops into a CI check.
76
78
 
77
79
  ## Scope
78
80
 
79
- This analyzes Excel VBA. It does not run macros and does not need Excel installed.
80
- Word and PowerPoint are not supported.
81
+ This analyzes the VBA inside Office files. It does not run macros and does not need
82
+ Office installed.
83
+
84
+ Each file is measured against its own host's object model, so a Word document is
85
+ never judged by Excel's surface. Readable containers: Excel (.xlsm, .xlsb, .xlam,
86
+ .xls), Word (.docm, .dotm, .doc), PowerPoint (.pptm, .potm) and Access (.accdb,
87
+ .mdb, read-only). Legacy .ppt is not readable yet, because its VBA project sits in
88
+ a compressed record the reader does not open.
81
89
 
82
90
  ## Documentation
83
91
 
@@ -11,11 +11,14 @@ These are re-exported at the package root for convenience:
11
11
  | Symbol | Role |
12
12
  | --- | --- |
13
13
  | `analyze_module(source, opts=None)` | Analyze one module's source; returns `list[VbaDiagnostic]`. Never raises. |
14
- | `analyze_project(modules, *, only=None, severity_overrides=None, conditional_compilation=None, whole_project=True, inline_suppression=True)` | Analyze a set of `ModuleInput`s with cross-module context; returns `dict[str, list[VbaDiagnostic]]`. `whole_project=False` for a partial set. |
14
+ | `analyze_project(modules, *, only=None, severity_overrides=None, conditional_compilation=None, whole_project=True, inline_suppression=True, host=None)` | Analyze a set of `ModuleInput`s with cross-module context; returns `dict[str, list[VbaDiagnostic]]`. `whole_project=False` for a partial set. `host` names the Office host (absent means Excel). |
15
15
  | `analyze_loose_file(path, *, severity_overrides=None, conditional_compilation=None, whole_project=False, inline_suppression=True)` | Analyze one `.bas`/`.cls`/`.frm` file (partial by default; the whole-project checks are skipped). |
16
16
  | `analyze_loose_files(paths, *, only=None, severity_overrides=None, conditional_compilation=None, whole_project=True, inline_suppression=True)` | Analyze several loose files as one project. |
17
17
  | `analyze_workbook(path, *, only=None, severity_overrides=None, conditional_compilation=None, inline_suppression=True)` | Analyze the VBA in an Excel workbook (via pyOpenVBA). |
18
- | `analyze_module_options_for(index, name, kind, *, severity_overrides=None, conditional_compilation=None, whole_project=True, inline_suppression=True)` | Build per-module `AnalyzeModuleOptions` from a populated `ProjectIndex`. |
18
+ | `analyze_office_file(path, *, only=None, severity_overrides=None, conditional_compilation=None, inline_suppression=True)` | The same for any readable Office container (Excel, Word, PowerPoint, Access); the extension selects the host model. |
19
+ | `host_token_for_file_name(name)` | The host a container implies (`excel`, `word`, `powerpoint`, `access`), or `None`. |
20
+ | `host_object_model_for_token(host)` | The model a host token selects: `None` for Excel (the default), the host's own model, or the empty model when a named host has none. |
21
+ | `analyze_module_options_for(index, name, kind, *, severity_overrides=None, conditional_compilation=None, whole_project=True, inline_suppression=True, host=None)` | Build per-module `AnalyzeModuleOptions` from a populated `ProjectIndex`. |
19
22
  | `build_project_index(modules)` | A `ProjectIndex` with every module registered. |
20
23
  | `AnalyzeModuleOptions` | Inputs for `analyze_module` (name, kind, project context, overrides). |
21
24
  | `VbaDiagnostic` | A single diagnostic (`code`, `message`, `severity`, `span`, `spec_reference`). |
@@ -69,7 +72,7 @@ them directly, but they are public and stable.
69
72
  | `pyvbaanalysis.conditional` | Conditional-compilation indexing and activity (`index_conditional_compilation`, `evaluate_conditional_expression`, ...). |
70
73
  | `pyvbaanalysis.types` | Type-name helpers: `normalize_type`, `is_known_scalar_type`, `is_numeric_type`, `numeric_literal_bounds`. |
71
74
  | `pyvbaanalysis.completion` | Member-completion surface and project-type resolution (`resolve_member_surface_at`, `resolve_type_name`, ...). |
72
- | `pyvbaanalysis.host` | The Excel host object model (`get_excel_object_model`, `resolve_host_alias`, ...). |
75
+ | `pyvbaanalysis.host` | The Excel, Word, PowerPoint and Access host object models and the host registry (`get_excel_object_model`, `host_object_model_for_token`, `resolve_host_alias`, ...). |
73
76
  | `pyvbaanalysis.runtime` | VBA runtime functions, constants, and objects (`resolve_runtime_function`, ...). |
74
77
  | `pyvbaanalysis.call` | Call-statement shape helpers. |
75
78
  | `pyvbaanalysis.flow` | Procedure labels and unstructured-flow detection. |
@@ -1,6 +1,6 @@
1
1
  # Diagnostic catalogue
2
2
 
3
- The diagnostic codes pyVBAanalysis can emit, generated from the rule metadata (`tools/generate_diagnostics_catalogue.py`). This table lists the 118 rule-metadata codes across 6 categories. A further 3 structural block-balance codes (`mismatched-end-keyword`, `missing-block-closer`, `unmatched-block-closer`) are emitted by the parser pass and are not in the metadata table, for a full set of 121 codes.
3
+ The diagnostic codes pyVBAanalysis can emit, generated from the rule metadata (`tools/generate_diagnostics_catalogue.py`). This table lists the 119 rule-metadata codes across 6 categories. A further 3 structural block-balance codes (`mismatched-end-keyword`, `missing-block-closer`, `unmatched-block-closer`) are emitted by the parser pass and are not in the metadata table, for a full set of 122 codes.
4
4
 
5
5
  Each code is reported only when it is provably correct; anything unknown or ambiguous stays quiet (the no-false-positive discipline). The **kind** column says what a code means: a *compile error* is rejected by the VBE compiler, a *runtime error* is a deterministic Run-time error, a *runtime risk* is a likely fault, and *style* is advisory.
6
6
 
@@ -69,11 +69,12 @@ Override a code's severity with `AnalyzeModuleOptions.severity_overrides` (or th
69
69
  | `undeclared-variable` | Variable not defined | error | compile error | MS-VBAL 5.2.4.1.1 |
70
70
  | `unknown-call` | Sub or Function not defined | error | compile error | MS-VBAL 5.4.2.1 |
71
71
 
72
- ## Semantic (50)
72
+ ## Semantic (51)
73
73
 
74
74
  | Code | Title | Default | Kind | Spec reference |
75
75
  | --- | --- | --- | --- | --- |
76
76
  | `ambiguous-enum-member` | Ambiguous Enum member reference | error | compile error | VBE compiler: Ambiguous name detected |
77
+ | `ambiguous-project-procedure` | Ambiguous unqualified call to a name two modules export | error | compile error | VBE compiler: Ambiguous name detected |
77
78
  | `argument-count` | Wrong number of arguments | error | compile error | MS-VBAL 5.4.2.1 |
78
79
  | `argument-object-type-mismatch` | Object argument type mismatch | error | compile error | MS-VBAL 5.3.1 |
79
80
  | `argument-shape-mismatch` | Argument shape (array/Type vs scalar) mismatch | error | compile error | MS-VBAL 5.3.1 (argument passing) / VBE compiler: ByRef argument type mismatch; array or user-defined type expected |
@@ -136,21 +136,53 @@ analyze_loose_files(["Module1.bas", "Widget.cls"]) # several, as one project
136
136
  skipped for it (see "Whole project vs a single file" below). Use `analyze_loose_files`
137
137
  to analyze several files together with shared cross-module context.
138
138
 
139
- ### Excel workbooks (.xlsm / .xlsb / .xlam / .xls)
139
+ ### Office macro containers
140
140
 
141
- The workbook reader reads VBA directly out of an Excel file via pyOpenVBA (the one
141
+ The container reader reads VBA directly out of an Office file via pyOpenVBA (the one
142
142
  runtime dependency). pyOpenVBA is imported lazily, so `import pyvbaanalysis` stays
143
- light. The analyzer targets Excel VBA; Word and PowerPoint are out of scope.
143
+ light.
144
+
145
+ | Host | Extensions |
146
+ | --- | --- |
147
+ | Excel | `.xlsm`, `.xlsb`, `.xlam`, `.xls` |
148
+ | Word | `.docm`, `.dotm`, `.doc` |
149
+ | PowerPoint | `.pptm`, `.potm` |
150
+ | Access | `.accdb`, `.mdb` (read-only) |
144
151
 
145
152
  ```python
146
- from pyvbaanalysis import analyze_workbook
153
+ from pyvbaanalysis import analyze_office_file
147
154
 
148
- analyze_workbook("Book.xlsm") # dict: module name -> diagnostics
149
- analyze_workbook("Book.xlsm", only=["Sheet1"]) # one module by name, full context
155
+ analyze_office_file("Book.xlsm") # dict: module name -> diagnostics
156
+ analyze_office_file("Report.docm") # resolved against Word's model
157
+ analyze_office_file("Book.xlsm", only=["Sheet1"]) # one module by name, full context
150
158
  ```
151
159
 
152
- A path that is not an Excel workbook, or a container with no readable VBA, raises
153
- `WorkbookReadError`.
160
+ The extension selects the host, so Word code is measured against Word's object model
161
+ and never against Excel's. Analyzing a Word module under Excel's model reports
162
+ members and constants that are perfectly legal in Word (`Selection.TypeText`,
163
+ `ActiveDocument`, `wdOrientPortrait`), which is the false positive this avoids.
164
+
165
+ `analyze_workbook` remains the Excel-only entry point and is unchanged.
166
+
167
+ A path that is not a readable container, or a container with no readable VBA, raises
168
+ `WorkbookReadError`. Legacy `.ppt` is not readable: pyOpenVBA reads it as a plain
169
+ CFB, but its VBA project sits inside a compressed record, so the extension is
170
+ rejected rather than failing later with a parse error
171
+ ([pyOpenVBA #17](https://github.com/WilliamSmithEdward/pyOpenVBA/issues/17)).
172
+
173
+ ### Choosing the host yourself
174
+
175
+ When you already have the module text, name the host directly. Absent means Excel,
176
+ so existing calls are unchanged; a named host with no model asserts no host
177
+ knowledge at all rather than falling back to Excel's.
178
+
179
+ ```python
180
+ from pyvbaanalysis import analyze_project
181
+
182
+ analyze_project(modules, host="word") # Word's object model
183
+ analyze_project(modules, host="outlook") # no model yet: stays quiet, never Excel's
184
+ analyze_project(modules) # Excel, exactly as before
185
+ ```
154
186
 
155
187
  The reader loads the workbook and its VBA into memory and does not bound the input
156
188
  size, so impose your own limit (for example a maximum file size) before pointing it
@@ -227,8 +259,9 @@ pyvbaanalysis Book.xlsm --format json
227
259
  ```
228
260
 
229
261
  A path may be a loose file, a folder of loose files (analyzed together as one
230
- project), or an Excel workbook. `pyvbaanalysis --version` prints the version, and
231
- `python -m pyvbaanalysis` is equivalent to the `pyvbaanalysis` command.
262
+ project), or an Office macro container, whose extension selects the host model.
263
+ `pyvbaanalysis --version` prints the version, and `python -m pyvbaanalysis` is
264
+ equivalent to the `pyvbaanalysis` command.
232
265
 
233
266
  Flags:
234
267
 
@@ -38,7 +38,7 @@ dependencies = ["pyOpenVBA>=3.4.0"]
38
38
  # underneath main (ruff 0.16 broke a green tree with zero repo commits).
39
39
  # Dependabot's weekly pip updates turn each tool upgrade into its own gated
40
40
  # PR, where a red CI run blocks the bump instead of breaking main.
41
- dev = ["pytest==9.1.1", "ruff==0.16.0", "mypy==2.3.0"]
41
+ dev = ["pytest==9.1.1", "ruff==0.16.2", "mypy==2.3.0"]
42
42
  build = ["build>=1.2", "twine>=5"]
43
43
 
44
44
  [project.scripts]
@@ -9,6 +9,8 @@ The headline entry points are re-exported here for convenience:
9
9
  * analyze_project(modules): analyze a set of modules with cross-module context.
10
10
  * analyze_workbook(path): analyze the VBA in an Excel workbook (read via pyOpenVBA,
11
11
  the one external runtime dependency).
12
+ * analyze_office_file(path): the same for any Office macro container (Excel, Word,
13
+ PowerPoint, or read-only Access), resolving against that host's object model.
12
14
  * analyze_loose_file(path) / analyze_loose_files(paths): analyze loose
13
15
  .bas / .cls / .frm export files.
14
16
 
@@ -30,10 +32,16 @@ from .diagnostics import (
30
32
  validate_severity_overrides,
31
33
  )
32
34
  from .project import analyze_module_options_for, analyze_project, build_project_index
33
- from .reader import analyze_loose_file, analyze_loose_files, analyze_workbook
35
+ from .host import host_object_model_for_token, host_token_for_file_name
36
+ from .reader import (
37
+ analyze_loose_file,
38
+ analyze_loose_files,
39
+ analyze_office_file,
40
+ analyze_workbook,
41
+ )
34
42
  from .symbols import ModuleInput, ModuleSymbolKind, ProjectIndex, ProjectIndexOptions
35
43
 
36
- __version__ = "1.4.2"
44
+ __version__ = "2.1.0"
37
45
 
38
46
  __all__ = [
39
47
  "AnalyzeModuleOptions",
@@ -50,9 +58,12 @@ __all__ = [
50
58
  "analyze_loose_files",
51
59
  "analyze_module",
52
60
  "analyze_module_options_for",
61
+ "analyze_office_file",
53
62
  "analyze_project",
54
63
  "analyze_workbook",
55
64
  "build_project_index",
65
+ "host_object_model_for_token",
66
+ "host_token_for_file_name",
56
67
  "line_col",
57
68
  "rule_metadata_by_code",
58
69
  "validate_severity_overrides",
@@ -30,13 +30,14 @@ from .diagnostics import (
30
30
  validate_severity_overrides,
31
31
  )
32
32
  from .project import analyze_project
33
+ from .host import host_token_for_file_name
33
34
  from .reader import (
34
- EXCEL_EXTENSIONS,
35
35
  LOOSE_EXTENSIONS,
36
+ OFFICE_EXTENSIONS,
36
37
  LooseFileReadError,
37
38
  WorkbookReadError,
38
39
  load_loose_module,
39
- read_workbook_modules,
40
+ read_office_modules,
40
41
  )
41
42
  from .symbols import ModuleInput
42
43
 
@@ -151,7 +152,7 @@ def _analyze_workbook_group(
151
152
  errors: list[str] = []
152
153
  for path in paths:
153
154
  try:
154
- modules = read_workbook_modules(path)
155
+ modules = read_office_modules(path)
155
156
  except WorkbookReadError as exc:
156
157
  errors.append(f"{path}: {exc}")
157
158
  continue
@@ -163,6 +164,7 @@ def _analyze_workbook_group(
163
164
  only=only or None,
164
165
  severity_overrides=severity_overrides or None,
165
166
  inline_suppression=inline_suppression,
167
+ host=host_token_for_file_name(path.name),
166
168
  )
167
169
  sources = {m.name: m.source for m in modules}
168
170
  results.append(_ProjectResult(str(path), diagnostics, sources))
@@ -332,7 +334,7 @@ def main(argv: Sequence[str] | None = None) -> int:
332
334
  print("error: unknown diagnostic code(s): " + ", ".join(unknown_codes), file=sys.stderr)
333
335
  return 2
334
336
 
335
- workbook_paths = [p for p in paths if p.is_file() and p.suffix.lower() in EXCEL_EXTENSIONS]
337
+ workbook_paths = [p for p in paths if p.is_file() and p.suffix.lower() in OFFICE_EXTENSIONS]
336
338
  other_paths = [p for p in paths if p not in workbook_paths]
337
339
  loose_paths = _gather_loose_paths(other_paths)
338
340
  unknown = [p for p in loose_paths if p.suffix.lower() not in LOOSE_EXTENSIONS]
@@ -114,10 +114,42 @@ class _ConditionalFrame:
114
114
  seen_unknown: bool
115
115
 
116
116
 
117
+ @dataclass(frozen=True, slots=True)
118
+ class _ConditionalArm:
119
+ """One arm of one `#If` chain, as a persistent stack.
120
+
121
+ ``parent`` is the enclosing chain's arm. Immutable, so an event can keep the
122
+ arm that was in effect when it was recorded without copying the stack, and
123
+ one arm is one object, which makes identity the comparison for
124
+ ``in_same_branch``.
125
+ """
126
+
127
+ chain: int
128
+ #: 0 for the `#If`, then one per `#ElseIf` / `#Else`.
129
+ index: int
130
+ parent: "_ConditionalArm | None"
131
+
132
+
133
+ def _arms_diverge(a: _ConditionalArm | None, b: _ConditionalArm | None) -> bool:
134
+ """Whether two arm stacks disagree about which arm of a shared chain they are
135
+ in. The branches then exclude each other whatever the constants are worth.
136
+ Stacks are as deep as the source nests directives, so the walk is short."""
137
+ outer = a
138
+ while outer is not None:
139
+ inner = b
140
+ while inner is not None:
141
+ if outer.chain == inner.chain:
142
+ return outer.index != inner.index
143
+ inner = inner.parent
144
+ outer = outer.parent
145
+ return False
146
+
147
+
117
148
  @dataclass(slots=True)
118
149
  class _ConditionalActivityEvent:
119
150
  start: int
120
151
  activity: ConditionalActivity
152
+ branch: _ConditionalArm | None = None
121
153
 
122
154
 
123
155
  class ConditionalActivityTracker:
@@ -128,7 +160,7 @@ class ConditionalActivityTracker:
128
160
  def __init__(self, events: list[_ConditionalActivityEvent]) -> None:
129
161
  self._events = events
130
162
 
131
- def activity_for_span(self, span: Span) -> ConditionalActivity:
163
+ def _event_for_span(self, span: Span) -> _ConditionalActivityEvent | None:
132
164
  # Directives starting at or after the queried offset are not applied.
133
165
  lo = -1
134
166
  hi = len(self._events) - 1
@@ -138,11 +170,41 @@ class ConditionalActivityTracker:
138
170
  lo = mid
139
171
  else:
140
172
  hi = mid - 1
141
- return self._events[lo].activity if lo >= 0 else ConditionalActivity.ACTIVE
173
+ return self._events[lo] if lo >= 0 else None
174
+
175
+ def activity_for_span(self, span: Span) -> ConditionalActivity:
176
+ event = self._event_for_span(span)
177
+ return event.activity if event is not None else ConditionalActivity.ACTIVE
142
178
 
143
179
  def is_inactive(self, span: Span) -> bool:
144
180
  return self.activity_for_span(span) is ConditionalActivity.INACTIVE
145
181
 
182
+ def mutually_exclusive(self, a: Span, b: Span) -> bool:
183
+ """Whether the two spans sit in different arms of one `#If` chain, and so
184
+ are never compiled together however the constants evaluate."""
185
+ left = self._event_for_span(a)
186
+ right = self._event_for_span(b)
187
+ return _arms_diverge(
188
+ left.branch if left is not None else None,
189
+ right.branch if right is not None else None,
190
+ )
191
+
192
+ def in_same_branch(self, a: Span, b: Span) -> bool:
193
+ """Whether the two spans sit under exactly the same arms, so every build
194
+ either compiles both or neither.
195
+
196
+ Stricter than "not mutually exclusive": spans in two SEPARATE chains are
197
+ neither exclusive nor in the same branch, because a build may take one and
198
+ not the other. Rules that pair two pieces of one construct need this, since
199
+ a pairing made across different chains is a guess about a build that may
200
+ never exist.
201
+ """
202
+ left = self._event_for_span(a)
203
+ right = self._event_for_span(b)
204
+ return (left.branch if left is not None else None) is (
205
+ right.branch if right is not None else None
206
+ )
207
+
146
208
 
147
209
  def create_conditional_activity_tracker(
148
210
  module: ModuleNode, env: ConditionalCompilationEnvironment | None = None
@@ -162,10 +224,25 @@ def _collect_conditional_activity_events(
162
224
  project_constants = _lowercased(effective_env.project_constants)
163
225
  stack: list[_ConditionalFrame] = []
164
226
  current = ConditionalActivity.ACTIVE
227
+ branch: _ConditionalArm | None = None
228
+ chains = 0
165
229
  events: list[_ConditionalActivityEvent] = []
166
230
  for occ in directives:
167
231
  current = _apply_conditional_directive(occ.directive, effective_env, project_constants, stack, current)
168
- events.append(_ConditionalActivityEvent(start=occ.directive.span.start, activity=current))
232
+ kind = occ.directive.directive_kind
233
+ if kind is ConditionalDirectiveKind.IF:
234
+ branch = _ConditionalArm(chain=chains, index=0, parent=branch)
235
+ chains += 1
236
+ elif kind in (ConditionalDirectiveKind.ELSE_IF, ConditionalDirectiveKind.ELSE):
237
+ # An `#ElseIf` with no open `#If` is a parse-level error; leave the
238
+ # stack alone rather than inventing an arm for it.
239
+ if branch is not None:
240
+ branch = _ConditionalArm(chain=branch.chain, index=branch.index + 1, parent=branch.parent)
241
+ elif kind is ConditionalDirectiveKind.END_IF:
242
+ branch = branch.parent if branch is not None else None
243
+ events.append(
244
+ _ConditionalActivityEvent(start=occ.directive.span.start, activity=current, branch=branch)
245
+ )
169
246
  return events
170
247
 
171
248