expressir 2.4.29-arm-linux-musl
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.
- checksums.yaml +7 -0
- data/.cargo/config.toml +3 -0
- data/.github/scripts/test_installed_gem.rb +34 -0
- data/.github/workflows/codeql.yml +35 -0
- data/.github/workflows/docs.yml +99 -0
- data/.github/workflows/links.yml +100 -0
- data/.github/workflows/native-gems.yml +201 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +37 -0
- data/.github/workflows/rust-ext.yml +41 -0
- data/.github/workflows/stress.yml +36 -0
- data/.github/workflows/validate_schemas.yml +48 -0
- data/.github/workflows/verify_remarks.yml +33 -0
- data/.gitignore +55 -0
- data/.hound.yml +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +297 -0
- data/CHANGELOG.md +159 -0
- data/Gemfile +18 -0
- data/README.adoc +2783 -0
- data/Rakefile +41 -0
- data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
- data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
- data/TODO.bugs/03-builder-mutable-state.md +43 -0
- data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
- data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
- data/TODO.bugs/06-collection-registry-single-source.md +53 -0
- data/TODO.bugs/07-require-relative-cleanup.md +42 -0
- data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
- data/TODO.bugs/09-parser-split.md +53 -0
- data/TODO.bugs/10-to-s-override.md +42 -0
- data/TODO.bugs/11-parser-class-variables.md +39 -0
- data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
- data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
- data/TODO.bugs/14-model-formatting-leak.md +30 -0
- data/TODO.bugs/15-expression-children-macro.md +27 -0
- data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
- data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
- data/TODO.bugs/18-const-get-private-constants.md +30 -0
- data/TODO.bugs/19-format-methods-public.md +22 -0
- data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
- data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
- data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
- data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
- data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
- data/TODO.bugs/25-debug-puts-in-production.md +21 -0
- data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
- data/TODO.bugs/27-package-build-god-method.md +19 -0
- data/TODO.bugs/28-package-god-class.md +30 -0
- data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
- data/TODO.bugs/30-unicode-map-extraction.md +19 -0
- data/TODO.bugs/README.md +43 -0
- data/TODO.max-perf/01-restore-ci-green.md +36 -0
- data/TODO.max-perf/02-streaming-parse-path.md +31 -0
- data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
- data/TODO.max-perf/04-benchmark-harness.md +28 -0
- data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
- data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
- data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
- data/TODO.max-perf/08-builder-build-perf.md +45 -0
- data/TODO.max-perf/09-grammar-cold-start.md +25 -0
- data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
- data/TODO.max-perf/11-ci-green-closeout.md +25 -0
- data/TODO.max-perf/12-require-boot-profile.md +25 -0
- data/TODO.max-perf/13-key-conversion-specs.md +26 -0
- data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
- data/TODO.parity-ee/00-overview.md +76 -0
- data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
- data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
- data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
- data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
- data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
- data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
- data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
- data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
- data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
- data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
- data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
- data/TODO.parity-ee/12-full-check-catalog.md +67 -0
- data/TODO.parity-ee/13-population-p21.md +54 -0
- data/TODO.parity-ee/14-shtolo-completion.md +44 -0
- data/TODO.parity-ee/15-part28-xml.md +51 -0
- data/TODO.parity-ee/16-compare-patch.md +48 -0
- data/TODO.parity-ee/17-official-tests.md +42 -0
- data/TODO.parity-ee/18-mim-mapping.md +47 -0
- data/TODO.parity-ee/19-architecture.md +97 -0
- data/TODO.parity-ee/parity-matrix.md +108 -0
- data/TODO.refpath-v2/00-overview.md +64 -0
- data/TODO.suma-improvements/00-overview.md +23 -0
- data/TODO.suma-improvements/01-prebuilt-platform-gems.md +33 -0
- data/TODO.suma-improvements/02-suma-e2e.md +39 -0
- data/TODO.suma-improvements/03-item-graph.md +22 -0
- data/TODO.suma-improvements/04-compiled-set-default.md +16 -0
- data/TODO.suma-improvements/05-validation-in-docs.md +17 -0
- data/TODO.suma-improvements/06-memory-remeasure.md +36 -0
- data/TODO.suma-improvements/07-part28-xml.md +15 -0
- data/TODO.suma-improvements/08-windows-gnu-toolchain.md +15 -0
- data/TODO.suma-improvements/09-checker-tranche3.md +13 -0
- data/TODO.suma-improvements/10-expressdoc.md +8 -0
- data/benchmark/srl_benchmark.rb +458 -0
- data/benchmark/srl_native_benchmark.rb +146 -0
- data/benchmark/srl_ruby_benchmark.rb +132 -0
- data/bin/console +11 -0
- data/bin/rspec +30 -0
- data/bin/setup +8 -0
- data/docs/Gemfile +12 -0
- data/docs/_config.yml +141 -0
- data/docs/_guides/changes/changes-format.adoc +778 -0
- data/docs/_guides/changes/importing-eengine.adoc +898 -0
- data/docs/_guides/changes/index.adoc +396 -0
- data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
- data/docs/_guides/changes/validating-changes.adoc +681 -0
- data/docs/_guides/cli/benchmark-performance.adoc +834 -0
- data/docs/_guides/cli/coverage-analysis.adoc +921 -0
- data/docs/_guides/cli/format-schemas.adoc +547 -0
- data/docs/_guides/cli/index.adoc +8 -0
- data/docs/_guides/cli/managing-changes.adoc +927 -0
- data/docs/_guides/cli/validate-ascii.adoc +645 -0
- data/docs/_guides/cli/validate-schemas.adoc +534 -0
- data/docs/_guides/formatter/formatter-architecture.adoc +401 -0
- data/docs/_guides/index.adoc +165 -0
- data/docs/_guides/ler/creating-packages.adoc +664 -0
- data/docs/_guides/ler/index.adoc +305 -0
- data/docs/_guides/ler/loading-packages.adoc +707 -0
- data/docs/_guides/ler/package-formats.adoc +748 -0
- data/docs/_guides/ler/querying-packages.adoc +826 -0
- data/docs/_guides/ler/step-packages.adoc +385 -0
- data/docs/_guides/ler/validating-packages.adoc +750 -0
- data/docs/_guides/liquid/basic-templates.adoc +813 -0
- data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
- data/docs/_guides/liquid/drops-reference.adoc +829 -0
- data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
- data/docs/_guides/liquid/index.adoc +468 -0
- data/docs/_guides/manifests/creating-manifests.adoc +483 -0
- data/docs/_guides/manifests/index.adoc +307 -0
- data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
- data/docs/_guides/manifests/validating-manifests.adoc +713 -0
- data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
- data/docs/_guides/ruby-api/index.adoc +257 -0
- data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
- data/docs/_guides/ruby-api/search-engine.adoc +609 -0
- data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
- data/docs/_pages/data-model.adoc +665 -0
- data/docs/_pages/express-language.adoc +506 -0
- data/docs/_pages/getting-started.adoc +414 -0
- data/docs/_pages/index.adoc +116 -0
- data/docs/_pages/introduction.adoc +256 -0
- data/docs/_pages/ler-packages.adoc +837 -0
- data/docs/_pages/parsers.adoc +709 -0
- data/docs/_pages/schema-manifests.adoc +431 -0
- data/docs/_references/index.adoc +228 -0
- data/docs/_tutorials/creating-ler-package.adoc +735 -0
- data/docs/_tutorials/documentation-coverage.adoc +795 -0
- data/docs/_tutorials/formatting-schemas.adoc +89 -0
- data/docs/_tutorials/index.adoc +231 -0
- data/docs/_tutorials/liquid-templates.adoc +806 -0
- data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
- data/docs/_tutorials/querying-schemas.adoc +751 -0
- data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
- data/docs/index.adoc +242 -0
- data/docs/lychee.toml +90 -0
- data/examples/demo_ler_usage.sh +86 -0
- data/examples/ler/README.md +111 -0
- data/examples/ler/simple_example.ler +0 -0
- data/examples/ler/simple_schema.exp +33 -0
- data/examples/ler_build.rb +75 -0
- data/examples/ler_cli.rb +79 -0
- data/examples/ler_demo_complete.rb +276 -0
- data/examples/ler_query.rb +91 -0
- data/examples/ler_query_examples.rb +305 -0
- data/examples/ler_stats.rb +81 -0
- data/examples/phase3_demo.rb +159 -0
- data/examples/query_demo_simple.rb +131 -0
- data/exe/expressir +11 -0
- data/exe/expressir-format +14 -0
- data/exe/expressir-format-test +82 -0
- data/expressir.gemspec +61 -0
- data/lib/expressir/3.2/expressir_core.so +0 -0
- data/lib/expressir/3.3/expressir_core.so +0 -0
- data/lib/expressir/3.4/expressir_core.so +0 -0
- data/lib/expressir/4.0/expressir_core.so +0 -0
- data/lib/expressir/benchmark.rb +310 -0
- data/lib/expressir/changes/item_change.rb +20 -0
- data/lib/expressir/changes/mapping_change.rb +16 -0
- data/lib/expressir/changes/schema_change.rb +85 -0
- data/lib/expressir/changes/version_change.rb +26 -0
- data/lib/expressir/changes.rb +9 -0
- data/lib/expressir/cli.rb +148 -0
- data/lib/expressir/commands/base.rb +25 -0
- data/lib/expressir/commands/benchmark.rb +59 -0
- data/lib/expressir/commands/benchmark_cache.rb +80 -0
- data/lib/expressir/commands/changes.rb +34 -0
- data/lib/expressir/commands/changes_import_eengine.rb +146 -0
- data/lib/expressir/commands/changes_validate.rb +63 -0
- data/lib/expressir/commands/check.rb +56 -0
- data/lib/expressir/commands/clean.rb +20 -0
- data/lib/expressir/commands/coverage.rb +458 -0
- data/lib/expressir/commands/expand.rb +26 -0
- data/lib/expressir/commands/file_violations.rb +70 -0
- data/lib/expressir/commands/fix.rb +33 -0
- data/lib/expressir/commands/flatten.rb +29 -0
- data/lib/expressir/commands/format.rb +43 -0
- data/lib/expressir/commands/manifest.rb +420 -0
- data/lib/expressir/commands/mapping_validate.rb +68 -0
- data/lib/expressir/commands/non_ascii_character.rb +49 -0
- data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
- data/lib/expressir/commands/package.rb +1222 -0
- data/lib/expressir/commands/parity_inputs.rb +160 -0
- data/lib/expressir/commands/validate.rb +106 -0
- data/lib/expressir/commands/validate_ascii.rb +94 -0
- data/lib/expressir/commands/validate_load.rb +91 -0
- data/lib/expressir/commands/version.rb +9 -0
- data/lib/expressir/commands/xsd.rb +23 -0
- data/lib/expressir/commands.rb +30 -0
- data/lib/expressir/config.rb +60 -0
- data/lib/expressir/coverage.rb +510 -0
- data/lib/expressir/eengine/arm_compare_report.rb +23 -0
- data/lib/expressir/eengine/changes_section.rb +16 -0
- data/lib/expressir/eengine/compare_report.rb +69 -0
- data/lib/expressir/eengine/mim_compare_report.rb +23 -0
- data/lib/expressir/eengine/modified_object.rb +21 -0
- data/lib/expressir/eengine.rb +9 -0
- data/lib/expressir/errors.rb +113 -0
- data/lib/expressir/express/adoc_hyperlink_formatter.rb +30 -0
- data/lib/expressir/express/adoc_source_formatter.rb +12 -0
- data/lib/expressir/express/ast_key_converter.rb +114 -0
- data/lib/expressir/express/builder.rb +277 -0
- data/lib/expressir/express/builder_context.rb +22 -0
- data/lib/expressir/express/builder_registry.rb +397 -0
- data/lib/expressir/express/builders/attribute_decl_builder.rb +32 -0
- data/lib/expressir/express/builders/built_in_builder.rb +75 -0
- data/lib/expressir/express/builders/constant_builder.rb +108 -0
- data/lib/expressir/express/builders/declaration_builder.rb +20 -0
- data/lib/expressir/express/builders/derive_clause_builder.rb +14 -0
- data/lib/expressir/express/builders/derived_attr_builder.rb +28 -0
- data/lib/expressir/express/builders/domain_rule_builder.rb +21 -0
- data/lib/expressir/express/builders/entity_decl_builder.rb +116 -0
- data/lib/expressir/express/builders/explicit_attr_builder.rb +49 -0
- data/lib/expressir/express/builders/expression_builder.rb +416 -0
- data/lib/expressir/express/builders/function_decl_builder.rb +86 -0
- data/lib/expressir/express/builders/helpers.rb +148 -0
- data/lib/expressir/express/builders/interface_builder.rb +151 -0
- data/lib/expressir/express/builders/inverse_attr_builder.rb +43 -0
- data/lib/expressir/express/builders/inverse_attr_type_builder.rb +30 -0
- data/lib/expressir/express/builders/inverse_clause_builder.rb +14 -0
- data/lib/expressir/express/builders/literal_builder.rb +93 -0
- data/lib/expressir/express/builders/procedure_decl_builder.rb +82 -0
- data/lib/expressir/express/builders/qualifier_builder.rb +102 -0
- data/lib/expressir/express/builders/reference_builder.rb +18 -0
- data/lib/expressir/express/builders/rule_decl_builder.rb +97 -0
- data/lib/expressir/express/builders/schema_body_decl_builder.rb +18 -0
- data/lib/expressir/express/builders/schema_decl_builder.rb +56 -0
- data/lib/expressir/express/builders/schema_version_builder.rb +34 -0
- data/lib/expressir/express/builders/simple_id_builder.rb +17 -0
- data/lib/expressir/express/builders/statement_builder.rb +243 -0
- data/lib/expressir/express/builders/subtype_constraint_builder.rb +166 -0
- data/lib/expressir/express/builders/syntax_builder.rb +30 -0
- data/lib/expressir/express/builders/type_builder.rb +238 -0
- data/lib/expressir/express/builders/type_decl_builder.rb +26 -0
- data/lib/expressir/express/builders/unique_clause_builder.rb +20 -0
- data/lib/expressir/express/builders/unique_rule_builder.rb +41 -0
- data/lib/expressir/express/builders/where_clause_builder.rb +20 -0
- data/lib/expressir/express/builders.rb +55 -0
- data/lib/expressir/express/cache.rb +76 -0
- data/lib/expressir/express/checker.rb +812 -0
- data/lib/expressir/express/concatenator.rb +79 -0
- data/lib/expressir/express/core.rb +95 -0
- data/lib/expressir/express/error.rb +124 -0
- data/lib/expressir/express/formatter.rb +149 -0
- data/lib/expressir/express/formatters/data_types_formatter.rb +362 -0
- data/lib/expressir/express/formatters/declarations_formatter.rb +753 -0
- data/lib/expressir/express/formatters/expressions_formatter.rb +180 -0
- data/lib/expressir/express/formatters/literals_formatter.rb +55 -0
- data/lib/expressir/express/formatters/references_formatter.rb +53 -0
- data/lib/expressir/express/formatters/remark_formatter.rb +270 -0
- data/lib/expressir/express/formatters/remark_item_formatter.rb +28 -0
- data/lib/expressir/express/formatters/statements_formatter.rb +272 -0
- data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +54 -0
- data/lib/expressir/express/formatters.rb +22 -0
- data/lib/expressir/express/grammar/parser.rb +712 -0
- data/lib/expressir/express/grammar.rb +11 -0
- data/lib/expressir/express/hyperlink_formatter.rb +35 -0
- data/lib/expressir/express/interface_dot.rb +105 -0
- data/lib/expressir/express/lazy_repository.rb +211 -0
- data/lib/expressir/express/line_map.rb +48 -0
- data/lib/expressir/express/listing.rb +174 -0
- data/lib/expressir/express/model_traversal.rb +42 -0
- data/lib/expressir/express/model_visitor.rb +25 -0
- data/lib/expressir/express/node_position_index.rb +342 -0
- data/lib/expressir/express/parallel_files.rb +229 -0
- data/lib/expressir/express/parser.rb +523 -0
- data/lib/expressir/express/pretty_formatter.rb +586 -0
- data/lib/expressir/express/pretty_gate.rb +61 -0
- data/lib/expressir/express/refs_overlay.rb +47 -0
- data/lib/expressir/express/remark_attacher.rb +1098 -0
- data/lib/expressir/express/remark_overlay.rb +86 -0
- data/lib/expressir/express/remark_scanner.rb +245 -0
- data/lib/expressir/express/resolve_references_model_visitor.rb +29 -0
- data/lib/expressir/express/schema_block_scanner.rb +137 -0
- data/lib/expressir/express/schema_head_formatter.rb +22 -0
- data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
- data/lib/expressir/express/schema_source_formatter.rb +15 -0
- data/lib/expressir/express/scope_resolver.rb +223 -0
- data/lib/expressir/express/self_schema_reference.rb +126 -0
- data/lib/expressir/express/shtolo.rb +679 -0
- data/lib/expressir/express/smrl_xml.rb +125 -0
- data/lib/expressir/express/source_formatter.rb +15 -0
- data/lib/expressir/express/streaming_builder.rb +435 -0
- data/lib/expressir/express/xsd.rb +259 -0
- data/lib/expressir/express.rb +53 -0
- data/lib/expressir/liquid.rb +1 -0
- data/lib/expressir/manifest/resolver.rb +210 -0
- data/lib/expressir/manifest/validator.rb +192 -0
- data/lib/expressir/manifest.rb +6 -0
- data/lib/expressir/mapping/refpath.rb +857 -0
- data/lib/expressir/mapping.rb +253 -0
- data/lib/expressir/model/cache.rb +18 -0
- data/lib/expressir/model/concerns.rb +42 -0
- data/lib/expressir/model/data_types/aggregate.rb +26 -0
- data/lib/expressir/model/data_types/array.rb +25 -0
- data/lib/expressir/model/data_types/bag.rb +21 -0
- data/lib/expressir/model/data_types/binary.rb +19 -0
- data/lib/expressir/model/data_types/boolean.rb +15 -0
- data/lib/expressir/model/data_types/enumeration.rb +21 -0
- data/lib/expressir/model/data_types/enumeration_item.rb +24 -0
- data/lib/expressir/model/data_types/generic.rb +24 -0
- data/lib/expressir/model/data_types/generic_entity.rb +24 -0
- data/lib/expressir/model/data_types/integer.rb +15 -0
- data/lib/expressir/model/data_types/list.rb +23 -0
- data/lib/expressir/model/data_types/logical.rb +15 -0
- data/lib/expressir/model/data_types/number.rb +15 -0
- data/lib/expressir/model/data_types/real.rb +17 -0
- data/lib/expressir/model/data_types/select.rb +23 -0
- data/lib/expressir/model/data_types/set.rb +21 -0
- data/lib/expressir/model/data_types/string.rb +19 -0
- data/lib/expressir/model/data_types.rb +25 -0
- data/lib/expressir/model/declarations/attribute.rb +38 -0
- data/lib/expressir/model/declarations/constant.rb +28 -0
- data/lib/expressir/model/declarations/derived_attribute.rb +25 -0
- data/lib/expressir/model/declarations/entity.rb +62 -0
- data/lib/expressir/model/declarations/function.rb +59 -0
- data/lib/expressir/model/declarations/informal_proposition_rule.rb +25 -0
- data/lib/expressir/model/declarations/interface.rb +37 -0
- data/lib/expressir/model/declarations/interface_item.rb +21 -0
- data/lib/expressir/model/declarations/interfaced_item.rb +33 -0
- data/lib/expressir/model/declarations/inverse_attribute.rb +25 -0
- data/lib/expressir/model/declarations/parameter.rb +28 -0
- data/lib/expressir/model/declarations/procedure.rb +57 -0
- data/lib/expressir/model/declarations/remark_item.rb +21 -0
- data/lib/expressir/model/declarations/rule.rb +66 -0
- data/lib/expressir/model/declarations/schema.rb +215 -0
- data/lib/expressir/model/declarations/schema_version.rb +19 -0
- data/lib/expressir/model/declarations/schema_version_item.rb +19 -0
- data/lib/expressir/model/declarations/subtype_constraint.rb +32 -0
- data/lib/expressir/model/declarations/type.rb +45 -0
- data/lib/expressir/model/declarations/unique_rule.rb +26 -0
- data/lib/expressir/model/declarations/variable.rb +28 -0
- data/lib/expressir/model/declarations/where_rule.rb +26 -0
- data/lib/expressir/model/declarations.rb +31 -0
- data/lib/expressir/model/dependency_resolver.rb +268 -0
- data/lib/expressir/model/exp_file.rb +42 -0
- data/lib/expressir/model/expressions/aggregate_initializer.rb +18 -0
- data/lib/expressir/model/expressions/aggregate_initializer_item.rb +20 -0
- data/lib/expressir/model/expressions/binary_expression.rb +59 -0
- data/lib/expressir/model/expressions/entity_constructor.rb +20 -0
- data/lib/expressir/model/expressions/function_call.rb +20 -0
- data/lib/expressir/model/expressions/interval.rb +29 -0
- data/lib/expressir/model/expressions/query_expression.rb +31 -0
- data/lib/expressir/model/expressions/unary_expression.rb +25 -0
- data/lib/expressir/model/expressions.rb +18 -0
- data/lib/expressir/model/identifier.rb +26 -0
- data/lib/expressir/model/indexes/entity_index.rb +103 -0
- data/lib/expressir/model/indexes/item_graph.rb +185 -0
- data/lib/expressir/model/indexes/reference_index.rb +148 -0
- data/lib/expressir/model/indexes/type_index.rb +149 -0
- data/lib/expressir/model/indexes.rb +12 -0
- data/lib/expressir/model/interface_validator.rb +384 -0
- data/lib/expressir/model/literals/binary.rb +17 -0
- data/lib/expressir/model/literals/integer.rb +17 -0
- data/lib/expressir/model/literals/logical.rb +21 -0
- data/lib/expressir/model/literals/real.rb +17 -0
- data/lib/expressir/model/literals/string.rb +20 -0
- data/lib/expressir/model/literals.rb +13 -0
- data/lib/expressir/model/model_element.rb +377 -0
- data/lib/expressir/model/references/attribute_reference.rb +19 -0
- data/lib/expressir/model/references/group_reference.rb +19 -0
- data/lib/expressir/model/references/index_reference.rb +23 -0
- data/lib/expressir/model/references/simple_reference.rb +21 -0
- data/lib/expressir/model/references.rb +12 -0
- data/lib/expressir/model/remark_format.rb +17 -0
- data/lib/expressir/model/remark_info.rb +92 -0
- data/lib/expressir/model/remark_placement.rb +42 -0
- data/lib/expressir/model/repository.rb +483 -0
- data/lib/expressir/model/repository_validator.rb +293 -0
- data/lib/expressir/model/search_engine.rb +551 -0
- data/lib/expressir/model/statements/alias.rb +31 -0
- data/lib/expressir/model/statements/assignment.rb +23 -0
- data/lib/expressir/model/statements/case.rb +42 -0
- data/lib/expressir/model/statements/case_action.rb +20 -0
- data/lib/expressir/model/statements/compound.rb +21 -0
- data/lib/expressir/model/statements/escape.rb +18 -0
- data/lib/expressir/model/statements/if.rb +26 -0
- data/lib/expressir/model/statements/null.rb +18 -0
- data/lib/expressir/model/statements/procedure_call.rb +22 -0
- data/lib/expressir/model/statements/repeat.rb +40 -0
- data/lib/expressir/model/statements/return.rb +20 -0
- data/lib/expressir/model/statements/skip.rb +18 -0
- data/lib/expressir/model/statements.rb +20 -0
- data/lib/expressir/model/supertype_expressions/binary_supertype_expression.rb +25 -0
- data/lib/expressir/model/supertype_expressions/oneof_supertype_expression.rb +17 -0
- data/lib/expressir/model/supertype_expressions.rb +12 -0
- data/lib/expressir/model.rb +39 -0
- data/lib/expressir/package/builder.rb +231 -0
- data/lib/expressir/package/metadata.rb +79 -0
- data/lib/expressir/package/reader.rb +149 -0
- data/lib/expressir/package.rb +8 -0
- data/lib/expressir/schema_manifest.rb +159 -0
- data/lib/expressir/schema_manifest_entry.rb +15 -0
- data/lib/expressir/version.rb +8 -0
- data/lib/expressir.rb +94 -0
- data/lib/tasks/verify_remarks.rake +16 -0
- data/rakelib/native.rake +24 -0
- data/rakelib/oracle.rake +52 -0
- data/rakelib/verify_remarks.rake +16 -0
- metadata +692 -0
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Handles attaching remarks (comments) to model elements after parsing.
|
|
6
|
+
#
|
|
7
|
+
# Two collaborators sit behind the `attach` interface:
|
|
8
|
+
# - {ScopeResolver} answers "which scope contains line N?"
|
|
9
|
+
# - {NodePositionIndex} answers "which model node is nearest line N?"
|
|
10
|
+
#
|
|
11
|
+
# Remark scanning itself lives in {RemarkScanner}; line→byte lookup lives
|
|
12
|
+
# in {LineMap}. This class is the orchestrator: it walks the remarks, asks
|
|
13
|
+
# the collaborators for targets, and writes the remarks onto the model.
|
|
14
|
+
#
|
|
15
|
+
# NOTE: Post-processing remark attachment has inherent limitations for scope-based
|
|
16
|
+
# matching. Remarks with simple tags (like "WR1") inside scopes (TYPE, ENTITY, etc.)
|
|
17
|
+
# cannot be perfectly matched without parsing context. This implementation prioritizes:
|
|
18
|
+
# 1. Exact path matches (e.g., "schema.entity.WR1")
|
|
19
|
+
# 2. Proximity-based matching for simple tags
|
|
20
|
+
# 3. NOT creating spurious schema-level items for ambiguous tags
|
|
21
|
+
class RemarkAttacher
|
|
22
|
+
# Type-driven registry: maps each model class to its collection attributes.
|
|
23
|
+
# Shared with {NodePositionIndex} via that class's own copy of the table.
|
|
24
|
+
# Two declarations rather than a cross-reference so each module is loadable
|
|
25
|
+
# on its own without forcing the other to load.
|
|
26
|
+
COLLECTION_REGISTRY = NodePositionIndex::COLLECTION_REGISTRY
|
|
27
|
+
|
|
28
|
+
# Collections holding executable statements — the regions a body
|
|
29
|
+
# comment can belong to.
|
|
30
|
+
STATEMENT_REGIONS = %i[
|
|
31
|
+
statements else_statements action_statements otherwise_statements
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
# Expression and statement child attributes are declared on the model
|
|
35
|
+
# via `child_attributes :foo, :bar, ...`. See TODO.bugs/15.
|
|
36
|
+
EXPRESSION_CHILDREN = Model::ModelElement.child_attributes_registry
|
|
37
|
+
|
|
38
|
+
# `WHERE <label> :` clause headers; the captured label maps to the
|
|
39
|
+
# 1-based line number(s) it appears on.
|
|
40
|
+
WHERE_CLAUSE_PATTERN = /\A\s*WHERE\s+(\w+)\s*:/i
|
|
41
|
+
|
|
42
|
+
def initialize(source)
|
|
43
|
+
@source = source
|
|
44
|
+
@attached_spans = Set.new
|
|
45
|
+
@line_map = LineMap.new(source.b)
|
|
46
|
+
@model = nil
|
|
47
|
+
@scope_resolver = nil
|
|
48
|
+
@node_index = nil
|
|
49
|
+
@owner_map = nil
|
|
50
|
+
@active_scope_map = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def attach(model)
|
|
54
|
+
@model = model
|
|
55
|
+
remarks = RemarkScanner.new(@source).scan
|
|
56
|
+
|
|
57
|
+
@node_index = NodePositionIndex.new(model, @line_map)
|
|
58
|
+
@scope_resolver = ScopeResolver.new(
|
|
59
|
+
source: @source,
|
|
60
|
+
model: model,
|
|
61
|
+
nodes_with_positions: @node_index.nodes,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
attach_tagged_remarks(remarks)
|
|
65
|
+
attach_untagged_remarks(remarks)
|
|
66
|
+
|
|
67
|
+
model
|
|
68
|
+
ensure
|
|
69
|
+
# Free expensive data structures once attachment is over. On the
|
|
70
|
+
# raising path this also drops the memoized ownership map, which
|
|
71
|
+
# would otherwise outlive the node index it was derived from.
|
|
72
|
+
@source = nil
|
|
73
|
+
@scope_resolver = nil
|
|
74
|
+
@node_index = nil
|
|
75
|
+
@line_map = nil
|
|
76
|
+
@owner_map = nil
|
|
77
|
+
@active_scope_map = nil
|
|
78
|
+
@source_lines_for_where_clause = nil
|
|
79
|
+
@where_clause_line_index = nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# ----- Tagged remark attachment -----
|
|
85
|
+
|
|
86
|
+
def attach_tagged_remarks(remarks)
|
|
87
|
+
tagged = remarks.select(&:tag)
|
|
88
|
+
return if tagged.empty?
|
|
89
|
+
|
|
90
|
+
tagged.each do |remark|
|
|
91
|
+
next if @attached_spans.include?(remark.position)
|
|
92
|
+
|
|
93
|
+
tag = remark.tag
|
|
94
|
+
target = nil
|
|
95
|
+
|
|
96
|
+
containing_scope = @scope_resolver.containing_scope_for(remark.line)
|
|
97
|
+
|
|
98
|
+
# Check if this is an informal proposition tag (IP\d+)
|
|
99
|
+
if tag.match?(/^IP\d+$/)
|
|
100
|
+
scope = containing_scope || @scope_resolver.find_by_source_text(remark.line)
|
|
101
|
+
if scope && supports_informal_propositions?(scope)
|
|
102
|
+
target = create_or_find_informal_proposition(scope, tag)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Standard path-based lookup
|
|
107
|
+
if target.nil?
|
|
108
|
+
# Handle prefixed tags like wr:WR1, ip:IP1, ur:UR1
|
|
109
|
+
if tag.include?(":") && !tag.include?(".")
|
|
110
|
+
target = handle_prefixed_tag(tag, containing_scope, @model,
|
|
111
|
+
get_schema_ids(@model))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Strategy 1: Try exact path lookup
|
|
115
|
+
if target.nil?
|
|
116
|
+
target = find_by_exact_path(@model, tag)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Strategy 1b: For paths with dots, try with scope path prefix first
|
|
120
|
+
if target.nil? && tag.include?(".")
|
|
121
|
+
if containing_scope && function_rule_procedure?(containing_scope)
|
|
122
|
+
scope_path = build_scope_path(containing_scope)
|
|
123
|
+
if scope_path
|
|
124
|
+
target = find_by_exact_path(@model, "#{scope_path}.#{tag}")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if target.nil?
|
|
129
|
+
schema_ids = get_schema_ids(@model)
|
|
130
|
+
schema_ids.each do |schema_id|
|
|
131
|
+
target = find_by_exact_path(@model, "#{schema_id}.#{tag}")
|
|
132
|
+
break if target
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Strategy 2: For simple tags, find in containing scope first
|
|
138
|
+
if target.nil? && !tag.include?(".")
|
|
139
|
+
if containing_scope
|
|
140
|
+
target = find_node_in_scope(containing_scope, tag)
|
|
141
|
+
|
|
142
|
+
if target.nil? && supports_where_rules?(containing_scope)
|
|
143
|
+
target = find_target_in_where_clause(containing_scope, tag,
|
|
144
|
+
remark.line)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if target.nil? && !function_rule_procedure?(containing_scope)
|
|
148
|
+
schema_ids = get_schema_ids(@model)
|
|
149
|
+
schema_ids.each do |schema_id|
|
|
150
|
+
target = find_by_exact_path(@model, "#{schema_id}.#{tag}")
|
|
151
|
+
break if target
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
schema_ids = get_schema_ids(@model)
|
|
156
|
+
schema_ids.each do |schema_id|
|
|
157
|
+
target = find_by_exact_path(@model, "#{schema_id}.#{tag}")
|
|
158
|
+
break if target
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Strategy 3: Create implicit item for qualified paths only
|
|
164
|
+
if target.nil? && tag.include?(".")
|
|
165
|
+
if containing_scope && function_rule_procedure?(containing_scope)
|
|
166
|
+
scope_path = build_scope_path(containing_scope)
|
|
167
|
+
if scope_path
|
|
168
|
+
target = create_implicit_remark_item(@model, "#{scope_path}.#{tag}",
|
|
169
|
+
get_schema_ids(@model))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
if target.nil?
|
|
173
|
+
target = create_implicit_remark_item(@model, tag,
|
|
174
|
+
get_schema_ids(@model))
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Strategy 4: For simple tags at schema level, create implicit item
|
|
179
|
+
if target.nil? && !tag.include?(".")
|
|
180
|
+
schema_ids = get_schema_ids(@model)
|
|
181
|
+
if schema_ids.any?
|
|
182
|
+
target = create_implicit_remark_item_at_schema(@model, tag,
|
|
183
|
+
schema_ids.first)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if target
|
|
189
|
+
add_remark(target, remark.text, format: remark.format, tag: remark.tag,
|
|
190
|
+
source_offset: remark.position)
|
|
191
|
+
@attached_spans << remark.position
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# ----- Untagged remark attachment -----
|
|
197
|
+
|
|
198
|
+
def attach_untagged_remarks(remarks)
|
|
199
|
+
untagged = remarks.reject(&:tag)
|
|
200
|
+
return unless untagged.any?
|
|
201
|
+
|
|
202
|
+
untagged.each do |remark|
|
|
203
|
+
next if @attached_spans.include?(remark.position)
|
|
204
|
+
|
|
205
|
+
line_content = line_content_for(remark.line)
|
|
206
|
+
if end_scope_line?(line_content)
|
|
207
|
+
matched_node = @node_index.node_for_end_scope_at(remark.line, line_content)
|
|
208
|
+
if matched_node
|
|
209
|
+
add_remark(matched_node, remark.text, format: remark.format, tag: nil,
|
|
210
|
+
source_offset: remark.position)
|
|
211
|
+
@attached_spans << remark.position
|
|
212
|
+
next
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
target, placement, region = find_body_comment_target(remark)
|
|
217
|
+
if target
|
|
218
|
+
add_remark(target, remark.text, format: remark.format, tag: nil,
|
|
219
|
+
placement: placement, region: region)
|
|
220
|
+
@attached_spans << remark.position
|
|
221
|
+
next
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
matched_node = @node_index.nearest_node_to(remark.line)
|
|
225
|
+
if matched_node
|
|
226
|
+
add_remark(matched_node, remark.text, format: remark.format, tag: nil,
|
|
227
|
+
source_offset: remark.position)
|
|
228
|
+
@attached_spans << remark.position
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def end_scope_line?(line_content)
|
|
234
|
+
line_content =~ /END_(SCHEMA|ENTITY|TYPE|FUNCTION|PROCEDURE|RULE)/i
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Own-line body comments belong to the next statement in the same
|
|
238
|
+
# statement region (Function body, THEN branch, ELSE branch, loop body):
|
|
239
|
+
# attached there with LEADING placement. A comment with no following
|
|
240
|
+
# statement in its region closes that region, so it attaches to the
|
|
241
|
+
# region's owner with TRAILING placement and the region's name — an IF
|
|
242
|
+
# owns two bodies that close at different keywords.
|
|
243
|
+
#
|
|
244
|
+
# Returns [nil, nil, nil] — "use the legacy fallback" — when the remark
|
|
245
|
+
# shares a line with a node or sits outside any statement-bearing node.
|
|
246
|
+
def find_body_comment_target(remark)
|
|
247
|
+
line = remark.line
|
|
248
|
+
# An own-line comment shares its line with no node. A node STARTING
|
|
249
|
+
# here means the remark is an inline tail (code; -- note). The
|
|
250
|
+
# end-line check is restricted to statements: container end_lines are
|
|
251
|
+
# child-derived approximations that can collide with comment lines.
|
|
252
|
+
if inline_remark?(remark)
|
|
253
|
+
target, placement, region = inline_target(remark,
|
|
254
|
+
@node_index.starting_at(line))
|
|
255
|
+
return [target, placement, region] if target
|
|
256
|
+
|
|
257
|
+
# A remark trailing a mid-construct keyword (`ELSE -- why`) starts
|
|
258
|
+
# nothing and closes nothing; handled before the legacy fallthrough,
|
|
259
|
+
# which would hand it to the enclosing construct unplaced — where
|
|
260
|
+
# nothing renders it.
|
|
261
|
+
mid = mid_keyword_inline_target(remark)
|
|
262
|
+
return mid if mid
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# A closing keyword on the next code line is decisive: the comment
|
|
266
|
+
# closes that body. Without this check the comment would instead be
|
|
267
|
+
# read as leading the next statement of an OUTER region, which is
|
|
268
|
+
# where it would wrongly render.
|
|
269
|
+
closing = closing_region_target(line)
|
|
270
|
+
return closing if closing.first
|
|
271
|
+
|
|
272
|
+
enclosing, region, = statement_region_for(line)
|
|
273
|
+
return [nil, nil, nil] unless region
|
|
274
|
+
|
|
275
|
+
following = region
|
|
276
|
+
.select { |n| n[:line] > line }
|
|
277
|
+
.min_by { |n| n[:position] }
|
|
278
|
+
if following
|
|
279
|
+
return [following[:node], Model::RemarkPlacement::LEADING, nil]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# No following statement and no closing keyword above: the comment is
|
|
283
|
+
# not demonstrably inside this body (it may sit after the whole
|
|
284
|
+
# declaration). Keep the legacy attachment rather than guessing.
|
|
285
|
+
[enclosing[:node], nil, nil]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Whether the remark trails code on its own line. Decided from the
|
|
289
|
+
# source text before it, not from node positions: container end lines
|
|
290
|
+
# are child-derived approximations that collide with comment lines and
|
|
291
|
+
# would misread an own-line comment as a trailing one.
|
|
292
|
+
def inline_remark?(remark)
|
|
293
|
+
content = line_content_for(remark.line).to_s
|
|
294
|
+
opener = content.index("--")
|
|
295
|
+
return false unless opener
|
|
296
|
+
|
|
297
|
+
!content[0...opener].strip.empty?
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# A comment trailing code on its line belongs to the statement it
|
|
301
|
+
# follows: `x := 1; -- why`, or the opener of a compound one, as in
|
|
302
|
+
# `IF x THEN -- why`.
|
|
303
|
+
#
|
|
304
|
+
# Multi-line statements were once excluded here, because appending a
|
|
305
|
+
# remark to one would carry it down to its closing keyword. They are
|
|
306
|
+
# admitted now that the formatter can put an opener remark back on the
|
|
307
|
+
# first line; the OPENER_REGION on the returned placement is what tells
|
|
308
|
+
# it to, and without that region the old appending behaviour stands.
|
|
309
|
+
def inline_target(remark, nodes)
|
|
310
|
+
started_here = nodes.select do |n|
|
|
311
|
+
n[:node].is_a?(Model::TakesInlineRemark) &&
|
|
312
|
+
n[:line] == remark.line &&
|
|
313
|
+
n[:position] && n[:position] < remark.position
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Of the two that began here, whichever starts later is the one the
|
|
317
|
+
# remark follows: `x := 0; IF n > 0 THEN -- why` trails the IF.
|
|
318
|
+
began = [single_line_owner(started_here, remark.line),
|
|
319
|
+
opener_owner(started_here, remark.line)]
|
|
320
|
+
.compact.max_by { |n| n[:position] }
|
|
321
|
+
|
|
322
|
+
owner = began || closing_owner(nodes, remark)
|
|
323
|
+
return [nil, nil, nil] unless owner
|
|
324
|
+
|
|
325
|
+
[owner[:node], Model::RemarkPlacement::INLINE,
|
|
326
|
+
opener_region(owner, remark.line)]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# A node beginning and ending on the remark's line. Ranked by where it
|
|
330
|
+
# ends, so of several sharing the line the outermost complete one wins:
|
|
331
|
+
# `CASE n OF 1 : x := 2; END_CASE; -- why` trails the CASE, not its
|
|
332
|
+
# action.
|
|
333
|
+
def single_line_owner(candidates, line)
|
|
334
|
+
candidates
|
|
335
|
+
.select { |n| n[:end_line] == line }
|
|
336
|
+
.max_by { |n| n[:position] + n[:node].source.to_s.length }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# A node whose opener is on the remark's line but which continues past
|
|
340
|
+
# it. Ranked by where it STARTS, not where it ends: such a node's source
|
|
341
|
+
# spans its whole body, so ranking by end would let an enclosing block
|
|
342
|
+
# outrank the one the remark actually follows.
|
|
343
|
+
def opener_owner(candidates, line)
|
|
344
|
+
candidates
|
|
345
|
+
.select { |n| n[:end_line] && n[:end_line] > line }
|
|
346
|
+
.max_by { |n| n[:position] }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Nothing began on the remark's line, so it may still be closing one
|
|
350
|
+
# that began earlier:
|
|
351
|
+
#
|
|
352
|
+
# REFERENCE FROM x
|
|
353
|
+
# (a, b); -- why
|
|
354
|
+
#
|
|
355
|
+
# Only reached as a fallback. Were it folded into the main lookup, an
|
|
356
|
+
# enclosing block would become a candidate for every remark inside it,
|
|
357
|
+
# since a node's end line is derived from its children.
|
|
358
|
+
def closing_owner(nodes, remark)
|
|
359
|
+
nodes
|
|
360
|
+
.select do |n|
|
|
361
|
+
n[:node].is_a?(Model::TakesInlineRemark) &&
|
|
362
|
+
n[:end_line] == remark.line && n[:line] &&
|
|
363
|
+
n[:line] < remark.line &&
|
|
364
|
+
n[:position] && n[:position] < remark.position
|
|
365
|
+
end
|
|
366
|
+
.max_by { |n| n[:position] }
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# A remark trailing a node that closes on its line follows the whole
|
|
370
|
+
# node, so it is written back the way an ordinary inline remark is. One
|
|
371
|
+
# that continues past the line trailed the node's opener instead, and
|
|
372
|
+
# belongs back there rather than after the closing keyword.
|
|
373
|
+
def opener_region(owner, line)
|
|
374
|
+
return nil unless owner[:end_line] && owner[:end_line] > line
|
|
375
|
+
|
|
376
|
+
Model::RemarkPlacement::OPENER_REGION
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Lines beginning with a mid-construct keyword. The keyword closes
|
|
380
|
+
# nothing and opens nothing, but the remark trailing it belongs to the
|
|
381
|
+
# construct it branches — written back after the keyword, not after the
|
|
382
|
+
# construct's END_.
|
|
383
|
+
MID_KEYWORD_REGIONS = {
|
|
384
|
+
/\AELSE\b/i => [Model::Statements::If,
|
|
385
|
+
Model::RemarkPlacement::ELSE_REGION],
|
|
386
|
+
/\AOTHERWISE\b/i => [Model::Statements::Case,
|
|
387
|
+
Model::RemarkPlacement::OTHERWISE_REGION],
|
|
388
|
+
}.freeze
|
|
389
|
+
|
|
390
|
+
# Attach `ELSE -- why` / `OTHERWISE : -- why` to the innermost
|
|
391
|
+
# enclosing IF / CASE with INLINE placement and the keyword's region.
|
|
392
|
+
# Returns [nil, nil, nil] when the line head is no mid-construct
|
|
393
|
+
# keyword or no construct of the right kind spans the line.
|
|
394
|
+
def mid_keyword_inline_target(remark)
|
|
395
|
+
content = line_content_for(remark.line).to_s
|
|
396
|
+
opener = content.index("--")
|
|
397
|
+
return [nil, nil, nil] unless opener
|
|
398
|
+
|
|
399
|
+
owner = MID_KEYWORD_REGIONS.find { |pattern, _| content[0...opener].strip.match?(pattern) }
|
|
400
|
+
return [nil, nil, nil] unless owner
|
|
401
|
+
|
|
402
|
+
klass, region = owner[1]
|
|
403
|
+
innermost = @node_index.nodes.select do |n|
|
|
404
|
+
n[:node].is_a?(klass) && n[:line] && n[:end_line] &&
|
|
405
|
+
n[:line] < remark.line && n[:end_line] > remark.line
|
|
406
|
+
end.max_by { |n| n[:line] }
|
|
407
|
+
return [nil, nil, nil] unless innermost
|
|
408
|
+
|
|
409
|
+
[innermost[:node], Model::RemarkPlacement::INLINE, region]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Which closing keyword ends which region of which owner. A comment
|
|
413
|
+
# sitting between a body's last statement and one of these keywords
|
|
414
|
+
# closes that body.
|
|
415
|
+
CLOSING_KEYWORDS = {
|
|
416
|
+
/\AELSE\b/i => [Model::Statements::If, :statements],
|
|
417
|
+
/\AEND_IF\b/i => [Model::Statements::If, :else_statements],
|
|
418
|
+
/\AOTHERWISE\b/i => [Model::Statements::Case, :action_statements],
|
|
419
|
+
/\AEND_CASE\b/i => [Model::Statements::Case, :otherwise_statements],
|
|
420
|
+
/\AEND_REPEAT\b/i => [Model::Statements::Repeat, :statements],
|
|
421
|
+
/\AEND_ALIAS\b/i => [Model::Statements::Alias, :statements],
|
|
422
|
+
/\AEND\s*;/i => [Model::Statements::Compound, :statements],
|
|
423
|
+
# A RULE's executable body ends at WHERE, not at END_RULE.
|
|
424
|
+
/\AWHERE\b/i => [Model::Declarations::Rule, :statements],
|
|
425
|
+
/\AEND_FUNCTION\b/i => [Model::Declarations::Function, :statements],
|
|
426
|
+
/\AEND_PROCEDURE\b/i => [Model::Declarations::Procedure, :statements],
|
|
427
|
+
# END_RULE closes the WHERE section when the rule has one.
|
|
428
|
+
/\AEND_RULE\b/i => [Model::Declarations::Rule, :where_rules],
|
|
429
|
+
}.freeze
|
|
430
|
+
|
|
431
|
+
# Regions whose owner may not have that body, in which case the
|
|
432
|
+
# keyword closes the earlier region instead.
|
|
433
|
+
REGION_FALLBACKS = {
|
|
434
|
+
[Model::Statements::If, :else_statements] =>
|
|
435
|
+
[:statements, ->(n) { n.else_statements&.length&.positive? }],
|
|
436
|
+
[Model::Statements::Case, :otherwise_statements] =>
|
|
437
|
+
[:action_statements, ->(n) { !n.otherwise_statement.nil? }],
|
|
438
|
+
[Model::Declarations::Rule, :where_rules] =>
|
|
439
|
+
[:statements, ->(n) { n.where_rules&.length&.positive? }],
|
|
440
|
+
}.freeze
|
|
441
|
+
|
|
442
|
+
# A node's indexed span stops at its last child, so a comment written
|
|
443
|
+
# after that child but before the node's closing keyword sits outside
|
|
444
|
+
# every span and never reaches statement_region_for. Resolve it from
|
|
445
|
+
# the keyword that follows: it names both the owner type and the body
|
|
446
|
+
# being closed.
|
|
447
|
+
def closing_region_target(line)
|
|
448
|
+
keyword_owner, region, keyword_line = closing_keyword_after(line)
|
|
449
|
+
return [nil, nil, nil] unless keyword_owner
|
|
450
|
+
|
|
451
|
+
# The owner is the construct the keyword actually closes — the
|
|
452
|
+
# innermost one still open at that line. Picking the latest node of
|
|
453
|
+
# the right class instead would grab an already-closed inner block
|
|
454
|
+
# (nested IFs) or an unrelated earlier declaration (a RULE, when the
|
|
455
|
+
# WHERE really belongs to an ENTITY).
|
|
456
|
+
opener_line = active_opener_line(keyword_line, keyword_owner)
|
|
457
|
+
return [nil, nil, nil] unless opener_line
|
|
458
|
+
|
|
459
|
+
owner = @node_index.starting_at(opener_line).find do |n|
|
|
460
|
+
n[:node].is_a?(keyword_owner)
|
|
461
|
+
end
|
|
462
|
+
return [nil, nil, nil] unless owner
|
|
463
|
+
|
|
464
|
+
# END_IF closes the THEN body when there is no ELSE; END_CASE closes
|
|
465
|
+
# the last action when there is no OTHERWISE.
|
|
466
|
+
fallback, present = REGION_FALLBACKS[[keyword_owner, region]]
|
|
467
|
+
region = fallback if fallback && !present.call(owner[:node])
|
|
468
|
+
|
|
469
|
+
[owner[:node], Model::RemarkPlacement::TRAILING, region.to_s]
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# The first non-blank, non-comment source line after `line`.
|
|
473
|
+
# Source keywords that open a nestable construct, paired with the class
|
|
474
|
+
# of node they produce. Used to find which construct a closing keyword
|
|
475
|
+
# actually belongs to.
|
|
476
|
+
OPENERS = [
|
|
477
|
+
[/\bIF\b.*?\bTHEN\b/i, Model::Statements::If],
|
|
478
|
+
[/\bCASE\b.*?\bOF\b/i, Model::Statements::Case],
|
|
479
|
+
[/\bREPEAT\b/i, Model::Statements::Repeat],
|
|
480
|
+
[/\bALIAS\b/i, Model::Statements::Alias],
|
|
481
|
+
[/\bBEGIN\b/i, Model::Statements::Compound],
|
|
482
|
+
[/\A\s*FUNCTION\b/i, Model::Declarations::Function],
|
|
483
|
+
[/\A\s*PROCEDURE\b/i, Model::Declarations::Procedure],
|
|
484
|
+
[/\A\s*RULE\b/i, Model::Declarations::Rule],
|
|
485
|
+
[/\A\s*ENTITY\b/i, :other],
|
|
486
|
+
[/\A\s*TYPE\b/i, :other],
|
|
487
|
+
].freeze
|
|
488
|
+
|
|
489
|
+
CLOSERS = /\bEND_IF\b|\bEND_CASE\b|\bEND_REPEAT\b|\bEND_ALIAS\b|\bEND_FUNCTION\b|\bEND_PROCEDURE\b|\bEND_RULE\b|\bEND_ENTITY\b|\bEND_TYPE\b|\bEND\s*;/i
|
|
490
|
+
|
|
491
|
+
# Strips what must not be scanned for keywords: string literals and a
|
|
492
|
+
# trailing `--` remark. Without this, `x := 'IF a THEN'` or a comment
|
|
493
|
+
# mentioning REPEAT would push a construct that never opened.
|
|
494
|
+
def keyword_scannable(content)
|
|
495
|
+
without_strings = content.gsub(/'[^']*'/, "''")
|
|
496
|
+
tail = without_strings.index("--")
|
|
497
|
+
tail ? without_strings[0...tail] : without_strings
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# The opening line of the innermost construct still open at
|
|
501
|
+
# `keyword_line`, or nil when that construct is not of `expected_class`.
|
|
502
|
+
def active_opener_line(keyword_line, expected_class)
|
|
503
|
+
active = active_scope_map[keyword_line]
|
|
504
|
+
return nil unless active && active[0] == expected_class
|
|
505
|
+
|
|
506
|
+
active[1]
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Line number => the construct open at the START of that line, as
|
|
510
|
+
# [class, opening_line]. Built once per source: rescanning from line 1
|
|
511
|
+
# for every trailing comment is quadratic, and on a comment-dense file
|
|
512
|
+
# that cost dominates parsing entirely.
|
|
513
|
+
def active_scope_map
|
|
514
|
+
@active_scope_map ||= build_active_scope_map
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def build_active_scope_map
|
|
518
|
+
map = {}
|
|
519
|
+
stack = []
|
|
520
|
+
(1..source_line_count).each do |ln|
|
|
521
|
+
map[ln] = stack.last
|
|
522
|
+
content = keyword_scannable(line_content_for(ln).to_s.strip)
|
|
523
|
+
next if content.empty? || content.start_with?("--")
|
|
524
|
+
|
|
525
|
+
line_events(content).each do |_offset, kind, klass|
|
|
526
|
+
kind == :open ? stack << [klass, ln] : stack.pop
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
map
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Opener/closer events on one line, ordered by where they appear.
|
|
533
|
+
# EVERY occurrence is collected, not just the first: a line holding two
|
|
534
|
+
# complete IF blocks contributes two openers and two closers, and
|
|
535
|
+
# recording only one opener would over-pop the enclosing construct.
|
|
536
|
+
def line_events(content)
|
|
537
|
+
events = []
|
|
538
|
+
OPENERS.each do |pattern, klass|
|
|
539
|
+
content.enum_for(:scan, pattern).each do
|
|
540
|
+
events << [Regexp.last_match.begin(0), :open, klass]
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
content.enum_for(:scan, CLOSERS).each do
|
|
544
|
+
events << [Regexp.last_match.begin(0), :close, nil]
|
|
545
|
+
end
|
|
546
|
+
events.sort_by(&:first)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def closing_keyword_after(line)
|
|
550
|
+
probe = line + 1
|
|
551
|
+
# Skip further comment lines AND blank lines: a comment separated
|
|
552
|
+
# from its closing keyword by an empty line still closes that body.
|
|
553
|
+
while probe <= source_line_count
|
|
554
|
+
content = line_content_for(probe).to_s.strip
|
|
555
|
+
break unless content.empty? || content.start_with?("--")
|
|
556
|
+
|
|
557
|
+
probe += 1
|
|
558
|
+
end
|
|
559
|
+
content = line_content_for(probe).to_s.strip
|
|
560
|
+
CLOSING_KEYWORDS.each do |pattern, owner_region|
|
|
561
|
+
return [*owner_region, probe] if content.match?(pattern)
|
|
562
|
+
end
|
|
563
|
+
[nil, nil, nil]
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def statement_region_for(line, nodes = nil)
|
|
567
|
+
candidates = (nodes || @node_index.spanning(line)).select do |n|
|
|
568
|
+
n[:line] && n[:end_line] && n[:line] <= line && n[:end_line] >= line &&
|
|
569
|
+
(n[:node].is_a?(Model::Statement) || function_rule_procedure?(n[:node]))
|
|
570
|
+
end
|
|
571
|
+
enclosing = innermost_candidate(candidates)
|
|
572
|
+
return [nil, nil, nil] unless enclosing
|
|
573
|
+
|
|
574
|
+
children = @node_index.children_in(enclosing[:node], STATEMENT_REGIONS)
|
|
575
|
+
return [enclosing, nil, nil] if children.empty?
|
|
576
|
+
|
|
577
|
+
preceding = children.select { |n| n[:line] < line }.max_by { |n| n[:position] }
|
|
578
|
+
following = children.select { |n| n[:line] > line }.min_by { |n| n[:position] }
|
|
579
|
+
region_attr = region_attr_for(line, preceding, following)
|
|
580
|
+
return [enclosing, nil, nil] unless region_attr
|
|
581
|
+
|
|
582
|
+
[enclosing, children.select { |n| n[:collection] == region_attr }, region_attr]
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Node end lines are child-derived approximations, so a parent's span
|
|
586
|
+
# can come out SMALLER than a child's and span size alone picks the
|
|
587
|
+
# wrong container. Ownership links are exact: drop every candidate
|
|
588
|
+
# that is an ancestor of another candidate, then pick the smallest
|
|
589
|
+
# span among the true leaves.
|
|
590
|
+
def innermost_candidate(candidates)
|
|
591
|
+
return candidates.first if candidates.length <= 1
|
|
592
|
+
|
|
593
|
+
owner_of = owner_map
|
|
594
|
+
ancestors = Set.new.compare_by_identity
|
|
595
|
+
candidates.each do |cand|
|
|
596
|
+
current = owner_of[cand[:node]]
|
|
597
|
+
while current
|
|
598
|
+
ancestors << current
|
|
599
|
+
current = owner_of[current]
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
leaves = candidates.reject { |n| ancestors.include?(n[:node]) }
|
|
604
|
+
(leaves.empty? ? candidates : leaves).min_by { |n| n[:end_line] - n[:line] }
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# The node index is immutable during attachment, so its ownership map
|
|
608
|
+
# only needs to be built once for all body remarks.
|
|
609
|
+
def owner_map
|
|
610
|
+
# Identity comparison must be enabled BEFORE the hash is populated.
|
|
611
|
+
# Model elements compare by value, so two distinct-but-equal nodes
|
|
612
|
+
# would collapse into one entry during a plain build, and switching
|
|
613
|
+
# to identity afterwards cannot recover the lost entry.
|
|
614
|
+
@owner_map ||= @node_index.nodes.each_with_object(
|
|
615
|
+
{}.compare_by_identity,
|
|
616
|
+
) { |n, map| map[n[:node]] = n[:owner] }
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# The keyword that opens each region, for regions that follow another
|
|
620
|
+
# region of the same owner. A comment in the gap belongs to whichever
|
|
621
|
+
# side of this keyword it was written on.
|
|
622
|
+
REGION_OPENERS = {
|
|
623
|
+
else_statements: /\A(?:.*;)?\s*ELSE(?:\s*--.*)?\z/i,
|
|
624
|
+
otherwise_statements: /\A\s*OTHERWISE\b/i,
|
|
625
|
+
}.freeze
|
|
626
|
+
|
|
627
|
+
# A comment between two regions of the same owner — between the THEN
|
|
628
|
+
# body and ELSE, or between the last CASE action and OTHERWISE — sits
|
|
629
|
+
# on one side of the keyword that opens the second region. The gap can
|
|
630
|
+
# hold only that keyword and comments, so scanning it is exact.
|
|
631
|
+
# Comment lines are skipped so prose mentioning the keyword cannot
|
|
632
|
+
# match.
|
|
633
|
+
def region_attr_for(line, preceding, following)
|
|
634
|
+
return following&.dig(:collection) unless preceding
|
|
635
|
+
|
|
636
|
+
following_attr = following&.dig(:collection)
|
|
637
|
+
opener = REGION_OPENERS[following_attr]
|
|
638
|
+
if opener && following_attr != preceding[:collection]
|
|
639
|
+
opener_line = (preceding[:end_line]...following[:line]).find do |ln|
|
|
640
|
+
content = line_content_for(ln).strip
|
|
641
|
+
!content.start_with?("--") && opener.match?(content)
|
|
642
|
+
end
|
|
643
|
+
return following_attr if opener_line && line > opener_line
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
preceding[:collection]
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# ----- Tag resolution (within a scope) -----
|
|
650
|
+
|
|
651
|
+
def find_node_in_scope(scope, tag)
|
|
652
|
+
return nil unless scope
|
|
653
|
+
|
|
654
|
+
collections_on(scope).each do |collection|
|
|
655
|
+
collection.each do |item|
|
|
656
|
+
next unless item.is_a?(Model::HasRemarks)
|
|
657
|
+
return item if item.id == tag
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
types = get_collection(scope, :types)
|
|
662
|
+
types&.each do |type|
|
|
663
|
+
result = find_enumeration_item_in_type(type, tag)
|
|
664
|
+
return result if result
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
statements = get_collection(scope, :statements)
|
|
668
|
+
statements&.each do |stmt|
|
|
669
|
+
result = find_node_in_statement(stmt, tag)
|
|
670
|
+
return result if result
|
|
671
|
+
|
|
672
|
+
result = find_query_in_expression(stmt, tag)
|
|
673
|
+
return result if result
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
nil
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def find_enumeration_item_in_type(type, tag)
|
|
680
|
+
return nil unless type
|
|
681
|
+
return nil unless type.is_a?(Model::Declarations::Type)
|
|
682
|
+
|
|
683
|
+
type.enumeration_items&.each do |item|
|
|
684
|
+
return item if item.id == tag
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
ut = type.underlying_type
|
|
688
|
+
return nil unless ut.is_a?(Model::DataTypes::Enumeration) && ut.items
|
|
689
|
+
|
|
690
|
+
ut.items.each { |item| return item if item.id == tag }
|
|
691
|
+
nil
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def find_query_in_expression(node, tag, visited = Set.new)
|
|
695
|
+
return nil unless node
|
|
696
|
+
return nil unless node.is_a?(Model::ModelElement)
|
|
697
|
+
return nil if visited.include?(node.object_id)
|
|
698
|
+
|
|
699
|
+
visited.add(node.object_id)
|
|
700
|
+
|
|
701
|
+
return node if node.is_a?(Model::Expressions::QueryExpression) && node.id == tag
|
|
702
|
+
|
|
703
|
+
attrs = EXPRESSION_CHILDREN[node.class]
|
|
704
|
+
return nil unless attrs
|
|
705
|
+
|
|
706
|
+
attrs.each do |attr|
|
|
707
|
+
value = node.public_send(attr)
|
|
708
|
+
case value
|
|
709
|
+
when Array
|
|
710
|
+
value.each do |val|
|
|
711
|
+
next unless val.is_a?(Model::ModelElement)
|
|
712
|
+
|
|
713
|
+
result = find_query_in_expression(val, tag, visited)
|
|
714
|
+
return result if result
|
|
715
|
+
end
|
|
716
|
+
when Model::ModelElement
|
|
717
|
+
result = find_query_in_expression(value, tag, visited)
|
|
718
|
+
return result if result
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
nil
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def function_rule_procedure?(node)
|
|
726
|
+
return false unless node
|
|
727
|
+
|
|
728
|
+
node.is_a?(Model::Declarations::Function) ||
|
|
729
|
+
node.is_a?(Model::Declarations::Rule) ||
|
|
730
|
+
node.is_a?(Model::Declarations::Procedure)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Handle prefixed tags like wr:WR1, ip:IP1, ur:UR1
|
|
734
|
+
def handle_prefixed_tag(tag, containing_scope, model, schema_ids)
|
|
735
|
+
prefix, id = tag.split(":")
|
|
736
|
+
return nil unless id
|
|
737
|
+
|
|
738
|
+
collection_attr = case prefix.downcase
|
|
739
|
+
when "wr" then :where_rules
|
|
740
|
+
when "ip" then :informal_propositions
|
|
741
|
+
when "ur" then :unique_rules
|
|
742
|
+
end
|
|
743
|
+
return nil unless collection_attr
|
|
744
|
+
|
|
745
|
+
collection = get_collection(containing_scope, collection_attr)
|
|
746
|
+
if collection
|
|
747
|
+
found = collection.find { |item| item.is_a?(Model::ModelElement) && item.id == id }
|
|
748
|
+
return found if found
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
schema_ids.each do |schema_id|
|
|
752
|
+
full_path = "#{schema_id}.#{tag.tr(':', '.')}"
|
|
753
|
+
found = safe_find(model, full_path)
|
|
754
|
+
return found if found
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
nil
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# Find target for remarks inside WHERE clauses by scanning source lines
|
|
761
|
+
# for `WHERE <id>:` patterns. Lives here (not in ScopeResolver) because
|
|
762
|
+
# it's about WHERE-rule membership, not scope membership.
|
|
763
|
+
def find_target_in_where_clause(scope, tag, remark_line)
|
|
764
|
+
return nil unless supports_where_rules?(scope)
|
|
765
|
+
|
|
766
|
+
where_rules = get_collection(scope, :where_rules)
|
|
767
|
+
return nil unless where_rules&.any?
|
|
768
|
+
|
|
769
|
+
where_clause_lines = where_clause_line_index
|
|
770
|
+
|
|
771
|
+
where_rules.each do |wr|
|
|
772
|
+
next unless wr.id
|
|
773
|
+
|
|
774
|
+
where_clause_lines.fetch(wr.id, []).each do |line_num|
|
|
775
|
+
next unless line_num < remark_line
|
|
776
|
+
|
|
777
|
+
return create_remark_item(wr, tag) if remark_line.between?(line_num, line_num + 5)
|
|
778
|
+
end
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
nil
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Single scan over the source lines mapping each `WHERE <id>:` label to
|
|
785
|
+
# its 1-based line number, so per-remark lookups stop re-testing every
|
|
786
|
+
# line against every WHERE rule's regex.
|
|
787
|
+
def where_clause_line_index
|
|
788
|
+
@where_clause_line_index ||= begin
|
|
789
|
+
index = Hash.new { |h, k| h[k] = [] }
|
|
790
|
+
source_lines_for_where_clause.each_with_index do |line, idx|
|
|
791
|
+
if (match = line.match(WHERE_CLAUSE_PATTERN))
|
|
792
|
+
index[match[1]] << (idx + 1)
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
index
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def source_lines_for_where_clause
|
|
800
|
+
# @source is set for the duration of `attach`; freed at the end.
|
|
801
|
+
@source_lines_for_where_clause ||= @source.lines
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def find_node_in_statement(stmt, tag)
|
|
805
|
+
case stmt
|
|
806
|
+
when Model::Statements::Alias
|
|
807
|
+
return stmt if stmt.id == tag
|
|
808
|
+
when Model::Statements::Repeat
|
|
809
|
+
return stmt if stmt.id == tag
|
|
810
|
+
when Model::Expressions::QueryExpression
|
|
811
|
+
return stmt if stmt.id == tag
|
|
812
|
+
end
|
|
813
|
+
nil
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def build_scope_path(node)
|
|
817
|
+
return nil unless node
|
|
818
|
+
|
|
819
|
+
parts = []
|
|
820
|
+
current = node
|
|
821
|
+
|
|
822
|
+
while current
|
|
823
|
+
if current.is_a?(Model::ModelElement) && current.id
|
|
824
|
+
parts.unshift(current.id)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
break if current.is_a?(Model::Declarations::Schema)
|
|
828
|
+
|
|
829
|
+
current = current.parent
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
parts.empty? ? nil : parts.join(".")
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# ----- Path-based lookup -----
|
|
836
|
+
|
|
837
|
+
def find_by_exact_path(model, path)
|
|
838
|
+
return nil unless path
|
|
839
|
+
return nil unless repository?(model) || exp_file?(model)
|
|
840
|
+
|
|
841
|
+
result = safe_find(model, path)
|
|
842
|
+
return result if result
|
|
843
|
+
|
|
844
|
+
normalized = path.tr(":", ".")
|
|
845
|
+
normalized == path ? nil : safe_find(model, normalized)
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# ----- Target creation -----
|
|
849
|
+
|
|
850
|
+
def create_implicit_remark_item_at_schema(model, item_id, schema_id)
|
|
851
|
+
return nil unless repository?(model) || exp_file?(model)
|
|
852
|
+
|
|
853
|
+
schema = safe_find(model, schema_id)
|
|
854
|
+
return nil unless schema.is_a?(Model::Declarations::Schema)
|
|
855
|
+
|
|
856
|
+
# Strip "wr:" prefix convention (e.g. "wr:IP1" → "IP1")
|
|
857
|
+
ip_id = extract_ip_id(item_id)
|
|
858
|
+
if ip_id && supports_informal_propositions?(schema)
|
|
859
|
+
return create_or_find_informal_proposition(schema, ip_id)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
return nil unless supports_remark_items?(schema)
|
|
863
|
+
|
|
864
|
+
existing = schema.remark_items&.find { |ri| ri.id == item_id }
|
|
865
|
+
return existing if existing
|
|
866
|
+
|
|
867
|
+
create_remark_item(schema, item_id)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def create_implicit_remark_item(model, path, schema_ids = [])
|
|
871
|
+
return nil unless repository?(model) || exp_file?(model)
|
|
872
|
+
|
|
873
|
+
# Handle "wr:IP1" convention: extract parent path and IP id directly
|
|
874
|
+
# so normalize_path doesn't split "wr:IP1" into "wr"."IP1" segments.
|
|
875
|
+
ip_prefix_match = path.match(/\A(.+)\.(\w+):(IP\d+)\z/)
|
|
876
|
+
if ip_prefix_match
|
|
877
|
+
parent_path = ip_prefix_match[1]
|
|
878
|
+
ip_id = ip_prefix_match[3]
|
|
879
|
+
parent = safe_find(model, parent_path)
|
|
880
|
+
if parent.nil? && schema_ids.any?
|
|
881
|
+
schema_ids.each do |schema_id|
|
|
882
|
+
parent = safe_find(model, "#{schema_id}.#{parent_path}")
|
|
883
|
+
break if parent
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
if parent && supports_informal_propositions?(parent)
|
|
888
|
+
return create_or_find_informal_proposition(parent, ip_id)
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
clean_path = normalize_path(path)
|
|
893
|
+
parts = clean_path.split(".")
|
|
894
|
+
return nil if parts.length < 2
|
|
895
|
+
|
|
896
|
+
(parts.length - 1).downto(1) do |i|
|
|
897
|
+
parent_path = parts[0...i].join(".")
|
|
898
|
+
item_id = parts[i]
|
|
899
|
+
|
|
900
|
+
parent = safe_find(model, parent_path)
|
|
901
|
+
|
|
902
|
+
if parent.nil? && schema_ids.any?
|
|
903
|
+
schema_ids.each do |schema_id|
|
|
904
|
+
parent = safe_find(model, "#{schema_id}.#{parent_path}")
|
|
905
|
+
break if parent
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
next unless parent
|
|
910
|
+
|
|
911
|
+
return create_item_at_parent(parent, item_id)
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
nil
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def normalize_path(path)
|
|
918
|
+
return path unless path.include?(":")
|
|
919
|
+
|
|
920
|
+
path_part, _, item_suffix = path.rpartition(":")
|
|
921
|
+
"#{path_part}.#{item_suffix}"
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def create_item_at_parent(parent, item_id)
|
|
925
|
+
# Strip "wr:" prefix convention (e.g. "wr:IP1" → "IP1")
|
|
926
|
+
ip_id = extract_ip_id(item_id)
|
|
927
|
+
if ip_id && supports_informal_propositions?(parent)
|
|
928
|
+
return create_or_find_informal_proposition(parent, ip_id)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
return nil unless supports_remark_items?(parent)
|
|
932
|
+
|
|
933
|
+
existing = parent.remark_items&.find { |ri| ri.id == item_id }
|
|
934
|
+
return existing if existing
|
|
935
|
+
|
|
936
|
+
create_remark_item(parent, item_id)
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
def create_or_find_informal_proposition(parent, id)
|
|
940
|
+
return nil unless supports_informal_propositions?(parent)
|
|
941
|
+
|
|
942
|
+
existing = parent.informal_propositions&.find { |ip| ip.id == id }
|
|
943
|
+
return existing if existing
|
|
944
|
+
|
|
945
|
+
ip = Model::Declarations::InformalPropositionRule.new(id: id)
|
|
946
|
+
ip.parent = parent
|
|
947
|
+
parent.informal_propositions ||= []
|
|
948
|
+
parent.informal_propositions << ip
|
|
949
|
+
safe_reset_children_by_id(parent)
|
|
950
|
+
|
|
951
|
+
remark_item = Model::Declarations::RemarkItem.new(id: id)
|
|
952
|
+
remark_item.parent = ip
|
|
953
|
+
ip.remark_items ||= []
|
|
954
|
+
ip.remark_items << remark_item
|
|
955
|
+
safe_reset_children_by_id(ip)
|
|
956
|
+
|
|
957
|
+
remark_item
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def create_remark_item(parent, id)
|
|
961
|
+
item = Model::Declarations::RemarkItem.new(id: id)
|
|
962
|
+
item.parent = parent
|
|
963
|
+
parent.remark_items ||= []
|
|
964
|
+
parent.remark_items << item
|
|
965
|
+
safe_reset_children_by_id(parent)
|
|
966
|
+
item
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# ----- Remark storage -----
|
|
970
|
+
|
|
971
|
+
def add_remark(node, text, format: Model::RemarkFormat::TAIL, tag: nil,
|
|
972
|
+
placement: nil, region: nil, source_offset: nil)
|
|
973
|
+
return unless node
|
|
974
|
+
return unless node.is_a?(Model::ModelElement)
|
|
975
|
+
# An empty remark body carries no content; storing it produced
|
|
976
|
+
# remarks == [""] entries (GH-363). Bare `--IPn:` informal
|
|
977
|
+
# propositions keep their declarations through
|
|
978
|
+
# create_or_find_informal_proposition, which does not run through
|
|
979
|
+
# here — only the redundant empty-body storage drops.
|
|
980
|
+
return if text.nil? || text.strip.empty?
|
|
981
|
+
|
|
982
|
+
if supports_remarks?(node)
|
|
983
|
+
if node_has_remarks?(node)
|
|
984
|
+
node.remarks ||= []
|
|
985
|
+
node.remarks << text
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
if tag.nil?
|
|
989
|
+
remark_info = Model::RemarkInfo.new(text: text, format: format,
|
|
990
|
+
placement: placement,
|
|
991
|
+
region: region,
|
|
992
|
+
source_offset: source_offset)
|
|
993
|
+
node.untagged_remarks ||= []
|
|
994
|
+
node.untagged_remarks << remark_info
|
|
995
|
+
end
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
# ----- Type predicates -----
|
|
1000
|
+
|
|
1001
|
+
def supports_remarks?(obj)
|
|
1002
|
+
obj.is_a?(Model::ModelElement)
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def node_has_remarks?(obj)
|
|
1006
|
+
obj.is_a?(Model::HasRemarks)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def supports_remark_items?(obj)
|
|
1010
|
+
obj.is_a?(Model::HasRemarkItems)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def supports_informal_propositions?(obj)
|
|
1014
|
+
obj.is_a?(Model::HasInformalPropositions)
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
# Extracts an IP id from a potentially prefixed item_id.
|
|
1018
|
+
# "wr:IP1" → "IP1", "IP1" → "IP1", "WR1" → nil
|
|
1019
|
+
def extract_ip_id(item_id)
|
|
1020
|
+
bare = item_id.sub(/\A\w+:/, "")
|
|
1021
|
+
bare.match?(/^IP\d+$/) ? bare : nil
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
def supports_where_rules?(obj)
|
|
1025
|
+
obj.is_a?(Model::HasWhereRules)
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
def repository?(obj)
|
|
1029
|
+
obj.is_a?(Model::Repository)
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def exp_file?(obj)
|
|
1033
|
+
obj.is_a?(Model::ExpFile)
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def cache?(obj)
|
|
1037
|
+
obj.is_a?(Model::Cache)
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
def get_schema_ids(model)
|
|
1041
|
+
if repository?(model) || exp_file?(model)
|
|
1042
|
+
model.schemas.filter_map(&:id)
|
|
1043
|
+
else
|
|
1044
|
+
[]
|
|
1045
|
+
end
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
# ----- Collection access -----
|
|
1049
|
+
|
|
1050
|
+
# Type-driven collection access — returns all collections for a node's type.
|
|
1051
|
+
def collections_on(node)
|
|
1052
|
+
attrs = COLLECTION_REGISTRY[node.class]
|
|
1053
|
+
return [] unless attrs
|
|
1054
|
+
|
|
1055
|
+
attrs.filter_map do |attr|
|
|
1056
|
+
col = node.public_send(attr)
|
|
1057
|
+
col if col.is_a?(Array)
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
# Type-driven single collection access — returns one named collection if the type has it.
|
|
1062
|
+
def get_collection(node, attr_name)
|
|
1063
|
+
return nil unless COLLECTION_REGISTRY[node.class]&.include?(attr_name)
|
|
1064
|
+
|
|
1065
|
+
collection = node.public_send(attr_name)
|
|
1066
|
+
collection if collection.is_a?(Array)
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
# ----- Helpers -----
|
|
1070
|
+
|
|
1071
|
+
def safe_find(model, path)
|
|
1072
|
+
return nil unless model
|
|
1073
|
+
|
|
1074
|
+
model.find(path)
|
|
1075
|
+
rescue StandardError
|
|
1076
|
+
nil
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
def safe_reset_children_by_id(obj)
|
|
1080
|
+
return unless obj
|
|
1081
|
+
return unless obj.is_a?(Model::ScopeContainer)
|
|
1082
|
+
|
|
1083
|
+
obj.reset_children_by_id
|
|
1084
|
+
end
|
|
1085
|
+
|
|
1086
|
+
def line_content_for(line_num)
|
|
1087
|
+
lines = source_lines_for_where_clause
|
|
1088
|
+
return "" if line_num < 1 || line_num > lines.length
|
|
1089
|
+
|
|
1090
|
+
lines[line_num - 1]
|
|
1091
|
+
end
|
|
1092
|
+
|
|
1093
|
+
def source_line_count
|
|
1094
|
+
source_lines_for_where_clause.length
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
end
|
|
1098
|
+
end
|