periplus-map 0.2.1__py3-none-any.whl
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.
- periplus/__init__.py +70 -0
- periplus/checkmap.py +80 -0
- periplus/cli.py +658 -0
- periplus/contract/schema/map.schema.json +138 -0
- periplus/contract/schema/navigation.schema.json +200 -0
- periplus/contract/schema/pack-file.schema.json +1405 -0
- periplus/contract/schema/pack-manifest.schema.json +245 -0
- periplus/contract/schema/project-settings.schema.json +85 -0
- periplus/contract/spec/engine.md +151 -0
- periplus/contract/spec/rules.md +352 -0
- periplus/detect.py +228 -0
- periplus/engine/__init__.py +5 -0
- periplus/engine/check.py +481 -0
- periplus/engine/emit.py +368 -0
- periplus/engine/packload.py +2321 -0
- periplus/engine/pattern.py +27 -0
- periplus/engine/rules.py +1946 -0
- periplus/engine/select.py +491 -0
- periplus/engine/source.py +172 -0
- periplus/engine/tree.py +424 -0
- periplus/errors.py +332 -0
- periplus/init.py +358 -0
- periplus/layout.py +28 -0
- periplus/manifest.py +498 -0
- periplus/map.py +364 -0
- periplus/navigate/__init__.py +1 -0
- periplus/navigate/find.py +143 -0
- periplus/navigate/ids.py +30 -0
- periplus/navigate/listing.py +42 -0
- periplus/navigate/mapfile.py +189 -0
- periplus/navigate/path.py +175 -0
- periplus/navigate/show.py +175 -0
- periplus/navigate/types.py +83 -0
- periplus/navigate/walk.py +138 -0
- periplus/packs/advancedqueue_basic@0.0.1/pack.yaml +14 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type.yaml +9 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_annotation.yaml +43 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent.yaml +8 -0
- periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call.yaml +8 -0
- periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/pack.yaml +15 -0
- periplus/packs/config_pages_basic@0.0.1/bundle/config_pages_bundle_config.yaml +54 -0
- periplus/packs/config_pages_basic@0.0.1/pack.yaml +11 -0
- periplus/packs/crop_basic@0.0.1/bundle/crop_bundle_config.yaml +54 -0
- periplus/packs/crop_basic@0.0.1/pack.yaml +11 -0
- periplus/packs/drupal@0.1.0/block/block_placement.yaml +25 -0
- periplus/packs/drupal@0.1.0/block/block_placement_code.yaml +58 -0
- periplus/packs/drupal@0.1.0/block/block_placement_config.yaml +53 -0
- periplus/packs/drupal@0.1.0/block/block_plugin.yaml +52 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_altered.yaml +61 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_annotation.yaml +69 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_attribute.yaml +56 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_content.yaml +46 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_derived.yaml +110 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_deriver.yaml +30 -0
- periplus/packs/drupal@0.1.0/block/block_type.yaml +14 -0
- periplus/packs/drupal@0.1.0/block/block_type_code.yaml +43 -0
- periplus/packs/drupal@0.1.0/block/block_type_config.yaml +45 -0
- periplus/packs/drupal@0.1.0/cache/cache.yaml +18 -0
- periplus/packs/drupal@0.1.0/cache/cache_backend.yaml +31 -0
- periplus/packs/drupal@0.1.0/component/sdc.yaml +19 -0
- periplus/packs/drupal@0.1.0/component/sdc_definition.yaml +25 -0
- periplus/packs/drupal@0.1.0/component/sdc_use.yaml +25 -0
- periplus/packs/drupal@0.1.0/config/config_object.yaml +65 -0
- periplus/packs/drupal@0.1.0/display/display.yaml +32 -0
- periplus/packs/drupal@0.1.0/display/display_code.yaml +53 -0
- periplus/packs/drupal@0.1.0/display/display_config.yaml +95 -0
- periplus/packs/drupal@0.1.0/entity/entity.yaml +33 -0
- periplus/packs/drupal@0.1.0/entity/entity_class.yaml +60 -0
- periplus/packs/drupal@0.1.0/entity/entity_save.yaml +27 -0
- periplus/packs/drupal@0.1.0/entity/entity_type_annotation.yaml +85 -0
- periplus/packs/drupal@0.1.0/entity/entity_type_attribute.yaml +76 -0
- periplus/packs/drupal@0.1.0/field/field_instance_code.yaml +40 -0
- periplus/packs/drupal@0.1.0/field/field_instance_config.yaml +58 -0
- periplus/packs/drupal@0.1.0/field/field_storage.yaml +37 -0
- periplus/packs/drupal@0.1.0/field/field_storage_code.yaml +39 -0
- periplus/packs/drupal@0.1.0/field/field_storage_config.yaml +24 -0
- periplus/packs/drupal@0.1.0/form/form.yaml +48 -0
- periplus/packs/drupal@0.1.0/form/form_class.yaml +50 -0
- periplus/packs/drupal@0.1.0/form/form_id.yaml +48 -0
- periplus/packs/drupal@0.1.0/form/form_methods.yaml +39 -0
- periplus/packs/drupal@0.1.0/hook/hook.yaml +8 -0
- periplus/packs/drupal@0.1.0/hook/hook_attribute.yaml +28 -0
- periplus/packs/drupal@0.1.0/module/module.yaml +8 -0
- periplus/packs/drupal@0.1.0/module/module_info.yaml +50 -0
- periplus/packs/drupal@0.1.0/pack.yaml +184 -0
- periplus/packs/drupal@0.1.0/plugin/plugin.yaml +17 -0
- periplus/packs/drupal@0.1.0/plugin/plugin_annotation.yaml +120 -0
- periplus/packs/drupal@0.1.0/recommendations/recommendations.yaml +14 -0
- periplus/packs/drupal@0.1.0/service/service.yaml +19 -0
- periplus/packs/drupal@0.1.0/service/service_autowire.yaml +28 -0
- periplus/packs/drupal@0.1.0/service/service_definition.yaml +24 -0
- periplus/packs/drupal@0.1.0/service/service_use.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/action/action.yaml +9 -0
- periplus/packs/drupal_basic@0.3.0/action/action_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/action/action_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/block/block_placement.yaml +16 -0
- periplus/packs/drupal_basic@0.3.0/block/block_placement_config.yaml +51 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin.yaml +14 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_annotation.yaml +47 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/block_type/block_type.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/block_type/block_type_config.yaml +54 -0
- periplus/packs/drupal_basic@0.3.0/boundary/boundary_types.yaml +19 -0
- periplus/packs/drupal_basic@0.3.0/bundle/bundle.yaml +20 -0
- periplus/packs/drupal_basic@0.3.0/bundle/bundle_config.yaml +393 -0
- periplus/packs/drupal_basic@0.3.0/call/call.yaml +349 -0
- periplus/packs/drupal_basic@0.3.0/component/component.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/config/config_object.yaml +119 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/display/display.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/display/display_config.yaml +203 -0
- periplus/packs/drupal_basic@0.3.0/display/display_mode_config.yaml +60 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/entity_type.yaml +16 -0
- periplus/packs/drupal_basic@0.3.0/field/field_instance_config.yaml +116 -0
- periplus/packs/drupal_basic@0.3.0/field/field_storage.yaml +21 -0
- periplus/packs/drupal_basic@0.3.0/field/field_storage_config.yaml +45 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/field_type/field_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_type/field_type_attribute.yaml +50 -0
- periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_attribute.yaml +50 -0
- periplus/packs/drupal_basic@0.3.0/form/form_id.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook.yaml +100 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook_attribute.yaml +64 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook_theme_include.yaml +34 -0
- periplus/packs/drupal_basic@0.3.0/library/library.yaml +12 -0
- periplus/packs/drupal_basic@0.3.0/library/library_definition.yaml +58 -0
- periplus/packs/drupal_basic@0.3.0/local_action/local_action.yaml +12 -0
- periplus/packs/drupal_basic@0.3.0/local_action/local_action_definition.yaml +49 -0
- periplus/packs/drupal_basic@0.3.0/mail/mail.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/mail/mail_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source_attribute.yaml +79 -0
- periplus/packs/drupal_basic@0.3.0/menu_link/menu_link.yaml +18 -0
- periplus/packs/drupal_basic@0.3.0/menu_link/menu_link_definition.yaml +48 -0
- periplus/packs/drupal_basic@0.3.0/module/module.yaml +7 -0
- periplus/packs/drupal_basic@0.3.0/module/module_info.yaml +58 -0
- periplus/packs/drupal_basic@0.3.0/pack.yaml +528 -0
- periplus/packs/drupal_basic@0.3.0/permission/permission.yaml +7 -0
- periplus/packs/drupal_basic@0.3.0/permission/permission_definition.yaml +27 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker.yaml +9 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/role/role.yaml +14 -0
- periplus/packs/drupal_basic@0.3.0/role/role_config.yaml +44 -0
- periplus/packs/drupal_basic@0.3.0/route/route.yaml +42 -0
- periplus/packs/drupal_basic@0.3.0/route/route_definition.yaml +113 -0
- periplus/packs/drupal_basic@0.3.0/route/route_options.yaml +55 -0
- periplus/packs/drupal_basic@0.3.0/route/route_requirements.yaml +108 -0
- periplus/packs/drupal_basic@0.3.0/service/service.yaml +56 -0
- periplus/packs/drupal_basic@0.3.0/service/service_definition.yaml +181 -0
- periplus/packs/drupal_basic@0.3.0/template/template.yaml +86 -0
- periplus/packs/drupal_basic@0.3.0/template/template_route_link.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/theme/theme.yaml +15 -0
- periplus/packs/drupal_basic@0.3.0/theme/theme_info.yaml +61 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/template_suggestion.yaml +29 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook.yaml +87 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook_render.yaml +20 -0
- periplus/packs/drupal_basic@0.3.0/url/url.yaml +88 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_field_attribute.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_filter_attribute.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_plugin.yaml +15 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_style_attribute.yaml +80 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment.yaml +12 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php.yaml +15 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php_array.yaml +52 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_twig.yaml +26 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/php_function_holder.yaml +13 -0
- periplus/packs/drupal_js_basic@0.0.1/behavior/behavior.yaml +198 -0
- periplus/packs/drupal_js_basic@0.0.1/pack.yaml +153 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting.yaml +13 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_js.yaml +15 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_php.yaml +92 -0
- periplus/packs/drush_basic@0.0.1/command/command.yaml +15 -0
- periplus/packs/drush_basic@0.0.1/command/command_annotation.yaml +60 -0
- periplus/packs/drush_basic@0.0.1/command/command_attribute.yaml +88 -0
- periplus/packs/drush_basic@0.0.1/pack.yaml +29 -0
- periplus/packs/eck_basic@0.0.1/bundle/eck_bundle_config.yaml +56 -0
- periplus/packs/eck_basic@0.0.1/entity_type/eck_entity_type_config.yaml +26 -0
- periplus/packs/eck_basic@0.0.1/pack.yaml +28 -0
- periplus/packs/go@0.0.2/interfaces/satisfies.yaml +47 -0
- periplus/packs/go@0.0.2/pack.yaml +51 -0
- periplus/packs/go@0.0.2/types/find_declarations.yaml +137 -0
- periplus/packs/go@0.0.2/types/node_types.yaml +36 -0
- periplus/packs/go_basic@0.0.3/pack.yaml +104 -0
- periplus/packs/go_basic@0.0.3/rules/calls.yaml +25 -0
- periplus/packs/go_basic@0.0.3/rules/declarations.yaml +40 -0
- periplus/packs/go_basic@0.0.3/rules/imports.yaml +16 -0
- periplus/packs/go_basic@0.0.3/types/node_types.yaml +15 -0
- periplus/packs/js_basic@0.0.1/function/function.yaml +68 -0
- periplus/packs/js_basic@0.0.1/pack.yaml +50 -0
- periplus/packs/laravel_basic@0.0.5/pack.yaml +313 -0
- periplus/packs/laravel_basic@0.0.5/rules/classes.yaml +42 -0
- periplus/packs/laravel_basic@0.0.5/rules/config.yaml +229 -0
- periplus/packs/laravel_basic@0.0.5/rules/routes.yaml +210 -0
- periplus/packs/laravel_basic@0.0.5/rules/types.yaml +50 -0
- periplus/packs/laravel_basic@0.0.5/rules/views.yaml +100 -0
- periplus/packs/paragraphs_basic@0.0.1/bundle/paragraph_bundle_config.yaml +54 -0
- periplus/packs/paragraphs_basic@0.0.1/pack.yaml +12 -0
- periplus/packs/php@0.1.0/edges/edge_kinds.yaml +64 -0
- periplus/packs/php@0.1.0/edges/find_calls.yaml +34 -0
- periplus/packs/php@0.1.0/edges/find_imports.yaml +18 -0
- periplus/packs/php@0.1.0/edges/find_member_access.yaml +22 -0
- periplus/packs/php@0.1.0/edges/find_structure.yaml +38 -0
- periplus/packs/php@0.1.0/embedded/embedded_regions.yaml +19 -0
- periplus/packs/php@0.1.0/imports/imports.yaml +20 -0
- periplus/packs/php@0.1.0/pack.yaml +117 -0
- periplus/packs/php@0.1.0/resolution/excluded_tokens.yaml +23 -0
- periplus/packs/php@0.1.0/resolution/resolution.yaml +35 -0
- periplus/packs/php@0.1.0/types/find_declarations.yaml +102 -0
- periplus/packs/php@0.1.0/types/node_types.yaml +55 -0
- periplus/packs/php_basic@0.2.0/edges/edge_kinds.yaml +48 -0
- periplus/packs/php_basic@0.2.0/edges/find_structure.yaml +750 -0
- periplus/packs/php_basic@0.2.0/imports/imports.yaml +19 -0
- periplus/packs/php_basic@0.2.0/pack.yaml +94 -0
- periplus/packs/php_basic@0.2.0/types/find_declarations.yaml +267 -0
- periplus/packs/php_basic@0.2.0/types/node_types.yaml +55 -0
- periplus/packs/profile_basic@0.0.1/bundle/profile_bundle_config.yaml +54 -0
- periplus/packs/profile_basic@0.0.1/pack.yaml +12 -0
- periplus/packs/salesforce_basic@0.0.1/event/salesforce_event.yaml +36 -0
- periplus/packs/salesforce_basic@0.0.1/pack.yaml +31 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping.yaml +19 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping_config.yaml +83 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field.yaml +8 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field_annotation.yaml +43 -0
- periplus/packs/twig@0.0.1/pack.yaml +26 -0
- periplus/packs/twig@0.0.1/types/template.yaml +57 -0
- periplus/packs/twig_basic@0.1.0/pack.yaml +11 -0
- periplus/packs/twig_tweak_basic@0.0.1/pack.yaml +27 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/block.yaml +20 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/entity.yaml +35 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/field.yaml +21 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/menu.yaml +21 -0
- periplus/packs/webform_basic@0.0.1/bundle/webform_submission_bundle_config.yaml +54 -0
- periplus/packs/webform_basic@0.0.1/pack.yaml +20 -0
- periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler.yaml +8 -0
- periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler_annotation.yaml +43 -0
- periplus/packs/yaml@0.0.1/pack.yaml +72 -0
- periplus/packs/yaml@0.0.1/types/find_mapping_entry.yaml +21 -0
- periplus/packs/yaml@0.0.1/types/yaml_types.yaml +44 -0
- periplus/packs/yaml_basic@0.1.0/pack.yaml +10 -0
- periplus/packs.py +447 -0
- periplus/preflight.py +102 -0
- periplus/py.typed +0 -0
- periplus/report.py +596 -0
- periplus/resolution.py +355 -0
- periplus/settings.py +617 -0
- periplus/settings.stub.yml +99 -0
- periplus/spec.py +774 -0
- periplus/update.py +197 -0
- periplus/validate.py +655 -0
- periplus_map-0.2.1.dist-info/METADATA +566 -0
- periplus_map-0.2.1.dist-info/RECORD +272 -0
- periplus_map-0.2.1.dist-info/WHEEL +4 -0
- periplus_map-0.2.1.dist-info/entry_points.txt +2 -0
- periplus_map-0.2.1.dist-info/licenses/LICENSE +202 -0
- periplus_map-0.2.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,2321 @@
|
|
|
1
|
+
"""Step one: read every rule file of the loaded packs into typed records and the type table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass, field, replace
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from periplus.engine.pattern import compile_pattern
|
|
11
|
+
from periplus.engine.select import FileTypes, FolderUnresolved, holds_double_star
|
|
12
|
+
from periplus.engine.tree import STEPS, Grammar, ImportRule, Step, _parser
|
|
13
|
+
from periplus.errors import ExitCode, HarnessError, Problem
|
|
14
|
+
from periplus.manifest import MANIFEST_FILENAME, LoadedPack
|
|
15
|
+
from periplus.validate import MANIFEST_SCHEMA, PACK_FILE_SCHEMA, _load, _schema, _yaml_files
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"PATTERN_VALUE",
|
|
19
|
+
"PLACEHOLDER",
|
|
20
|
+
"TEXT",
|
|
21
|
+
"BoundaryEntry",
|
|
22
|
+
"Condition",
|
|
23
|
+
"EXECUTED_KEYS",
|
|
24
|
+
"UNEXECUTED_KEYS",
|
|
25
|
+
"EdgeKind",
|
|
26
|
+
"EdgeSpec",
|
|
27
|
+
"FileInfo",
|
|
28
|
+
"HasKey",
|
|
29
|
+
"IdMatches",
|
|
30
|
+
"KeyEquals",
|
|
31
|
+
"PackRulesUnreadable",
|
|
32
|
+
"Rule",
|
|
33
|
+
"RuleSet",
|
|
34
|
+
"TypeInfo",
|
|
35
|
+
"Value",
|
|
36
|
+
"load_rules",
|
|
37
|
+
"namespace_of",
|
|
38
|
+
"namespaces",
|
|
39
|
+
"node_types",
|
|
40
|
+
"twin_type",
|
|
41
|
+
"unexecuted",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
#: The prefix of a rule's key paths.
|
|
45
|
+
_RULE = "file.rules.*."
|
|
46
|
+
|
|
47
|
+
#: The key paths, below their holder, of resolution steps and the separator beside them.
|
|
48
|
+
_RESOLVE = (
|
|
49
|
+
*(".resolve", ".resolve.*.full_if_prefixed", ".resolve.*.prefix_with"),
|
|
50
|
+
*(".resolve.*.prefix_bare_with", ".separator"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
#: The key paths, below a rule's id or edge end, of the steps that look a name up in a table.
|
|
54
|
+
_TABLE_STEPS = (".resolve.*.lookup_last_segment_in",)
|
|
55
|
+
|
|
56
|
+
#: Every key path of the two pack schemas that the engine or the pack check executes. ``*`` is one
|
|
57
|
+
#: key or item. A path starts with the document it sits in: ``manifest.`` or ``file.``.
|
|
58
|
+
EXECUTED_KEYS = frozenset(
|
|
59
|
+
{
|
|
60
|
+
*(f"manifest.{k}" for k in ("pack", "version", "depends", "folders", "identity")),
|
|
61
|
+
"manifest.tables",
|
|
62
|
+
*(f"manifest.boundary{k}" for k in ("", ".*.type", ".*.ancestry", ".*.names")),
|
|
63
|
+
*(f"manifest.identity.call_{k}" for k in ("arguments", "argument", "argument_name")),
|
|
64
|
+
*(f"manifest.identity.call_literal{k}" for k in ("s", "_content", "_delimiters")),
|
|
65
|
+
*(f"manifest.detect{k}" for k in ("", ".any_of", ".any_of.*.path", ".any_of.*.contains")),
|
|
66
|
+
*(
|
|
67
|
+
f"manifest.grammar{k}"
|
|
68
|
+
for k in ("", ".provider", ".provider_version", ".language", ".grammar_version")
|
|
69
|
+
),
|
|
70
|
+
*(
|
|
71
|
+
f"manifest.grammar.constant_access{k}"
|
|
72
|
+
for k in ("", ".node", ".scope", ".name", ".enclosing")
|
|
73
|
+
),
|
|
74
|
+
*(f"manifest.grammar.constant_declaration{k}" for k in ("", ".node", ".name", ".value")),
|
|
75
|
+
*(
|
|
76
|
+
f"manifest.files{k}"
|
|
77
|
+
for k in (
|
|
78
|
+
*("", ".extends", ".extensions", ".add_extensions", ".exclude", ".add_excludes"),
|
|
79
|
+
*(".types", ".types.*.endings", ".types.*.names", ".types.*.reader"),
|
|
80
|
+
".types.*.format",
|
|
81
|
+
*(f".types.*.path_values{k}" for k in ("", ".*.pattern", ".*.template")),
|
|
82
|
+
".types.*.path_values.*.relative_to",
|
|
83
|
+
*(f".types.*.path_values.*{k}" for k in (".case", ".normalize")),
|
|
84
|
+
*(f".types.*.path_values.*.normalize.*{k}" for k in (".replace", ".with")),
|
|
85
|
+
*(f".types.*.sections{k}" for k in ("", ".*.name", ".*.open", ".*.close")),
|
|
86
|
+
*(f".types.*.spans{k}" for k in ("", ".*.name", ".*.open", ".*.balance")),
|
|
87
|
+
*(f".types.*{k}" for k in _RESOLVE),
|
|
88
|
+
)
|
|
89
|
+
),
|
|
90
|
+
*(f"file.needs{k}" for k in ("", ".types", ".roles")),
|
|
91
|
+
*("file.roles", "file.roles.*.name"),
|
|
92
|
+
*(f"file.edge_kinds{k}" for k in ("", ".*.kind", ".*.from", ".*.to")),
|
|
93
|
+
*(
|
|
94
|
+
f"file.node_types{k}"
|
|
95
|
+
for k in ("", ".*.name", ".*.parent", ".*.abstract", ".*.id_namespace")
|
|
96
|
+
),
|
|
97
|
+
*(f"file.imports{k}" for k in ("", ".*.ast", ".*.group_ast", ".*.clause", ".*.facts")),
|
|
98
|
+
*(f"file.imports.*.bind{k}" for k in ("", ".name", ".prefix_from_group", ".strip_prefix")),
|
|
99
|
+
"file.imports.*.bind.skip_when_field_set",
|
|
100
|
+
*(f"file.imports.*.bind{k}" for k in (".unbound_aliases", ".normalize")),
|
|
101
|
+
*(f"file.imports.*.bind.normalize.*{k}" for k in (".replace", ".with")),
|
|
102
|
+
"file.rules",
|
|
103
|
+
*(f"{_RULE}{k}" for k in ("rule", "reads", "in", "confidence", "match", "id", "emits")),
|
|
104
|
+
*(f"{_RULE}line{k}" for k in ("", ".capture")),
|
|
105
|
+
*(f"{_RULE}values{k}" for k in ("", ".*.from", ".*.from.capture", ".*.template")),
|
|
106
|
+
*(f"{_RULE}values.*{k}" for k in (".from.enclosing", ".join")),
|
|
107
|
+
*(
|
|
108
|
+
f"{_RULE}values.*{k}"
|
|
109
|
+
for k in (".normalize", ".normalize.*.replace", ".normalize.*.with")
|
|
110
|
+
),
|
|
111
|
+
f"{_RULE}values.*.case",
|
|
112
|
+
*(
|
|
113
|
+
f"{_RULE}match.{k}"
|
|
114
|
+
for k in (
|
|
115
|
+
*("file", "ending", "filetype", "each", "where", "declaration", "name_child"),
|
|
116
|
+
*("reference", "skip_names", "whole_written"),
|
|
117
|
+
*("body_child", "attribute", "annotation", "text", "reads_sections"),
|
|
118
|
+
*("node_type", "follow"),
|
|
119
|
+
)
|
|
120
|
+
),
|
|
121
|
+
*(f"{_RULE}match.follow.*.{k}" for k in ("edge", "ancestors", "node", "id_matches")),
|
|
122
|
+
*(
|
|
123
|
+
f"{_RULE}match.where.*.{k}"
|
|
124
|
+
for k in ("key", "has_key", "id_matches", "is_mapping", "applies_to", "has_child")
|
|
125
|
+
),
|
|
126
|
+
*(f"{_RULE}match.where.*.name_matches{k}" for k in ("", ".child", ".pattern")),
|
|
127
|
+
*(
|
|
128
|
+
f"{_RULE}id.{k}"
|
|
129
|
+
for k in (
|
|
130
|
+
*("from", "from.capture", "from.attribute_argument", "from.annotation_key"),
|
|
131
|
+
*("from.argument", "from.*.qualified", "from.*.docblock"),
|
|
132
|
+
*("from.*.qualified.docblock", "pattern"),
|
|
133
|
+
*("template", "must_be", "normalize", "normalize.*.replace", "normalize.*.with"),
|
|
134
|
+
)
|
|
135
|
+
),
|
|
136
|
+
*(f"{_RULE}id{k}" for k in (*_RESOLVE, *_TABLE_STEPS)),
|
|
137
|
+
*(f"{_RULE}emits.*.binding{k}" for k in ("", ".name", ".target")),
|
|
138
|
+
*(f"{_RULE}emits.*.{k}" for k in ("node", "node.type", "attribute", "attribute.name")),
|
|
139
|
+
*(f"{_RULE}emits.*.{k}" for k in ("attribute.from", "attribute.template", "edge")),
|
|
140
|
+
f"{_RULE}emits.*.edge.kind",
|
|
141
|
+
f"{_RULE}emits.*.edge.to.from.attribute_argument",
|
|
142
|
+
f"{_RULE}emits.*.edge.to.from.argument",
|
|
143
|
+
*(
|
|
144
|
+
f"{_RULE}emits.*.edge.{end}{k}"
|
|
145
|
+
for end in ("from", "to")
|
|
146
|
+
for k in (
|
|
147
|
+
*("", ".from", ".template", ".type", ".each", ".pattern", ".where", ".on_miss"),
|
|
148
|
+
*(".where.*.matches", ".where.*.listed_at", ".from.key", ".from.capture"),
|
|
149
|
+
".where.*.not_matches",
|
|
150
|
+
*(".from.*.key", ".from.*.each", ".types", ".declared", *_RESOLVE),
|
|
151
|
+
*_TABLE_STEPS,
|
|
152
|
+
)
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _schema_paths(schema: Mapping[str, Any], node: object, path: str) -> set[str]:
|
|
159
|
+
"""Each key path below ``path`` the schema node accepts, its ``$defs`` followed. ``$schema``
|
|
160
|
+
names the contract a file is written against and carries no data, so it is left out."""
|
|
161
|
+
if not isinstance(node, Mapping):
|
|
162
|
+
return set()
|
|
163
|
+
if "$ref" in node:
|
|
164
|
+
return _schema_paths(schema, schema["$defs"][node["$ref"].rsplit("/", 1)[-1]], path)
|
|
165
|
+
found: set[str] = set()
|
|
166
|
+
for name, child in node.get("properties", {}).items():
|
|
167
|
+
if name != "$schema":
|
|
168
|
+
found |= {f"{path}.{name}", *_schema_paths(schema, child, f"{path}.{name}")}
|
|
169
|
+
for branch in (*node.get("oneOf", ()), *node.get("anyOf", ())):
|
|
170
|
+
found |= _schema_paths(schema, branch, path)
|
|
171
|
+
for key in ("items", "additionalProperties"):
|
|
172
|
+
found |= _schema_paths(schema, node.get(key), f"{path}.*")
|
|
173
|
+
return found
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
#: Every other key path the two pack schemas accept. A rule that holds one is not executed; a pack
|
|
177
|
+
#: that holds any other one is reported as a rule the engine cannot execute.
|
|
178
|
+
UNEXECUTED_KEYS: tuple[str, ...] = tuple(
|
|
179
|
+
sorted(
|
|
180
|
+
{
|
|
181
|
+
path
|
|
182
|
+
for name, document in (("manifest", MANIFEST_SCHEMA), ("file", PACK_FILE_SCHEMA))
|
|
183
|
+
for path in _schema_paths(_schema(document), _schema(document), name)
|
|
184
|
+
}
|
|
185
|
+
- EXECUTED_KEYS
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def unexecuted(document: object, prefix: str) -> list[str]:
|
|
191
|
+
"""The paths of ``UNEXECUTED_KEYS`` that start with ``prefix`` and that the document holds,
|
|
192
|
+
each without the prefix, leaving out a path below another one found. A rule's paths are found
|
|
193
|
+
only under the rule prefix ``file.rules.*.``, since the rule itself is reported."""
|
|
194
|
+
found = [
|
|
195
|
+
path.removeprefix(prefix)
|
|
196
|
+
for path in UNEXECUTED_KEYS
|
|
197
|
+
if path.startswith(prefix)
|
|
198
|
+
and (prefix == _RULE or not path.startswith(_RULE))
|
|
199
|
+
and _holds(document, path.removeprefix(prefix).split("."))
|
|
200
|
+
]
|
|
201
|
+
return [path for path in found if not any(path.startswith(f"{q}.") for q in found)]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _holds(value: object, segments: Sequence[str]) -> bool:
|
|
205
|
+
if not segments:
|
|
206
|
+
return True
|
|
207
|
+
head, rest = segments[0], segments[1:]
|
|
208
|
+
if head == "*":
|
|
209
|
+
children = list(value.values()) if isinstance(value, Mapping) else []
|
|
210
|
+
children += value if isinstance(value, list) else []
|
|
211
|
+
return any(_holds(child, rest) for child in children)
|
|
212
|
+
return isinstance(value, Mapping) and head in value and _holds(value[head], rest)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
#: The values a reference rule gives its edge ends' templates, beside its name_matches groups.
|
|
216
|
+
_REFERENCE_VALUES = {"enclosing_type", "enclosing_method"}
|
|
217
|
+
|
|
218
|
+
#: The keys of the rule shapes this engine executes. A rule with any other key is not run.
|
|
219
|
+
_RULE_KEYS = {"rule", "reads", "in", "match", "id", "emits", "confidence"}
|
|
220
|
+
|
|
221
|
+
#: The match keys of a rule that reads a parse tree.
|
|
222
|
+
_DECLARATION_MATCH = {
|
|
223
|
+
*("declaration", "name_child", "body_child", "where", "filetype", "file", "ending"),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#: The names the engine fills for a declaration rule's id. Any other name its template holds is a
|
|
227
|
+
#: path value, a settings value or a field of the matched node.
|
|
228
|
+
_TREE_FILLED = {
|
|
229
|
+
*("declared_name", "namespace", "qualified_name", "enclosing_type", "enclosing_method"),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#: The edge ends of a tree rule that name a node, where the other kind of end is a written name.
|
|
233
|
+
_TREE_ENDS = {"this_node", "enclosing_class"}
|
|
234
|
+
|
|
235
|
+
#: The edge ends of a declaration rule: those, and the node the file's own rule made for it.
|
|
236
|
+
_DECLARATION_ENDS = {*_TREE_ENDS, "enclosing_file", "enclosing_method"}
|
|
237
|
+
|
|
238
|
+
#: The edge ends of an attribute rule that name a node; the other kind is an argument's value.
|
|
239
|
+
_ATTRIBUTE_ENDS = {"this_node", "enclosing_class", "enclosing_method"}
|
|
240
|
+
|
|
241
|
+
#: The edge ends whose value is the whole id another rule minted, namespace and all.
|
|
242
|
+
_WHOLE_ENDS = {"enclosing_declaration", "enclosing_class", "enclosing_method", "declared"}
|
|
243
|
+
|
|
244
|
+
#: A ``{name}`` in an id or edge template.
|
|
245
|
+
PLACEHOLDER = re.compile(r"\{(\w+)\}")
|
|
246
|
+
|
|
247
|
+
#: A ``{name}`` in an id pattern: a path or settings value, filled as literal text. A name starts
|
|
248
|
+
#: with a letter, so a quantifier such as ``{2}`` stays one.
|
|
249
|
+
PATTERN_VALUE = re.compile(r"\{([A-Za-z]\w*)\}")
|
|
250
|
+
|
|
251
|
+
#: The key path an edge source ``text`` reads: an entry whose value is text holds it under this key
|
|
252
|
+
#: alone, which no other key path reaches.
|
|
253
|
+
TEXT = ""
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class PackRulesUnreadable(HarnessError):
|
|
257
|
+
"""A rule file cannot be read, a type is declared twice, or a rule emits an undeclared type."""
|
|
258
|
+
|
|
259
|
+
code = ExitCode.PACK_RULES_UNREADABLE
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@dataclass(frozen=True, slots=True)
|
|
263
|
+
class TypeInfo:
|
|
264
|
+
"""One node type: its id namespace, its parent type, and the pack that declared it."""
|
|
265
|
+
|
|
266
|
+
name: str
|
|
267
|
+
id_namespace: str | None
|
|
268
|
+
parent: str | None
|
|
269
|
+
pack: str
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True, slots=True)
|
|
273
|
+
class KeyEquals:
|
|
274
|
+
"""A ``where`` condition: a value at the key path equals ``value``, type included, or with
|
|
275
|
+
``negate`` none does.
|
|
276
|
+
|
|
277
|
+
An absent key equals ``None``.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
path: str
|
|
281
|
+
value: Any
|
|
282
|
+
negate: bool = False
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@dataclass(frozen=True, slots=True)
|
|
286
|
+
class HasKey:
|
|
287
|
+
"""A ``where`` condition: the key path reaches something."""
|
|
288
|
+
|
|
289
|
+
path: str
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass(frozen=True, slots=True)
|
|
293
|
+
class IdMatches:
|
|
294
|
+
"""A ``where`` condition: the pattern is found in the entry's key or the file's stem."""
|
|
295
|
+
|
|
296
|
+
pattern: re.Pattern[str]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass(frozen=True, slots=True)
|
|
300
|
+
class IsMapping:
|
|
301
|
+
"""A ``where`` condition: whether the entry's value is a mapping, an empty value counting as an
|
|
302
|
+
empty mapping, equals ``value``. A whole file's root is a mapping."""
|
|
303
|
+
|
|
304
|
+
value: bool
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
Condition = KeyEquals | HasKey | IdMatches | IsMapping
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@dataclass(frozen=True, slots=True)
|
|
311
|
+
class Value:
|
|
312
|
+
"""A named value: one capture, or a template filled from the values known, then reshaped by
|
|
313
|
+
``normalize`` and then ``case``, ``lower``, ``upper`` or ``""``. A path value holds the
|
|
314
|
+
``pattern`` searched in the file's path, and its template is filled from that pattern's
|
|
315
|
+
captures; with ``relative_to``, a folder name, the pattern is searched in the path below the
|
|
316
|
+
deepest directory of that folder that holds the file. ``enclosing`` names a span and one
|
|
317
|
+
capture of its opening: the value joins that capture of each span holding the match with
|
|
318
|
+
``join``."""
|
|
319
|
+
|
|
320
|
+
name: str
|
|
321
|
+
capture: str
|
|
322
|
+
template: str | None
|
|
323
|
+
normalize: tuple[tuple[re.Pattern[str], str], ...] = ()
|
|
324
|
+
case: str = ""
|
|
325
|
+
pattern: re.Pattern[str] | None = None
|
|
326
|
+
relative_to: str = ""
|
|
327
|
+
enclosing: tuple[str, str] | None = None
|
|
328
|
+
join: str = ""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dataclass(frozen=True, slots=True)
|
|
332
|
+
class FileInfo:
|
|
333
|
+
"""What a declared file type says about its files: the values its path gives, and its
|
|
334
|
+
sections, each a name, an opening pattern and a closing template; and the resolution steps,
|
|
335
|
+
with their separator, an edge end of a text rule takes when it declares none. Last its spans,
|
|
336
|
+
each a name, an opening pattern, and the opening and closing characters it balances."""
|
|
337
|
+
|
|
338
|
+
path_values: tuple[Value, ...] = ()
|
|
339
|
+
sections: tuple[tuple[str, re.Pattern[str], str], ...] = ()
|
|
340
|
+
resolve: tuple[Step, ...] = ()
|
|
341
|
+
separator: str = ""
|
|
342
|
+
spans: tuple[tuple[str, re.Pattern[str], str, str], ...] = ()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass(frozen=True, slots=True)
|
|
346
|
+
class Rule:
|
|
347
|
+
"""One rule. ``supported`` is false for a shape this engine does not execute.
|
|
348
|
+
|
|
349
|
+
``each`` is the key path whose mapping entries the rule fires on, ``""`` for the file's top
|
|
350
|
+
level, or ``None`` for the whole file. ``id_template`` names ``{file_stem}`` and ``{key}``.
|
|
351
|
+
|
|
352
|
+
A rule that reads a parse tree holds ``declaration``, the tree node type it fires on, and its
|
|
353
|
+
``id_template`` names the tree sources instead; it is ``""`` for an edge rule with no id, and
|
|
354
|
+
for an id from the call's argument ``id_argument``, a position.
|
|
355
|
+
|
|
356
|
+
A rule that matches an attribute holds ``attribute``, the full name of its class, and
|
|
357
|
+
``applies_to``, the tree node type the attribute sits on. Its id is the value of the argument
|
|
358
|
+
``id_argument``, a name or a position, and ``must_be_literal`` says that value must be a text
|
|
359
|
+
literal. Its ``attributes`` pair a name with an argument.
|
|
360
|
+
|
|
361
|
+
A rule that matches a docblock annotation holds ``annotation``, the bare name written after the
|
|
362
|
+
at sign, and ``applies_to``; ``id_argument`` and the second of each ``attributes`` pair name a
|
|
363
|
+
key of the annotation's body.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
pack: str
|
|
367
|
+
name: str
|
|
368
|
+
supported: bool
|
|
369
|
+
folders: tuple[str, ...]
|
|
370
|
+
glob: str
|
|
371
|
+
filetype: str
|
|
372
|
+
node_type: str
|
|
373
|
+
confidence: str
|
|
374
|
+
where: tuple[Condition, ...] = ()
|
|
375
|
+
normalize: tuple[tuple[re.Pattern[str], str], ...] = ()
|
|
376
|
+
attributes: tuple[tuple[str, str], ...] = ()
|
|
377
|
+
edges: tuple[EdgeSpec, ...] = ()
|
|
378
|
+
reason: str = ""
|
|
379
|
+
twin_type: str = ""
|
|
380
|
+
each: str | None = None
|
|
381
|
+
id_template: str = "{file_stem}"
|
|
382
|
+
#: A declaration rule: the pattern searched in the text of the source ``id_source``, its named
|
|
383
|
+
#: captures joining the values ``id_template`` may name; ``""`` for none.
|
|
384
|
+
id_pattern: str = ""
|
|
385
|
+
id_source: str = ""
|
|
386
|
+
declaration: str = ""
|
|
387
|
+
name_child: str = ""
|
|
388
|
+
#: A declaration rule: the children walked from the match to its name, each by field or node
|
|
389
|
+
#: type, when ``name_child`` is a list; ``name_child`` is then ``""``.
|
|
390
|
+
name_path: tuple[str, ...] = ()
|
|
391
|
+
#: A declaration rule: the pattern of its id's docblock source, whose one named group is the
|
|
392
|
+
#: value, and whether that value is resolved through the file's imports and namespace.
|
|
393
|
+
docblock: re.Pattern[str] | None = None
|
|
394
|
+
docblock_qualified: bool = False
|
|
395
|
+
body_child: str = ""
|
|
396
|
+
#: A declaration rule: the tree node types the match must have as direct children, and the
|
|
397
|
+
#: fields of the matched node its id names.
|
|
398
|
+
has_child: tuple[str, ...] = ()
|
|
399
|
+
fields: tuple[str, ...] = ()
|
|
400
|
+
#: A declaration rule: the fields its id reads resolved through the file's imports.
|
|
401
|
+
qualified: tuple[str, ...] = ()
|
|
402
|
+
#: A declaration or reference rule: each child, by field or node type, and the pattern its
|
|
403
|
+
#: whole text must match.
|
|
404
|
+
name_matches: tuple[tuple[str, re.Pattern[str]], ...] = ()
|
|
405
|
+
attribute: str = ""
|
|
406
|
+
applies_to: str = ""
|
|
407
|
+
id_argument: str = ""
|
|
408
|
+
must_be_literal: bool = False
|
|
409
|
+
annotation: str = ""
|
|
410
|
+
#: A reference rule: the tree node type it fires on, its ``name_child`` the written name's
|
|
411
|
+
#: child, and ``member_child`` the child whose text follows the resolved name, or ``""``.
|
|
412
|
+
#: ``skip_names`` holds the written names that make nothing, and ``whole_written`` says every
|
|
413
|
+
#: node below the written name must be a written name too.
|
|
414
|
+
reference: str = ""
|
|
415
|
+
member_child: str = ""
|
|
416
|
+
skip_names: frozenset[str] = frozenset()
|
|
417
|
+
whole_written: bool = False
|
|
418
|
+
#: A rule that reads its file as text: the pattern it fires on, and the capture of the id.
|
|
419
|
+
#: Its ``attributes`` and each edge's ``key`` name a capture.
|
|
420
|
+
text: re.Pattern[str] | None = None
|
|
421
|
+
id_capture: str = ""
|
|
422
|
+
#: A text rule's named values in order, the sections its match reads inside, and each
|
|
423
|
+
#: attribute given as a name and a template; a text rule's ``id_template`` is its id template.
|
|
424
|
+
values: tuple[Value, ...] = ()
|
|
425
|
+
reads_sections: tuple[str, ...] = ()
|
|
426
|
+
templates: tuple[tuple[str, str], ...] = ()
|
|
427
|
+
#: A text rule's bindings, each a name template and a target template, and its id's
|
|
428
|
+
#: resolution steps with their separator.
|
|
429
|
+
bindings: tuple[tuple[str, str], ...] = ()
|
|
430
|
+
id_resolve: tuple[Step, ...] = ()
|
|
431
|
+
id_separator: str = ""
|
|
432
|
+
#: A text rule's ``line`` capture: the node, its values and templates sit at its line.
|
|
433
|
+
line_capture: str = ""
|
|
434
|
+
#: A declaration, text or reference rule's ``match.ending``: the claimed endings of the files
|
|
435
|
+
#: it reads; empty for any.
|
|
436
|
+
endings: tuple[str, ...] = ()
|
|
437
|
+
#: A map rule: the node type it matches, ``""`` on a file rule, and its follow steps, each a
|
|
438
|
+
#: step name and its value: a kind, a tuple of kinds, a template or a compiled pattern.
|
|
439
|
+
matches_type: str = ""
|
|
440
|
+
follow: tuple[tuple[str, Any], ...] = ()
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@dataclass(frozen=True, slots=True)
|
|
444
|
+
class EdgeSpec:
|
|
445
|
+
"""One edge a rule emits. ``parts`` is the list form of the end's source.
|
|
446
|
+
|
|
447
|
+
A part is ``("key", path)``, ``("each", path)`` or ``("entry", "")`` for the entry's own key;
|
|
448
|
+
``from: []`` is the one part ``("file", "")``, a template over the path values alone;
|
|
449
|
+
without parts the end reads ``key``. A rule
|
|
450
|
+
that reads data runs from ``this_node`` to that end, and ``finish`` is ``""``. A rule that
|
|
451
|
+
reads a parse tree names each end in ``start`` and ``finish``: ``this_node``,
|
|
452
|
+
``enclosing_class``, ``declared`` for the node of the type ``declared`` holds at that end, or,
|
|
453
|
+
for ``finish``, ``qualified_name`` for each name written in the match. An attribute rule also
|
|
454
|
+
names ``enclosing_method``, and its ``finish`` may be ``attribute_argument`` with the argument
|
|
455
|
+
in ``key``. A reference rule's ``key`` is the position of the call's argument its end reads,
|
|
456
|
+
or ``""`` for the written name.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
kind: str
|
|
460
|
+
key: str
|
|
461
|
+
each: bool
|
|
462
|
+
matches: str | None
|
|
463
|
+
pattern: re.Pattern[str] | None
|
|
464
|
+
template: str | None
|
|
465
|
+
parts: tuple[tuple[str, str], ...]
|
|
466
|
+
start: str = "this_node"
|
|
467
|
+
finish: str = ""
|
|
468
|
+
#: The template begins with a declared id namespace and two colons, so its value is a full id.
|
|
469
|
+
full_id: bool = False
|
|
470
|
+
#: The key path of a list in the same file that must hold the end's value; ``None`` for none.
|
|
471
|
+
listed_at: str | None = None
|
|
472
|
+
#: Globs a list-form end's each key must not match; the end's ``matches`` glob it must.
|
|
473
|
+
not_matches: tuple[str, ...] = ()
|
|
474
|
+
#: The type the composed end has when no rule finds it; ``""`` for the kind's types there.
|
|
475
|
+
#: A data or text rule's composed end is the from end when ``start`` is ``""``.
|
|
476
|
+
to_type: str = ""
|
|
477
|
+
#: The composed end becomes an unresolved node when no rule maps its id.
|
|
478
|
+
on_miss: bool = False
|
|
479
|
+
#: A text rule's end: its resolution steps and their separator.
|
|
480
|
+
resolve: tuple[Step, ...] = ()
|
|
481
|
+
separator: str = ""
|
|
482
|
+
#: The composed end's candidate types, in order, in place of ``to_type``; ``to_type`` is the
|
|
483
|
+
#: first of them.
|
|
484
|
+
types: tuple[str, ...] = ()
|
|
485
|
+
#: The node type of a ``declared`` end, at from and at to; ``""`` at an end that is not one.
|
|
486
|
+
declared: tuple[str, str] = ("", "")
|
|
487
|
+
#: A reference rule's from end written as a template, in place of ``enclosing_declaration``.
|
|
488
|
+
from_template: str | None = None
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@dataclass(frozen=True, slots=True)
|
|
492
|
+
class EdgeKind:
|
|
493
|
+
"""One edge kind: the types allowed at each end."""
|
|
494
|
+
|
|
495
|
+
name: str
|
|
496
|
+
from_types: tuple[str, ...]
|
|
497
|
+
to_types: tuple[str, ...]
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@dataclass(frozen=True, slots=True)
|
|
501
|
+
class BoundaryEntry:
|
|
502
|
+
"""One name a boundary group lists: the pack and group that list it, the group's node type,
|
|
503
|
+
the type that classifies a first-party descendant of it or ``None``, and the edge kinds that
|
|
504
|
+
carry descent toward it."""
|
|
505
|
+
|
|
506
|
+
pack: str
|
|
507
|
+
group: str
|
|
508
|
+
type: str
|
|
509
|
+
classifies: str | None
|
|
510
|
+
ancestry: tuple[str, ...]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@dataclass(frozen=True, slots=True)
|
|
514
|
+
class RuleSet:
|
|
515
|
+
"""Every rule in load order, the table of declared types and the table of edge kinds.
|
|
516
|
+
|
|
517
|
+
``grammars`` holds, for each file type a supported tree rule reads, the grammar of its pack.
|
|
518
|
+
``values`` holds the project's settings values, which every template may name.
|
|
519
|
+
``tables`` holds, for each pack, the tables its table steps may name.
|
|
520
|
+
``boundary`` holds, by node id, each name a boundary group of a loaded pack lists.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
rules: tuple[Rule, ...]
|
|
524
|
+
types: Mapping[str, TypeInfo]
|
|
525
|
+
edge_kinds: Mapping[str, EdgeKind]
|
|
526
|
+
conflicts: tuple[Problem, ...] = ()
|
|
527
|
+
grammars: Mapping[str, Grammar] = field(default_factory=dict)
|
|
528
|
+
files: Mapping[str, FileInfo] = field(default_factory=dict)
|
|
529
|
+
values: Mapping[str, str] = field(default_factory=dict)
|
|
530
|
+
tables: Mapping[str, Mapping[str, Mapping[str, str]]] = field(default_factory=dict)
|
|
531
|
+
boundary: Mapping[str, BoundaryEntry] = field(default_factory=dict)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def load_rules(
|
|
535
|
+
packs: Sequence[LoadedPack],
|
|
536
|
+
values: Mapping[str, str] | None = None,
|
|
537
|
+
origins: Mapping[str, str] | None = None,
|
|
538
|
+
) -> RuleSet:
|
|
539
|
+
"""Read each pack's rule files, in pack order and then file order, and check the types.
|
|
540
|
+
|
|
541
|
+
Raises ``PackRulesUnreadable`` for an unreadable file, a type entry with no string name, a
|
|
542
|
+
type declared twice, and a supported rule that emits a type with no id namespace. An edge kind
|
|
543
|
+
declared twice with different ends is left in ``RuleSet.conflicts``, so the run can report it
|
|
544
|
+
beside the other problems of the packs, and so is a name the settings ``values`` and a pack
|
|
545
|
+
both give, a path value template naming a value nothing gives, and a declaration rule's id
|
|
546
|
+
naming a name that is no field of its grammar and no value, and so is a boundary group whose
|
|
547
|
+
type, classifying type or ancestry kind the run cannot use. ``origins`` names the settings
|
|
548
|
+
file of each settings value. Every pack given has passed its schema.
|
|
549
|
+
"""
|
|
550
|
+
values = dict(values or {})
|
|
551
|
+
types: dict[str, TypeInfo] = {}
|
|
552
|
+
kinds: dict[str, EdgeKind] = {}
|
|
553
|
+
conflicts: list[Problem] = []
|
|
554
|
+
declared_at: dict[str, tuple[str, str]] = {}
|
|
555
|
+
rules: list[Rule] = []
|
|
556
|
+
items: list[Mapping[str, Any]] = []
|
|
557
|
+
sources: list[str] = []
|
|
558
|
+
siblings: dict[str, list[Mapping[str, Any]]] = {}
|
|
559
|
+
imports: dict[str, list[Mapping[str, Any]]] = {}
|
|
560
|
+
tables = _tables(packs)
|
|
561
|
+
for pack in packs:
|
|
562
|
+
for relative, node in _yaml_files(pack.directory):
|
|
563
|
+
if relative == MANIFEST_FILENAME:
|
|
564
|
+
continue
|
|
565
|
+
document, readable = _load(node, [])
|
|
566
|
+
if not readable or not isinstance(document, Mapping):
|
|
567
|
+
raise PackRulesUnreadable(
|
|
568
|
+
f"{pack.name.pack}/{relative} cannot be read as a rule file",
|
|
569
|
+
{"pack": pack.name.pack, "file": relative},
|
|
570
|
+
)
|
|
571
|
+
_read_types(pack.name.pack, document.get("node_types"), types)
|
|
572
|
+
_read_kinds(
|
|
573
|
+
pack.name.pack, relative, document.get("edge_kinds"), kinds, conflicts, declared_at
|
|
574
|
+
)
|
|
575
|
+
imports.setdefault(pack.name.pack, []).extend(document.get("imports", []))
|
|
576
|
+
for item in document.get("rules", []):
|
|
577
|
+
rules.append(_read_rule(pack.name.pack, item, frozenset(tables[pack.name.pack])))
|
|
578
|
+
items.append(item)
|
|
579
|
+
sources.append(relative)
|
|
580
|
+
siblings.setdefault(pack.name.pack, []).append(item)
|
|
581
|
+
prefixes = tuple(f"{t.id_namespace}::" for t in types.values() if t.id_namespace)
|
|
582
|
+
rules = [
|
|
583
|
+
_check_edges(_full_ids(_with_twin(rule, item, siblings[rule.pack]), prefixes), types, kinds)
|
|
584
|
+
for rule, item in zip(rules, items, strict=True)
|
|
585
|
+
]
|
|
586
|
+
file_types = FileTypes.build(packs, {})
|
|
587
|
+
rules = [_check_ending(_check_reader(rule, file_types), file_types) for rule in rules]
|
|
588
|
+
files = _file_info(packs)
|
|
589
|
+
rules = [_check_names(rule, files.get(rule.filetype, FileInfo()), values) for rule in rules]
|
|
590
|
+
conflicts.extend(_collisions(rules, sources, files, packs, values, origins or {}))
|
|
591
|
+
grammars: dict[str, Grammar] = {}
|
|
592
|
+
for index, rule in enumerate(rules):
|
|
593
|
+
tree = rule.declaration or rule.attribute or rule.annotation or rule.reference
|
|
594
|
+
if rule.supported and tree and rule.filetype not in grammars:
|
|
595
|
+
try:
|
|
596
|
+
grammars[rule.filetype] = _grammar(rule.filetype, packs, imports)
|
|
597
|
+
except _Unsupported as reason:
|
|
598
|
+
rules[index] = replace(rule, supported=False, reason=str(reason))
|
|
599
|
+
if rules[index].supported and rule.declaration:
|
|
600
|
+
conflicts.extend(_unknown_fields(rule, sources[index], grammars[rule.filetype]))
|
|
601
|
+
if rules[index].supported and rule.attribute and not grammars[rule.filetype].attribute:
|
|
602
|
+
missing = f"the grammar for {rule.filetype} declares no identity.attribute block"
|
|
603
|
+
rules[index] = replace(rule, supported=False, reason=missing)
|
|
604
|
+
if rules[index].supported and rule.annotation and not grammars[rule.filetype].comment:
|
|
605
|
+
missing = f"the grammar for {rule.filetype} declares no identity.comment"
|
|
606
|
+
rules[index] = replace(rule, supported=False, reason=missing)
|
|
607
|
+
for rule in rules:
|
|
608
|
+
if (
|
|
609
|
+
rule.supported
|
|
610
|
+
and rule.node_type in types
|
|
611
|
+
and namespace_of(types, rule.node_type) is None
|
|
612
|
+
):
|
|
613
|
+
raise PackRulesUnreadable(
|
|
614
|
+
f"rule {rule.name} of {rule.pack} emits {rule.node_type}, "
|
|
615
|
+
"which no loaded pack declares with an id_namespace",
|
|
616
|
+
{"pack": rule.pack, "rule": rule.name, "type": rule.node_type},
|
|
617
|
+
)
|
|
618
|
+
boundary = _boundary_entries(packs, types, kinds, conflicts)
|
|
619
|
+
return RuleSet(
|
|
620
|
+
rules=tuple(rules),
|
|
621
|
+
types=types,
|
|
622
|
+
edge_kinds=kinds,
|
|
623
|
+
conflicts=tuple(conflicts),
|
|
624
|
+
grammars=grammars,
|
|
625
|
+
files=files,
|
|
626
|
+
values=values,
|
|
627
|
+
tables=tables,
|
|
628
|
+
boundary=boundary,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _tables(packs: Sequence[LoadedPack]) -> dict[str, dict[str, Mapping[str, str]]]:
|
|
633
|
+
"""The tables of each pack and of every pack it depends on, directly or not; of two tables of
|
|
634
|
+
one name, the one of the pack nearer in the walk from the pack wins."""
|
|
635
|
+
declared: dict[str, Mapping[str, Mapping[str, str]]] = {}
|
|
636
|
+
for p in packs:
|
|
637
|
+
own = p.manifest.document.get("tables")
|
|
638
|
+
declared[p.name.pack] = own if isinstance(own, Mapping) else {}
|
|
639
|
+
found: dict[str, dict[str, Mapping[str, str]]] = {}
|
|
640
|
+
for pack, walk in _visible_packs(packs).items():
|
|
641
|
+
visible: dict[str, Mapping[str, str]] = {}
|
|
642
|
+
for name in walk:
|
|
643
|
+
for table, entries in declared.get(name, {}).items():
|
|
644
|
+
visible.setdefault(table, entries)
|
|
645
|
+
found[pack] = visible
|
|
646
|
+
return found
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _visible_packs(packs: Sequence[LoadedPack]) -> dict[str, list[str]]:
|
|
650
|
+
"""Each pack's name, then the name of every pack it depends on, directly or not, nearer
|
|
651
|
+
first."""
|
|
652
|
+
depends = {p.name.pack: [d.partition("@")[0] for d in p.manifest.depends] for p in packs}
|
|
653
|
+
found: dict[str, list[str]] = {}
|
|
654
|
+
for pack in depends:
|
|
655
|
+
seen: list[str] = []
|
|
656
|
+
pending = [pack]
|
|
657
|
+
while pending:
|
|
658
|
+
name = pending.pop(0)
|
|
659
|
+
if name not in seen:
|
|
660
|
+
seen.append(name)
|
|
661
|
+
pending.extend(depends.get(name, []))
|
|
662
|
+
found[pack] = seen
|
|
663
|
+
return found
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _boundary(packs: Sequence[LoadedPack]) -> dict[str, Mapping[str, Mapping[str, Any]]]:
|
|
667
|
+
"""The boundary groups each pack declares, by pack. No rule names a group, so no group hides
|
|
668
|
+
another of the same name."""
|
|
669
|
+
declared: dict[str, Mapping[str, Mapping[str, Any]]] = {}
|
|
670
|
+
for p in packs:
|
|
671
|
+
own = p.manifest.document.get("boundary")
|
|
672
|
+
declared[p.name.pack] = own if isinstance(own, Mapping) else {}
|
|
673
|
+
return declared
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
#: The boundary block's working shape, named by each refusal of a group.
|
|
677
|
+
_BOUNDARY_SHAPE = (
|
|
678
|
+
"boundary: {<group>: {type: <node type>, ancestry: [<edge kind>], "
|
|
679
|
+
"names: {<name>: <node type or ~>}}}"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _boundary_entries(
|
|
684
|
+
packs: Sequence[LoadedPack],
|
|
685
|
+
types: Mapping[str, TypeInfo],
|
|
686
|
+
kinds: Mapping[str, EdgeKind],
|
|
687
|
+
conflicts: list[Problem],
|
|
688
|
+
) -> dict[str, BoundaryEntry]:
|
|
689
|
+
"""Each name of every group a loaded pack declares, by node id.
|
|
690
|
+
|
|
691
|
+
A group whose type no pack the declaring pack sees declares with an id namespace, whose names
|
|
692
|
+
value no such pack declares, or whose ancestry names a kind no pack declares is left out, with
|
|
693
|
+
a problem in ``conflicts``. An id two packs list adds a problem too: which pack draws it is
|
|
694
|
+
precedence, not decided here.
|
|
695
|
+
"""
|
|
696
|
+
declared = _boundary(packs)
|
|
697
|
+
found: dict[str, BoundaryEntry] = {}
|
|
698
|
+
for pack, walk in _visible_packs(packs).items():
|
|
699
|
+
for group, spec in declared[pack].items():
|
|
700
|
+
kind = str(spec["type"])
|
|
701
|
+
namespace = namespace_of(types, kind)
|
|
702
|
+
reasons: list[str] = []
|
|
703
|
+
if kind not in types or types[kind].pack not in walk or namespace is None:
|
|
704
|
+
reasons.append(
|
|
705
|
+
f"the type {kind}, which neither {pack} nor a pack it depends on declares "
|
|
706
|
+
"with an id_namespace"
|
|
707
|
+
)
|
|
708
|
+
else:
|
|
709
|
+
reasons.extend(
|
|
710
|
+
f"the names value {value} for {name}, which neither {pack} nor a pack it "
|
|
711
|
+
"depends on declares"
|
|
712
|
+
for name, value in spec["names"].items()
|
|
713
|
+
if value is not None and (value not in types or types[value].pack not in walk)
|
|
714
|
+
)
|
|
715
|
+
reasons.extend(
|
|
716
|
+
f"the ancestry kind {edge}, which no loaded pack declares"
|
|
717
|
+
for edge in spec["ancestry"]
|
|
718
|
+
if edge not in kinds
|
|
719
|
+
)
|
|
720
|
+
for reason in reasons:
|
|
721
|
+
conflicts.append(
|
|
722
|
+
Problem(
|
|
723
|
+
ExitCode.RULE_NOT_EXECUTABLE,
|
|
724
|
+
f"{pack}/{MANIFEST_FILENAME}: the boundary group {group} names {reason}; "
|
|
725
|
+
f"write {_BOUNDARY_SHAPE}",
|
|
726
|
+
{"pack": pack, "file": MANIFEST_FILENAME, "group": str(group)},
|
|
727
|
+
)
|
|
728
|
+
)
|
|
729
|
+
if reasons:
|
|
730
|
+
continue
|
|
731
|
+
for name, value in spec["names"].items():
|
|
732
|
+
node = f"{namespace}::{name}"
|
|
733
|
+
first = found.setdefault(
|
|
734
|
+
node, BoundaryEntry(pack, str(group), kind, value, tuple(spec["ancestry"]))
|
|
735
|
+
)
|
|
736
|
+
if first.pack != pack:
|
|
737
|
+
conflicts.append(
|
|
738
|
+
Problem(
|
|
739
|
+
ExitCode.RULE_NOT_EXECUTABLE,
|
|
740
|
+
f"{pack}/{MANIFEST_FILENAME}: the boundary group {group} names {node}, "
|
|
741
|
+
f"which the pack {first.pack} lists too in its group {first.group}; "
|
|
742
|
+
f"list it in one pack and write {_BOUNDARY_SHAPE}",
|
|
743
|
+
{"pack": pack, "file": MANIFEST_FILENAME, "group": str(group)},
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
return found
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _file_info(packs: Sequence[LoadedPack]) -> dict[str, FileInfo]:
|
|
750
|
+
"""Each declared file type's path values, sections and spans, by type name.
|
|
751
|
+
|
|
752
|
+
Raises ``PackRulesUnreadable`` for a pattern that does not compile, a span whose balance is not
|
|
753
|
+
two different characters, and a ``relative_to`` naming a folder no loaded pack declares or
|
|
754
|
+
whose value holds ``**``, naming the pack.
|
|
755
|
+
"""
|
|
756
|
+
found: dict[str, FileInfo] = {}
|
|
757
|
+
for pack in packs:
|
|
758
|
+
files = pack.manifest.document.get("files")
|
|
759
|
+
declared = files.get("types") if isinstance(files, Mapping) else None
|
|
760
|
+
for name, spec in declared.items() if isinstance(declared, Mapping) else ():
|
|
761
|
+
try:
|
|
762
|
+
values = tuple(
|
|
763
|
+
_read_value(str(key), value, path=True)
|
|
764
|
+
for key, value in spec.get("path_values", {}).items()
|
|
765
|
+
)
|
|
766
|
+
sections = tuple(
|
|
767
|
+
(str(s["name"]), _regex(s["open"]), _closing(s["close"], s["open"]))
|
|
768
|
+
for s in spec.get("sections", [])
|
|
769
|
+
)
|
|
770
|
+
steps = _read_steps(spec.get("resolve", []))
|
|
771
|
+
spans = tuple(
|
|
772
|
+
(str(s["name"]), _regex(s["open"]), *_balance(s["balance"]))
|
|
773
|
+
for s in spec.get("spans", [])
|
|
774
|
+
)
|
|
775
|
+
for value in values:
|
|
776
|
+
_flat_folder(value, packs)
|
|
777
|
+
except _Unsupported as reason:
|
|
778
|
+
raise PackRulesUnreadable(
|
|
779
|
+
f"{pack.name.pack}/{MANIFEST_FILENAME}: the file type {name}: {reason}",
|
|
780
|
+
{"pack": pack.name.pack, "file": MANIFEST_FILENAME, "name": str(name)},
|
|
781
|
+
) from reason
|
|
782
|
+
known = found.get(name, FileInfo())
|
|
783
|
+
found[name] = FileInfo(
|
|
784
|
+
known.path_values + values,
|
|
785
|
+
known.sections + sections,
|
|
786
|
+
known.resolve or steps,
|
|
787
|
+
known.separator if known.resolve else str(spec.get("separator", "")),
|
|
788
|
+
known.spans + spans,
|
|
789
|
+
)
|
|
790
|
+
return found
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def _balance(pair: object) -> tuple[str, str]:
|
|
794
|
+
"""A span's opening and closing characters."""
|
|
795
|
+
if not (
|
|
796
|
+
isinstance(pair, list)
|
|
797
|
+
and len(pair) == 2
|
|
798
|
+
and all(isinstance(c, str) and len(c) == 1 for c in pair)
|
|
799
|
+
and pair[0] != pair[1]
|
|
800
|
+
):
|
|
801
|
+
raise _Unsupported("a span's balance is not two different characters")
|
|
802
|
+
return pair[0], pair[1]
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _flat_folder(value: Value, packs: Sequence[LoadedPack]) -> None:
|
|
806
|
+
"""Refuse a ``relative_to`` naming a folder no loaded pack declares, or whose value holds
|
|
807
|
+
``**``: the deepest directory below it would win over the folder itself."""
|
|
808
|
+
if not value.relative_to:
|
|
809
|
+
return
|
|
810
|
+
key = f"path_values.{value.name}.relative_to"
|
|
811
|
+
try:
|
|
812
|
+
deep = holds_double_star(value.relative_to, packs, {})
|
|
813
|
+
except FolderUnresolved as error:
|
|
814
|
+
raise _Unsupported(f"{key} names the folder {value.relative_to}: {error}") from error
|
|
815
|
+
if deep:
|
|
816
|
+
raise _Unsupported(f"{key} names the folder {value.relative_to}, whose value holds **")
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _closing(close: object, opening: object) -> str:
|
|
820
|
+
"""The closing template, checked to compile with each placeholder of a named capture of the
|
|
821
|
+
opening pattern standing for empty text."""
|
|
822
|
+
groups = _regex(opening).groupindex
|
|
823
|
+
if not isinstance(close, str):
|
|
824
|
+
raise _Unsupported("a section's close is not a string")
|
|
825
|
+
_regex(PLACEHOLDER.sub(lambda p: "" if p.group(1) in groups else p.group(0), close))
|
|
826
|
+
return close
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def _check_names(rule: Rule, info: FileInfo, values: Mapping[str, str]) -> Rule:
|
|
830
|
+
"""The rule, marked not executed when a template names a value nothing gives, a value's
|
|
831
|
+
template names a value defined after it, or the match reads a section its file type does not
|
|
832
|
+
declare. A declaration rule is given the fields its id names."""
|
|
833
|
+
tree = rule.declaration or rule.attribute or rule.annotation or rule.reference
|
|
834
|
+
known = {value.name for value in info.path_values} | set(values)
|
|
835
|
+
if rule.supported and rule.declaration:
|
|
836
|
+
captures = set(compile_pattern(PATTERN_VALUE.sub("", rule.id_pattern)).groupindex)
|
|
837
|
+
clash = captures & (_TREE_FILLED | known)
|
|
838
|
+
unknown = set(PATTERN_VALUE.findall(rule.id_pattern)) - known
|
|
839
|
+
if clash or unknown:
|
|
840
|
+
reason = (
|
|
841
|
+
f"the id pattern captures {', '.join(sorted(clash))}, a name the engine, a path "
|
|
842
|
+
"value or a settings value gives"
|
|
843
|
+
if clash
|
|
844
|
+
else f"the id pattern names {', '.join(sorted(unknown))}, which is no path value "
|
|
845
|
+
"or settings value"
|
|
846
|
+
)
|
|
847
|
+
return replace(rule, supported=False, reason=reason)
|
|
848
|
+
named = {*PLACEHOLDER.findall(rule.id_template), rule.id_source} - {""}
|
|
849
|
+
named = named - _TREE_FILLED - known - captures - set(_groups(rule.docblock))
|
|
850
|
+
if set(rule.qualified) - named:
|
|
851
|
+
lacking = ", ".join(sorted(set(rule.qualified) - named))
|
|
852
|
+
reason = f"the id source qualified names {lacking}, which is no field of the match"
|
|
853
|
+
return replace(rule, supported=False, reason=reason)
|
|
854
|
+
return replace(rule, fields=tuple(sorted(named)))
|
|
855
|
+
if rule.supported and rule.matches_type:
|
|
856
|
+
return _check_map_names(rule, values)
|
|
857
|
+
if not rule.supported or tree:
|
|
858
|
+
return rule
|
|
859
|
+
if rule.text is not None:
|
|
860
|
+
before = known | set(rule.text.groupindex)
|
|
861
|
+
for value in rule.values:
|
|
862
|
+
later = set(PLACEHOLDER.findall(value.template or "")) - before
|
|
863
|
+
if later:
|
|
864
|
+
missing = ", ".join(sorted(later))
|
|
865
|
+
reason = (
|
|
866
|
+
f"the value {value.name} names {missing}, which is no capture, path value, "
|
|
867
|
+
"settings value or value defined before it"
|
|
868
|
+
)
|
|
869
|
+
return replace(rule, supported=False, reason=reason)
|
|
870
|
+
before.add(value.name)
|
|
871
|
+
known |= {*rule.text.groupindex, *(value.name for value in rule.values)}
|
|
872
|
+
templates = [rule.id_template, *(v.template or "" for v in rule.values)]
|
|
873
|
+
templates += [t for _, t in rule.templates] + [e.template or "" for e in rule.edges]
|
|
874
|
+
templates += [t for binding in rule.bindings for t in binding]
|
|
875
|
+
else:
|
|
876
|
+
known |= {"file_stem", *(("key",) if rule.each is not None else ())}
|
|
877
|
+
templates = [rule.id_template]
|
|
878
|
+
names = {name for template in templates for name in PLACEHOLDER.findall(template)}
|
|
879
|
+
if names - known:
|
|
880
|
+
missing = ", ".join(sorted(names - known))
|
|
881
|
+
return replace(
|
|
882
|
+
rule,
|
|
883
|
+
supported=False,
|
|
884
|
+
reason=f"a template names {missing}, which is "
|
|
885
|
+
"no capture, value, path value or settings value the rule has",
|
|
886
|
+
)
|
|
887
|
+
if rule.text is not None:
|
|
888
|
+
steps = [*rule.id_resolve, *(step for edge in rule.edges for step in edge.resolve)]
|
|
889
|
+
prefixing = ("prefix_with", "prefix_bare_with")
|
|
890
|
+
named = {argument for kind, argument in steps if kind in prefixing} - known
|
|
891
|
+
if any(not edge.resolve for edge in rule.edges):
|
|
892
|
+
paths = {value.name for value in info.path_values} | set(values)
|
|
893
|
+
named |= {argument for kind, argument in info.resolve if kind in prefixing} - paths
|
|
894
|
+
if named:
|
|
895
|
+
return replace(
|
|
896
|
+
rule,
|
|
897
|
+
supported=False,
|
|
898
|
+
reason=f"a prefix_with step names {', '.join(sorted(named))}, which is "
|
|
899
|
+
"no capture, value, path value or settings value the rule has",
|
|
900
|
+
)
|
|
901
|
+
sections = {name for name, _, _ in info.sections}
|
|
902
|
+
if set(rule.reads_sections) - sections:
|
|
903
|
+
missing = ", ".join(sorted(set(rule.reads_sections) - sections))
|
|
904
|
+
reason = f"match.reads_sections names {missing}, which its file type does not declare"
|
|
905
|
+
return replace(rule, supported=False, reason=reason)
|
|
906
|
+
captured = {(name, group) for name, opening, _, _ in info.spans for group in opening.groupindex}
|
|
907
|
+
for value in rule.values:
|
|
908
|
+
if value.enclosing is not None and value.enclosing not in captured:
|
|
909
|
+
span, capture = value.enclosing
|
|
910
|
+
reason = (
|
|
911
|
+
f"the value {value.name} names the capture {capture} of the span {span}, which "
|
|
912
|
+
"its file type does not declare"
|
|
913
|
+
)
|
|
914
|
+
return replace(rule, supported=False, reason=reason)
|
|
915
|
+
return rule
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _check_map_names(rule: Rule, values: Mapping[str, str]) -> Rule:
|
|
919
|
+
"""The map rule, marked not executed when a follow step's node template names a name that no
|
|
920
|
+
id_matches before it captures and no settings value gives, or its id or an edge template names
|
|
921
|
+
one that no id_matches of the rule captures and no settings value gives."""
|
|
922
|
+
known = set(values)
|
|
923
|
+
for condition in rule.where:
|
|
924
|
+
known |= set(condition.pattern.groupindex) if isinstance(condition, IdMatches) else set()
|
|
925
|
+
for name, value in rule.follow:
|
|
926
|
+
if name == "id_matches":
|
|
927
|
+
known |= set(value.groupindex)
|
|
928
|
+
elif name == "node" and set(PLACEHOLDER.findall(value)) - known:
|
|
929
|
+
lacking = ", ".join(sorted(set(PLACEHOLDER.findall(value)) - known))
|
|
930
|
+
reason = f"the follow step node {value} names {lacking}, which no id_matches captures"
|
|
931
|
+
return replace(rule, supported=False, reason=reason + " before it")
|
|
932
|
+
templates = [rule.id_template, *(edge.template or "" for edge in rule.edges)]
|
|
933
|
+
missing = sorted({name for t in templates for name in PLACEHOLDER.findall(t)} - known)
|
|
934
|
+
if missing:
|
|
935
|
+
reason = f"a template names {', '.join(missing)}, which no id_matches of the rule captures"
|
|
936
|
+
return replace(rule, supported=False, reason=reason)
|
|
937
|
+
return rule
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _unknown_fields(rule: Rule, source: str, grammar: Grammar) -> list[Problem]:
|
|
941
|
+
"""A problem for each name the declaration rule's id reads as a node field that is no field of
|
|
942
|
+
the grammar, naming the rule, the template and the name. A grammar that cannot be imported
|
|
943
|
+
is left to the grammar check."""
|
|
944
|
+
try:
|
|
945
|
+
language = _parser(grammar.language).language
|
|
946
|
+
except Exception: # noqa: BLE001 - any failure of a grammar's import, reported elsewhere
|
|
947
|
+
language = None
|
|
948
|
+
if language is None:
|
|
949
|
+
return []
|
|
950
|
+
return [
|
|
951
|
+
Problem(
|
|
952
|
+
ExitCode.PACK_RULES_UNREADABLE,
|
|
953
|
+
f"{rule.pack}/{source}: the rule {rule.name} has the id template "
|
|
954
|
+
f"{rule.id_template}, which names {name}, no field of the {grammar.language} "
|
|
955
|
+
"grammar, path value or settings value",
|
|
956
|
+
{"pack": rule.pack, "file": source, "rule": rule.name, "name": name},
|
|
957
|
+
)
|
|
958
|
+
for name in rule.fields
|
|
959
|
+
if language.field_id_for_name(name) is None
|
|
960
|
+
]
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _collisions(
|
|
964
|
+
rules: Sequence[Rule],
|
|
965
|
+
sources: Sequence[str],
|
|
966
|
+
files: Mapping[str, FileInfo],
|
|
967
|
+
packs: Sequence[LoadedPack],
|
|
968
|
+
values: Mapping[str, str],
|
|
969
|
+
origins: Mapping[str, str],
|
|
970
|
+
) -> list[Problem]:
|
|
971
|
+
"""Each path value name that the engine fills already (on a tree file type, also the names it
|
|
972
|
+
fills for a declaration rule and ``line``), that a rule on its file type captures or names as
|
|
973
|
+
a value, or that a settings value gives; each settings value name the engine fills, or a rule
|
|
974
|
+
captures or names as a value; and each path value template naming a name that is no capture
|
|
975
|
+
of its pattern and no settings value, or both: a problem naming the pack, the file and the
|
|
976
|
+
names, and the settings file of a settings value."""
|
|
977
|
+
problems: list[Problem] = []
|
|
978
|
+
tree_filled = {"file_stem", "key", "line", *_TREE_FILLED}
|
|
979
|
+
|
|
980
|
+
def setting(name: str) -> str:
|
|
981
|
+
origin = f" of {origins[name]}" if name in origins else ""
|
|
982
|
+
return f"the settings value {name}{origin}"
|
|
983
|
+
|
|
984
|
+
for pack in packs:
|
|
985
|
+
files_block = pack.manifest.document.get("files")
|
|
986
|
+
declared = files_block.get("types") if isinstance(files_block, Mapping) else None
|
|
987
|
+
for kind, spec in declared.items() if isinstance(declared, Mapping) else ():
|
|
988
|
+
path_values = spec.get("path_values", {}) if isinstance(spec, Mapping) else {}
|
|
989
|
+
tree = isinstance(spec, Mapping) and spec.get("reader") == "tree"
|
|
990
|
+
for name, value in path_values.items():
|
|
991
|
+
if name in (tree_filled if tree else {"file_stem", "key"}):
|
|
992
|
+
problems.append(_collision(pack.name.pack, MANIFEST_FILENAME, name, kind))
|
|
993
|
+
where = f"{pack.name.pack}/{MANIFEST_FILENAME}: the path value {name} of the file "
|
|
994
|
+
detail = {"pack": pack.name.pack, "file": MANIFEST_FILENAME, "name": name}
|
|
995
|
+
if name in values:
|
|
996
|
+
message = f"{where}type {kind} has the name of {setting(name)}"
|
|
997
|
+
problems.append(Problem(ExitCode.PACK_RULES_UNREADABLE, message, detail))
|
|
998
|
+
captures = set(compile_pattern(value["pattern"]).groupindex)
|
|
999
|
+
named = set(PLACEHOLDER.findall(value["template"]))
|
|
1000
|
+
for missing in sorted(named - captures - set(values)):
|
|
1001
|
+
message = (
|
|
1002
|
+
f"{where}type {kind} has the template {value['template']}, which names "
|
|
1003
|
+
f"{missing}, no capture of its pattern and no settings value"
|
|
1004
|
+
)
|
|
1005
|
+
problems.append(Problem(ExitCode.PACK_RULES_UNREADABLE, message, detail))
|
|
1006
|
+
for both in sorted(named & captures & set(values)):
|
|
1007
|
+
message = (
|
|
1008
|
+
f"{where}type {kind} has the template {value['template']}, which names "
|
|
1009
|
+
f"{both}, both a capture of its pattern and a settings value"
|
|
1010
|
+
)
|
|
1011
|
+
problems.append(Problem(ExitCode.PACK_RULES_UNREADABLE, message, detail))
|
|
1012
|
+
for rule, source in zip(rules, sources, strict=True):
|
|
1013
|
+
names = {value.name for value in files.get(rule.filetype, FileInfo()).path_values}
|
|
1014
|
+
given = {*(rule.text.groupindex if rule.text is not None else ())}
|
|
1015
|
+
given |= {value.name for value in rule.values}
|
|
1016
|
+
problems.extend(
|
|
1017
|
+
_collision(rule.pack, source, name, rule.filetype) for name in sorted(names & given)
|
|
1018
|
+
)
|
|
1019
|
+
problems.extend(
|
|
1020
|
+
Problem(
|
|
1021
|
+
ExitCode.PACK_RULES_UNREADABLE,
|
|
1022
|
+
f"{rule.pack}/{source}: the rule {rule.name} captures or names the value {name}, "
|
|
1023
|
+
f"which is also {setting(name)}",
|
|
1024
|
+
{"pack": rule.pack, "file": source, "name": name},
|
|
1025
|
+
)
|
|
1026
|
+
for name in sorted(set(values) & given)
|
|
1027
|
+
)
|
|
1028
|
+
problems.extend(
|
|
1029
|
+
Problem(
|
|
1030
|
+
ExitCode.PACK_RULES_UNREADABLE,
|
|
1031
|
+
f"{setting(name)} has a name the engine fills",
|
|
1032
|
+
{"name": name},
|
|
1033
|
+
)
|
|
1034
|
+
for name in sorted(set(values) & tree_filled)
|
|
1035
|
+
)
|
|
1036
|
+
return problems
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _collision(pack: str, file: str, name: str, kind: str) -> Problem:
|
|
1040
|
+
return Problem(
|
|
1041
|
+
ExitCode.PACK_RULES_UNREADABLE,
|
|
1042
|
+
f"{pack}/{file}: the path value {name} of the file type {kind} is a name the engine or "
|
|
1043
|
+
"a rule already fills",
|
|
1044
|
+
{"pack": pack, "file": file, "name": name},
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _check_reader(rule: Rule, types: FileTypes) -> Rule:
|
|
1049
|
+
"""The rule, marked not executed when every type of its file type is read otherwise than the
|
|
1050
|
+
rule reads: a text rule text, a tree rule a tree, any other rule that reads content data. A
|
|
1051
|
+
file type no pack declares is read as data."""
|
|
1052
|
+
if rule.text is not None:
|
|
1053
|
+
needed = "text"
|
|
1054
|
+
elif rule.declaration or rule.attribute or rule.annotation or rule.reference:
|
|
1055
|
+
needed = "tree"
|
|
1056
|
+
elif rule.where or rule.attributes or rule.edges or rule.each is not None:
|
|
1057
|
+
needed = "data"
|
|
1058
|
+
else:
|
|
1059
|
+
return rule
|
|
1060
|
+
readers = {kind.reader for kind in types.named(rule.filetype)} or {"data"}
|
|
1061
|
+
if not rule.supported or needed in readers:
|
|
1062
|
+
return rule
|
|
1063
|
+
reason = f"the rule reads {needed}, and the file type {rule.filetype} is read as "
|
|
1064
|
+
return replace(rule, supported=False, reason=reason + ", ".join(sorted(readers)))
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _check_ending(rule: Rule, types: FileTypes) -> Rule:
|
|
1068
|
+
"""The rule, marked not executed when its ``match.ending`` names an ending no pack claims."""
|
|
1069
|
+
claimed = sorted({ending for kind in types.types for ending in kind.endings})
|
|
1070
|
+
unclaimed = [ending for ending in rule.endings if ending not in claimed]
|
|
1071
|
+
if not rule.supported or not unclaimed:
|
|
1072
|
+
return rule
|
|
1073
|
+
reason = f"match.ending names {', '.join(unclaimed)}, which no pack claims; the claimed "
|
|
1074
|
+
return replace(rule, supported=False, reason=reason + f"endings are {', '.join(claimed)}")
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _grammar(
|
|
1078
|
+
filetype: str, packs: Sequence[LoadedPack], imports: Mapping[str, list[Mapping[str, Any]]]
|
|
1079
|
+
) -> Grammar:
|
|
1080
|
+
"""The grammar of the pack that declares the file type read as a tree, and pins one.
|
|
1081
|
+
|
|
1082
|
+
Its ``identity`` block, an open object in the manifest schema, gives the full-name parts and
|
|
1083
|
+
separator, the namespace declaration and its name field, and the written-name node types. The
|
|
1084
|
+
class constant keys come from the ``grammar`` block of the first pack that reads the file type
|
|
1085
|
+
and declares them; the manifest schema refuses one without the other.
|
|
1086
|
+
"""
|
|
1087
|
+
owners = {kind.pack for kind in FileTypes.build(packs, {}).named(filetype)}
|
|
1088
|
+
constants: Mapping[str, Any] = next(
|
|
1089
|
+
(
|
|
1090
|
+
block
|
|
1091
|
+
for pack in packs
|
|
1092
|
+
for block in [pack.manifest.document.get("grammar")]
|
|
1093
|
+
if pack.name.pack in owners
|
|
1094
|
+
and isinstance(block, Mapping)
|
|
1095
|
+
and "constant_access" in block
|
|
1096
|
+
),
|
|
1097
|
+
{},
|
|
1098
|
+
)
|
|
1099
|
+
for pack in packs:
|
|
1100
|
+
document = pack.manifest.document
|
|
1101
|
+
grammar, identity = document.get("grammar"), document.get("identity")
|
|
1102
|
+
if pack.name.pack not in owners or not isinstance(grammar, Mapping):
|
|
1103
|
+
continue
|
|
1104
|
+
unread = _Unsupported(
|
|
1105
|
+
f"{pack.name.pack}/{MANIFEST_FILENAME} declares the grammar for {filetype}, and "
|
|
1106
|
+
"its identity block or an imports entry lacks a key the engine reads: "
|
|
1107
|
+
"qualified_name.parts and .separator, namespace.declaration and .name_child or "
|
|
1108
|
+
"namespace.path_value, "
|
|
1109
|
+
"written_names, an import's ast, clause and facts with a bind naming only its "
|
|
1110
|
+
"facts, last_segment or path, an optional attribute mapping, an optional comment "
|
|
1111
|
+
"and optional resolution steps"
|
|
1112
|
+
)
|
|
1113
|
+
if not isinstance(identity, Mapping):
|
|
1114
|
+
raise unread
|
|
1115
|
+
try:
|
|
1116
|
+
qualified, namespace = identity["qualified_name"], identity["namespace"]
|
|
1117
|
+
attribute = identity.get("attribute", {})
|
|
1118
|
+
access = constants.get("constant_access", {})
|
|
1119
|
+
declaration = constants.get("constant_declaration", {})
|
|
1120
|
+
if "path_value" in namespace:
|
|
1121
|
+
namespace = {"declaration": "", "name_child": "", **namespace}
|
|
1122
|
+
return Grammar(
|
|
1123
|
+
language=str(grammar["language"]),
|
|
1124
|
+
parts=tuple(str(part) for part in qualified["parts"]),
|
|
1125
|
+
separator=str(qualified["separator"]),
|
|
1126
|
+
namespace=str(namespace["declaration"]),
|
|
1127
|
+
namespace_name=str(namespace["name_child"]),
|
|
1128
|
+
namespace_value=str(namespace.get("path_value", "")),
|
|
1129
|
+
written=frozenset(str(name) for name in identity["written_names"]),
|
|
1130
|
+
imports=tuple(_import_rule(entry) for entry in imports.get(pack.name.pack, [])),
|
|
1131
|
+
attribute=str(attribute.get("node", "")),
|
|
1132
|
+
attribute_list=str(attribute.get("list", "")),
|
|
1133
|
+
arguments=str(attribute.get("arguments", "")),
|
|
1134
|
+
argument=str(attribute.get("argument", "")),
|
|
1135
|
+
argument_name=str(attribute.get("argument_name", "")),
|
|
1136
|
+
literals=frozenset(str(name) for name in attribute.get("literals", [])),
|
|
1137
|
+
literal_content=str(attribute.get("literal_content", "")),
|
|
1138
|
+
comment=str(identity.get("comment", "")),
|
|
1139
|
+
resolve=_read_steps(identity.get("resolve", [])),
|
|
1140
|
+
call_arguments=str(identity.get("call_arguments", "arguments")),
|
|
1141
|
+
call_argument=str(identity.get("call_argument", "")),
|
|
1142
|
+
call_argument_name=str(identity.get("call_argument_name", "")),
|
|
1143
|
+
call_literals=frozenset(map(str, identity.get("call_literals", []))),
|
|
1144
|
+
call_literal_content=frozenset(map(str, identity.get("call_literal_content", []))),
|
|
1145
|
+
call_literal_delimiters=frozenset(
|
|
1146
|
+
map(str, identity.get("call_literal_delimiters", []))
|
|
1147
|
+
),
|
|
1148
|
+
constant_access=str(access.get("node", "")),
|
|
1149
|
+
constant_scope=frozenset(map(str, access.get("scope", []))),
|
|
1150
|
+
constant_name=frozenset(map(str, access.get("name", []))),
|
|
1151
|
+
constant_enclosing=frozenset(map(str, access.get("enclosing", []))),
|
|
1152
|
+
constant_declaration=str(declaration.get("node", "")),
|
|
1153
|
+
constant_declared_name=frozenset(map(str, declaration.get("name", []))),
|
|
1154
|
+
constant_value=frozenset(map(str, declaration.get("value", []))),
|
|
1155
|
+
)
|
|
1156
|
+
except (KeyError, TypeError) as error:
|
|
1157
|
+
raise unread from error
|
|
1158
|
+
raise _Unsupported(f"no loaded pack declares a grammar for the file type {filetype}")
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def _import_rule(entry: Mapping[str, Any]) -> ImportRule:
|
|
1162
|
+
"""One ``imports`` entry. Raises ``KeyError`` when its ``bind`` names a fact it lacks."""
|
|
1163
|
+
bind, facts = entry.get("bind", {}), {str(k): str(v) for k, v in entry["facts"].items()}
|
|
1164
|
+
rule = ImportRule(
|
|
1165
|
+
declaration=entry["ast"],
|
|
1166
|
+
group=entry.get("group_ast"),
|
|
1167
|
+
clause=entry["clause"],
|
|
1168
|
+
facts=tuple(facts.items()),
|
|
1169
|
+
names=tuple(str(source) for source in bind.get("name", [])),
|
|
1170
|
+
prefix_from_group=bind.get("prefix_from_group") is True,
|
|
1171
|
+
strip_prefix=str(bind.get("strip_prefix", "")),
|
|
1172
|
+
skip_when=str(bind.get("skip_when_field_set", "")),
|
|
1173
|
+
normalize=_read_normalize(bind["normalize"]) if "normalize" in bind else (),
|
|
1174
|
+
unbound_aliases=frozenset(str(alias) for alias in bind.get("unbound_aliases", [])),
|
|
1175
|
+
)
|
|
1176
|
+
for fact in (*rule.names, *([rule.skip_when] if rule.skip_when else [])):
|
|
1177
|
+
if fact not in ("last_segment", "path") and fact not in facts:
|
|
1178
|
+
raise KeyError(fact)
|
|
1179
|
+
return rule
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def namespace_of(types: Mapping[str, TypeInfo], name: str) -> str | None:
|
|
1183
|
+
"""The type's own id namespace, or the nearest ancestor's along ``parent``."""
|
|
1184
|
+
seen: set[str] = set()
|
|
1185
|
+
while name in types and name not in seen:
|
|
1186
|
+
seen.add(name)
|
|
1187
|
+
if types[name].id_namespace is not None:
|
|
1188
|
+
return types[name].id_namespace
|
|
1189
|
+
parent = types[name].parent
|
|
1190
|
+
if parent is None:
|
|
1191
|
+
return None
|
|
1192
|
+
name = parent
|
|
1193
|
+
return None
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def namespaces(types: Mapping[str, TypeInfo], names: Sequence[str]) -> list[str]:
|
|
1197
|
+
"""The distinct id namespaces of the types that have one, sorted so the first is stable."""
|
|
1198
|
+
found = {namespace_of(types, name) for name in names}
|
|
1199
|
+
return sorted(namespace for namespace in found if namespace is not None)
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def node_types(rule: Mapping[str, Any]) -> list[str]:
|
|
1203
|
+
"""The type of each node the rule entry emits."""
|
|
1204
|
+
return [str(emit["node"]["type"]) for emit in rule.get("emits", []) if "node" in emit]
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def twin_type(rule: Mapping[str, Any], siblings: Sequence[Mapping[str, Any]]) -> str | None:
|
|
1208
|
+
"""The node type of the edge rule's twin, or ``None`` when it has none.
|
|
1209
|
+
|
|
1210
|
+
The twin is the first other rule of the same pack that emits a node and whose ``match``, ``id``
|
|
1211
|
+
and ``in`` equal the edge rule's as documents. ``siblings`` is every rule entry of that pack.
|
|
1212
|
+
"""
|
|
1213
|
+
for other in siblings:
|
|
1214
|
+
nodes = node_types(other)
|
|
1215
|
+
if (
|
|
1216
|
+
other is not rule
|
|
1217
|
+
and nodes
|
|
1218
|
+
and all(other.get(key) == rule.get(key) for key in ("match", "id", "in"))
|
|
1219
|
+
):
|
|
1220
|
+
return nodes[0]
|
|
1221
|
+
return None
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _with_twin(rule: Rule, item: Mapping[str, Any], siblings: Sequence[Mapping[str, Any]]) -> Rule:
|
|
1225
|
+
"""An edge rule, given the type of its twin node rule."""
|
|
1226
|
+
if not (rule.supported and rule.edges):
|
|
1227
|
+
return rule
|
|
1228
|
+
found = twin_type(item, siblings)
|
|
1229
|
+
return replace(rule, twin_type=found) if found else rule
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
def _full_ids(rule: Rule, prefixes: tuple[str, ...]) -> Rule:
|
|
1233
|
+
"""The rule, each edge marked ``full_id`` when its template begins with one of the prefixes."""
|
|
1234
|
+
edges = tuple(
|
|
1235
|
+
replace(edge, full_id=(edge.template or "").startswith(prefixes)) for edge in rule.edges
|
|
1236
|
+
)
|
|
1237
|
+
return replace(rule, edges=edges) if edges != rule.edges else rule
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
def _check_edges(rule: Rule, types: Mapping[str, TypeInfo], kinds: Mapping[str, EdgeKind]) -> Rule:
|
|
1241
|
+
"""The rule, marked not executed when an edge it emits cannot be minted."""
|
|
1242
|
+
# A map rule's node ends name nodes already in the map, by their whole id.
|
|
1243
|
+
whole = _WHOLE_ENDS | set(_MAP_ENDS) if rule.matches_type else _WHOLE_ENDS
|
|
1244
|
+
for edge in rule.edges if rule.supported else ():
|
|
1245
|
+
kind = kinds.get(edge.kind)
|
|
1246
|
+
if kind is None:
|
|
1247
|
+
continue
|
|
1248
|
+
from_twin = bool(rule.twin_type) and edge.start == "this_node"
|
|
1249
|
+
to_twin = bool(rule.twin_type) and edge.finish == "this_node"
|
|
1250
|
+
# A composed end that is a full id, or that lists its types, needs no namespace of its
|
|
1251
|
+
# kind's types; neither does an end that takes the whole id of a node another rule found.
|
|
1252
|
+
composed_full = edge.full_id or bool(edge.types)
|
|
1253
|
+
from_full = composed_full and not edge.start or edge.start in whole
|
|
1254
|
+
to_full = composed_full and bool(edge.start) or edge.finish in whole
|
|
1255
|
+
if any(namespace_of(types, name) is None for name in edge.types):
|
|
1256
|
+
reason = f"a type the end of {edge.kind} lists has no id namespace"
|
|
1257
|
+
return replace(rule, supported=False, reason=reason)
|
|
1258
|
+
if not from_twin and not from_full and len(namespaces(types, kind.from_types)) != 1:
|
|
1259
|
+
reason = f"the types at the from end of {edge.kind} do not share one id namespace"
|
|
1260
|
+
elif not to_twin and not to_full and len(namespaces(types, kind.to_types)) != 1:
|
|
1261
|
+
reason = f"the types at the to end of {edge.kind} do not share one id namespace"
|
|
1262
|
+
else:
|
|
1263
|
+
continue
|
|
1264
|
+
return replace(rule, supported=False, reason=reason)
|
|
1265
|
+
return rule
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _read_kinds(
|
|
1269
|
+
pack: str,
|
|
1270
|
+
file: str,
|
|
1271
|
+
declared: object,
|
|
1272
|
+
kinds: dict[str, EdgeKind],
|
|
1273
|
+
conflicts: list[Problem],
|
|
1274
|
+
declared_at: dict[str, tuple[str, str]],
|
|
1275
|
+
) -> None:
|
|
1276
|
+
for entry in declared if isinstance(declared, list) else []:
|
|
1277
|
+
name = entry.get("kind") if isinstance(entry, Mapping) else None
|
|
1278
|
+
ends: list[tuple[str, ...]] = []
|
|
1279
|
+
for end in (entry.get("from"), entry.get("to")) if isinstance(entry, Mapping) else ():
|
|
1280
|
+
listed = [end] if isinstance(end, str) else end
|
|
1281
|
+
if isinstance(listed, list) and all(isinstance(t, str) for t in listed):
|
|
1282
|
+
ends.append(tuple(sorted(listed)))
|
|
1283
|
+
if not isinstance(name, str) or len(ends) != 2:
|
|
1284
|
+
raise PackRulesUnreadable(
|
|
1285
|
+
f"an edge_kinds entry of {pack} is not a mapping with a string kind and its ends",
|
|
1286
|
+
{"pack": pack},
|
|
1287
|
+
)
|
|
1288
|
+
kind = EdgeKind(name, ends[0], ends[1])
|
|
1289
|
+
first_pack, first_file = declared_at.setdefault(name, (pack, file))
|
|
1290
|
+
if kinds.setdefault(name, kind) != kind:
|
|
1291
|
+
message = (
|
|
1292
|
+
f"the edge kind {name} is declared with different ends in {pack}/{file}, "
|
|
1293
|
+
f"after {first_pack}/{first_file}"
|
|
1294
|
+
)
|
|
1295
|
+
detail = {"kind": name, "pack": pack, "file": file}
|
|
1296
|
+
conflicts.append(Problem(ExitCode.PACK_RULES_UNREADABLE, message, detail))
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def _read_types(pack: str, declared: object, types: dict[str, TypeInfo]) -> None:
|
|
1300
|
+
for entry in declared if isinstance(declared, list) else []:
|
|
1301
|
+
name = entry.get("name") if isinstance(entry, Mapping) else None
|
|
1302
|
+
if not isinstance(name, str):
|
|
1303
|
+
raise PackRulesUnreadable(
|
|
1304
|
+
f"a node_types entry of {pack} is not a mapping with a string name",
|
|
1305
|
+
{"pack": pack},
|
|
1306
|
+
)
|
|
1307
|
+
if name in types:
|
|
1308
|
+
raise PackRulesUnreadable(
|
|
1309
|
+
f"the type {name} is declared by both {types[name].pack} and {pack}",
|
|
1310
|
+
{"type": name},
|
|
1311
|
+
)
|
|
1312
|
+
types[name] = TypeInfo(
|
|
1313
|
+
name=name,
|
|
1314
|
+
id_namespace=entry.get("id_namespace"),
|
|
1315
|
+
parent=entry.get("parent"),
|
|
1316
|
+
pack=pack,
|
|
1317
|
+
)
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
class _Unsupported(Exception):
|
|
1321
|
+
"""A rule shape the engine does not execute, with the reason."""
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
def _read_rule(pack: str, item: Any, tables: frozenset[str] = frozenset()) -> Rule:
|
|
1325
|
+
"""The rule as a record; ``supported`` holds only for the shapes the engine runs. ``tables``
|
|
1326
|
+
names the tables its steps may name."""
|
|
1327
|
+
name = str(item.get("rule")) if isinstance(item, Mapping) else ""
|
|
1328
|
+
try:
|
|
1329
|
+
keys = unexecuted(item, _RULE)
|
|
1330
|
+
if keys:
|
|
1331
|
+
raise _Unsupported(f"the key {', '.join(keys)} is accepted and not executed")
|
|
1332
|
+
return _build_rule(pack, item, tables)
|
|
1333
|
+
except _Unsupported as reason:
|
|
1334
|
+
return Rule(pack, name, False, (), "", "", "", "", reason=str(reason))
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def _build_rule(pack: str, item: Any, tables: frozenset[str]) -> Rule:
|
|
1338
|
+
if item.get("reads") == "map":
|
|
1339
|
+
return _build_map(pack, item)
|
|
1340
|
+
if (
|
|
1341
|
+
isinstance(item.get("id"), Mapping)
|
|
1342
|
+
and "pattern" in item["id"]
|
|
1343
|
+
and "declaration" not in item["match"]
|
|
1344
|
+
):
|
|
1345
|
+
raise _Unsupported("only the id of a tree declaration rule takes a pattern")
|
|
1346
|
+
if "text" in item["match"]:
|
|
1347
|
+
return _build_text(pack, item, tables)
|
|
1348
|
+
if isinstance(item.get("id"), Mapping) and {"resolve", "separator"} & set(item["id"]):
|
|
1349
|
+
raise _Unsupported("only the id of a text rule takes resolution steps")
|
|
1350
|
+
if "declaration" in item["match"]:
|
|
1351
|
+
return _build_declaration(pack, item)
|
|
1352
|
+
if "reference" in item["match"]:
|
|
1353
|
+
return _build_reference(pack, item, tables)
|
|
1354
|
+
if "attribute" in item["match"] or "annotation" in item["match"]:
|
|
1355
|
+
return _build_attribute(pack, item)
|
|
1356
|
+
shape = _Unsupported("the rule has a shape the engine does not execute")
|
|
1357
|
+
if not isinstance(item, Mapping) or set(item) != _RULE_KEYS or item["reads"] != "file":
|
|
1358
|
+
raise shape
|
|
1359
|
+
match, emits, folders = item["match"], item["emits"], item["in"]
|
|
1360
|
+
if (
|
|
1361
|
+
not isinstance(folders, list)
|
|
1362
|
+
or not all(isinstance(folder, str) for folder in folders)
|
|
1363
|
+
or not isinstance(match, dict)
|
|
1364
|
+
or not {"file", "filetype"} <= set(match) <= {"file", "filetype", "where", "each"}
|
|
1365
|
+
or not isinstance(match["file"], str)
|
|
1366
|
+
or not isinstance(match["filetype"], str)
|
|
1367
|
+
or not isinstance(item["id"], dict)
|
|
1368
|
+
or not set(item["id"]) <= {"from", "normalize", "template"}
|
|
1369
|
+
or not isinstance(emits, list)
|
|
1370
|
+
or not isinstance(item["confidence"], str)
|
|
1371
|
+
):
|
|
1372
|
+
raise shape
|
|
1373
|
+
entries = [e for e in emits if isinstance(e, dict) and len(e) == 1]
|
|
1374
|
+
if len(entries) != len(emits) or not entries:
|
|
1375
|
+
raise shape
|
|
1376
|
+
nodes = [e["node"] for e in entries if "node" in e]
|
|
1377
|
+
attributes = [e["attribute"] for e in entries if "attribute" in e]
|
|
1378
|
+
edges = [e["edge"] for e in entries if "edge" in e]
|
|
1379
|
+
if len(nodes) + len(attributes) + len(edges) != len(entries) or (
|
|
1380
|
+
edges and (nodes or attributes)
|
|
1381
|
+
):
|
|
1382
|
+
raise shape
|
|
1383
|
+
if not edges and (
|
|
1384
|
+
len(nodes) != 1 or not isinstance(nodes[0], dict) or set(nodes[0]) != {"type"}
|
|
1385
|
+
):
|
|
1386
|
+
raise shape
|
|
1387
|
+
each = _read_each(match["each"]) if "each" in match else None
|
|
1388
|
+
read_edges = tuple(_read_edge(e) for e in edges)
|
|
1389
|
+
if each is None and any(edge.key == TEXT and not edge.parts for edge in read_edges):
|
|
1390
|
+
raise _Unsupported("an edge end from text needs match.each")
|
|
1391
|
+
return Rule(
|
|
1392
|
+
pack=pack,
|
|
1393
|
+
name=str(item["rule"]),
|
|
1394
|
+
supported=True,
|
|
1395
|
+
folders=tuple(folders),
|
|
1396
|
+
glob=match["file"],
|
|
1397
|
+
filetype=match["filetype"],
|
|
1398
|
+
node_type=str(nodes[0]["type"]) if nodes else "",
|
|
1399
|
+
confidence=item["confidence"],
|
|
1400
|
+
where=_read_where(match.get("where", [])),
|
|
1401
|
+
normalize=_read_normalize(item["id"].get("normalize", [])),
|
|
1402
|
+
attributes=tuple(_read_attribute(a) for a in attributes),
|
|
1403
|
+
edges=read_edges,
|
|
1404
|
+
each=each,
|
|
1405
|
+
id_template=_id_template(item["id"], each is not None),
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _build_text(pack: str, item: Mapping[str, Any], tables: frozenset[str]) -> Rule:
|
|
1410
|
+
"""A rule that reads its file as text and fires on each match of ``match.text``. The id comes
|
|
1411
|
+
from one capture or a template; each attribute from one capture, one value or a template; each
|
|
1412
|
+
edge runs between this_node and an end from one capture or a template, in either direction.
|
|
1413
|
+
A template names captures, path values and the rule's ``values``. A rule that emits bindings
|
|
1414
|
+
emits nothing else and has no id; the id and an edge end may hold resolution steps."""
|
|
1415
|
+
match, emits, block = item["match"], item["emits"], item.get("id")
|
|
1416
|
+
bindings = [emit["binding"] for emit in emits if "binding" in emit]
|
|
1417
|
+
if bindings:
|
|
1418
|
+
if "id" in item or len(bindings) != len(emits):
|
|
1419
|
+
raise _Unsupported("a text rule with a binding emits only bindings and has no id")
|
|
1420
|
+
block = {"template": ""}
|
|
1421
|
+
if (
|
|
1422
|
+
not _RULE_KEYS - {"id"} <= set(item) <= {*_RULE_KEYS, "values", "line"}
|
|
1423
|
+
or item["reads"] != "file"
|
|
1424
|
+
or not {"file", "filetype", "text"} <= set(match)
|
|
1425
|
+
or not set(match) <= {"file", "ending", "filetype", "text", "reads_sections"}
|
|
1426
|
+
or not isinstance(block, Mapping)
|
|
1427
|
+
or not set(block) <= {"from", "template", "normalize", "resolve", "separator"}
|
|
1428
|
+
or len({"from", "template"} & set(block)) != 1
|
|
1429
|
+
or not isinstance(block.get("template", ""), str)
|
|
1430
|
+
or not isinstance(match.get("reads_sections", []), list)
|
|
1431
|
+
):
|
|
1432
|
+
raise _Unsupported(
|
|
1433
|
+
"a text rule is not a file rule with in, an id from one capture or a template, and "
|
|
1434
|
+
"match.file, match.filetype and match.text, with optional ending, reads_sections, "
|
|
1435
|
+
"values and line"
|
|
1436
|
+
)
|
|
1437
|
+
pattern = _regex(match["text"])
|
|
1438
|
+
values = item.get("values", {})
|
|
1439
|
+
if not isinstance(values, Mapping):
|
|
1440
|
+
raise _Unsupported("the values of a text rule are not a mapping")
|
|
1441
|
+
read_values = tuple(_read_value(str(name), spec) for name, spec in values.items())
|
|
1442
|
+
nodes = node_types(item)
|
|
1443
|
+
attributes = [emit["attribute"] for emit in emits if "attribute" in emit]
|
|
1444
|
+
edges = [emit["edge"] for emit in emits if "edge" in emit]
|
|
1445
|
+
if (
|
|
1446
|
+
len(nodes) + len(attributes) + len(edges) + len(bindings) != len(emits)
|
|
1447
|
+
or len(nodes) > 1
|
|
1448
|
+
or any(emit["node"].keys() != {"type"} for emit in emits if "node" in emit)
|
|
1449
|
+
or (edges and (nodes or attributes))
|
|
1450
|
+
or (not edges and not nodes and not bindings)
|
|
1451
|
+
or not all(isinstance(b.get(k), str) for b in bindings for k in ("name", "target"))
|
|
1452
|
+
):
|
|
1453
|
+
raise _Unsupported(
|
|
1454
|
+
"a text rule emits one node with a type and attributes, edges, or bindings"
|
|
1455
|
+
)
|
|
1456
|
+
read: list[tuple[str, str]] = []
|
|
1457
|
+
templates: list[tuple[str, str]] = []
|
|
1458
|
+
captures = [_capture(block["from"])] if "from" in block else []
|
|
1459
|
+
named = [value.name for value in read_values]
|
|
1460
|
+
for attribute in attributes:
|
|
1461
|
+
name, source = attribute.get("name"), attribute.get("from")
|
|
1462
|
+
if not isinstance(name, str) or set(attribute) not in (
|
|
1463
|
+
{"name", "from"},
|
|
1464
|
+
{"name", "template"},
|
|
1465
|
+
):
|
|
1466
|
+
raise _Unsupported(
|
|
1467
|
+
"an attribute of a text rule is not a name and a capture, a value or a template"
|
|
1468
|
+
)
|
|
1469
|
+
if "template" in attribute and isinstance(attribute["template"], str):
|
|
1470
|
+
templates.append((name, attribute["template"]))
|
|
1471
|
+
elif isinstance(source, Mapping) and set(source) == {"value"} and source["value"] in named:
|
|
1472
|
+
read.append((name, str(source["value"])))
|
|
1473
|
+
else:
|
|
1474
|
+
read.append((name, _capture(source)))
|
|
1475
|
+
captures.append(read[-1][1])
|
|
1476
|
+
specs = []
|
|
1477
|
+
for edge in edges:
|
|
1478
|
+
flipped = edge.get("to") == "this_node" != edge.get("from")
|
|
1479
|
+
end = edge.get("from") if flipped else edge.get("to")
|
|
1480
|
+
if (
|
|
1481
|
+
set(edge) != {"kind", "from", "to"}
|
|
1482
|
+
or "this_node" not in (edge.get("from"), edge.get("to"))
|
|
1483
|
+
or not isinstance(end, Mapping)
|
|
1484
|
+
or not set(end)
|
|
1485
|
+
<= {"from", "template", "type", "types", "on_miss", "resolve", "separator"}
|
|
1486
|
+
or not {"from", "template"} & set(end)
|
|
1487
|
+
or not isinstance(end.get("template", ""), str)
|
|
1488
|
+
or not _types_ok(end)
|
|
1489
|
+
):
|
|
1490
|
+
raise _Unsupported(
|
|
1491
|
+
"an edge of a text rule does not run between this_node and a capture or a template"
|
|
1492
|
+
)
|
|
1493
|
+
key = _capture(end["from"]) if "from" in end else ""
|
|
1494
|
+
captures += [key] if key else []
|
|
1495
|
+
specs.append(
|
|
1496
|
+
EdgeSpec(
|
|
1497
|
+
edge["kind"],
|
|
1498
|
+
key,
|
|
1499
|
+
False,
|
|
1500
|
+
None,
|
|
1501
|
+
None,
|
|
1502
|
+
end.get("template"),
|
|
1503
|
+
(),
|
|
1504
|
+
start="" if flipped else "this_node",
|
|
1505
|
+
finish="this_node" if flipped else "",
|
|
1506
|
+
to_type=end.get("type", next(iter(end.get("types", [])), "")),
|
|
1507
|
+
on_miss="on_miss" in end,
|
|
1508
|
+
resolve=_read_steps(end.get("resolve", []), tables),
|
|
1509
|
+
separator=str(end.get("separator", "")),
|
|
1510
|
+
types=tuple(end.get("types", ())),
|
|
1511
|
+
)
|
|
1512
|
+
)
|
|
1513
|
+
captures += [value.capture for value in read_values if value.capture]
|
|
1514
|
+
line = _capture(item["line"]) if "line" in item else ""
|
|
1515
|
+
captures += [line] if line else []
|
|
1516
|
+
if any(capture not in pattern.groupindex for capture in captures):
|
|
1517
|
+
raise _Unsupported("a capture a text rule names is not a named group of match.text")
|
|
1518
|
+
if set(named) & set(pattern.groupindex):
|
|
1519
|
+
raise _Unsupported("a value of a text rule has the name of a capture")
|
|
1520
|
+
return Rule(
|
|
1521
|
+
pack=pack,
|
|
1522
|
+
name=str(item["rule"]),
|
|
1523
|
+
supported=True,
|
|
1524
|
+
folders=tuple(item["in"]),
|
|
1525
|
+
glob=match["file"],
|
|
1526
|
+
endings=_read_ending(match),
|
|
1527
|
+
filetype=match["filetype"],
|
|
1528
|
+
node_type=nodes[0] if nodes else "",
|
|
1529
|
+
confidence=item["confidence"],
|
|
1530
|
+
normalize=_read_normalize(block.get("normalize", [])),
|
|
1531
|
+
attributes=tuple(read),
|
|
1532
|
+
edges=tuple(specs),
|
|
1533
|
+
id_template=block.get("template", ""),
|
|
1534
|
+
text=pattern,
|
|
1535
|
+
id_capture=_capture(block["from"]) if "from" in block else "",
|
|
1536
|
+
values=read_values,
|
|
1537
|
+
reads_sections=tuple(str(name) for name in match.get("reads_sections", [])),
|
|
1538
|
+
templates=tuple(templates),
|
|
1539
|
+
bindings=tuple((b["name"], b["target"]) for b in bindings),
|
|
1540
|
+
id_resolve=_read_steps(block.get("resolve", []), tables),
|
|
1541
|
+
id_separator=str(block.get("separator", "")),
|
|
1542
|
+
line_capture=line,
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def _read_steps(steps: object, tables: frozenset[str] = frozenset()) -> tuple[Step, ...]:
|
|
1547
|
+
"""Resolution steps: each a kind that takes no text, or a mapping of one kind to its text. A
|
|
1548
|
+
table step's text is one of ``tables``."""
|
|
1549
|
+
read: list[Step] = []
|
|
1550
|
+
for step in steps if isinstance(steps, list) else [None]:
|
|
1551
|
+
pair = next(iter(step.items()), (None, "")) if isinstance(step, Mapping) else (step, "")
|
|
1552
|
+
kind, text = pair
|
|
1553
|
+
if (
|
|
1554
|
+
not isinstance(kind, str)
|
|
1555
|
+
or kind not in STEPS
|
|
1556
|
+
or (isinstance(step, Mapping) and len(step) != 1)
|
|
1557
|
+
or STEPS[kind] != (isinstance(text, str) and text != "")
|
|
1558
|
+
):
|
|
1559
|
+
raise _Unsupported(
|
|
1560
|
+
"a resolution step is not bound_first_segment, as_written, or one of "
|
|
1561
|
+
"full_if_prefixed, prefix_with, prefix_bare_with and lookup_last_segment_in with "
|
|
1562
|
+
"its text"
|
|
1563
|
+
)
|
|
1564
|
+
if kind.startswith("lookup_") and text not in tables:
|
|
1565
|
+
raise _Unsupported(
|
|
1566
|
+
f"the resolution step {kind} names {text}, which no tables key of the pack or a "
|
|
1567
|
+
"pack it depends on declares"
|
|
1568
|
+
)
|
|
1569
|
+
read.append((kind, text))
|
|
1570
|
+
return tuple(read)
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
def _read_value(name: str, spec: object, path: bool = False) -> Value:
|
|
1574
|
+
"""One value: from one capture, the capture of each enclosing span with a join, or a
|
|
1575
|
+
template, or for a path value a pattern and a template with an optional ``relative_to``; with
|
|
1576
|
+
optional normalize and case."""
|
|
1577
|
+
keys = set(spec) - {"normalize", "case"} if isinstance(spec, Mapping) else set()
|
|
1578
|
+
source = spec.get("from") if isinstance(spec, Mapping) else None
|
|
1579
|
+
span = (
|
|
1580
|
+
source if isinstance(source, Mapping) and set(source) == {"enclosing", "capture"} else None
|
|
1581
|
+
)
|
|
1582
|
+
enclosing = span is not None
|
|
1583
|
+
if path:
|
|
1584
|
+
wanted = keys - {"relative_to"} == {"pattern", "template"}
|
|
1585
|
+
else:
|
|
1586
|
+
wanted = keys in ({"from"}, {"template"}) or (enclosing and keys == {"from", "join"})
|
|
1587
|
+
if (
|
|
1588
|
+
not isinstance(spec, Mapping)
|
|
1589
|
+
or not wanted
|
|
1590
|
+
or not all(isinstance(spec.get(k, ""), str) for k in ("template", "relative_to", "join"))
|
|
1591
|
+
or spec.get("case", "") not in ("", "lower", "upper")
|
|
1592
|
+
):
|
|
1593
|
+
raise _Unsupported(
|
|
1594
|
+
f"the value {name} is not from one capture, from an enclosing span's capture, or a "
|
|
1595
|
+
"template, with optional normalize and case"
|
|
1596
|
+
)
|
|
1597
|
+
return Value(
|
|
1598
|
+
name,
|
|
1599
|
+
_capture(source) if "from" in spec and not enclosing else "",
|
|
1600
|
+
spec.get("template"),
|
|
1601
|
+
_read_normalize(spec.get("normalize", [])),
|
|
1602
|
+
str(spec.get("case", "")),
|
|
1603
|
+
_regex(spec["pattern"]) if path else None,
|
|
1604
|
+
str(spec.get("relative_to", "")),
|
|
1605
|
+
(str(span["enclosing"]), str(span["capture"])) if span is not None else None,
|
|
1606
|
+
str(spec.get("join", "")),
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
|
|
1610
|
+
def _capture(source: object) -> str:
|
|
1611
|
+
"""The group ``{capture: name}`` names."""
|
|
1612
|
+
if not isinstance(source, Mapping) or set(source) != {"capture"}:
|
|
1613
|
+
raise _Unsupported("a source of a text rule is not one capture")
|
|
1614
|
+
return str(source["capture"])
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def _build_declaration(pack: str, item: Mapping[str, Any]) -> Rule:
|
|
1618
|
+
"""A rule that reads a parse tree: it fires on each tree node of the ``declaration`` type."""
|
|
1619
|
+
match, emits = item["match"], item["emits"]
|
|
1620
|
+
if (
|
|
1621
|
+
set(item) | {"id"} != _RULE_KEYS
|
|
1622
|
+
or item["reads"] != "file"
|
|
1623
|
+
or not set(match) <= _DECLARATION_MATCH
|
|
1624
|
+
or "filetype" not in match
|
|
1625
|
+
or not isinstance(match.get("name_child", ""), (str, list))
|
|
1626
|
+
):
|
|
1627
|
+
raise _Unsupported(
|
|
1628
|
+
"a declaration rule is not a file rule with in, match.filetype and no match keys "
|
|
1629
|
+
"beyond declaration, name_child, body_child, where, file and ending"
|
|
1630
|
+
)
|
|
1631
|
+
where = match.get("where", [])
|
|
1632
|
+
if not isinstance(where, list) or not all(
|
|
1633
|
+
isinstance(c, Mapping)
|
|
1634
|
+
and len(c) == 1
|
|
1635
|
+
and set(c) <= {"has_child", "name_matches"}
|
|
1636
|
+
and (isinstance(next(iter(c.values())), str) or "name_matches" in c)
|
|
1637
|
+
for c in where
|
|
1638
|
+
):
|
|
1639
|
+
raise _Unsupported(
|
|
1640
|
+
"the match.where of a declaration rule holds only has_child and name_matches conditions"
|
|
1641
|
+
)
|
|
1642
|
+
nodes = node_types(item)
|
|
1643
|
+
edges = [_read_tree_edge(emit["edge"]) for emit in emits if "edge" in emit]
|
|
1644
|
+
if (
|
|
1645
|
+
len(nodes) + len(edges) != len(emits)
|
|
1646
|
+
or len(nodes) > 1
|
|
1647
|
+
or any(emit["node"].keys() != {"type"} for emit in emits if "node" in emit)
|
|
1648
|
+
):
|
|
1649
|
+
raise _Unsupported("a declaration rule emits one node with a type, or edges only")
|
|
1650
|
+
block = item.get("id", {})
|
|
1651
|
+
argument = _argument(block.get("from"), "argument") if isinstance(block, Mapping) else None
|
|
1652
|
+
if argument is not None and not set(block) <= {"from", "normalize"}:
|
|
1653
|
+
raise _Unsupported("an id from a call's argument takes only an optional normalize")
|
|
1654
|
+
if argument is not None and "body_child" in match:
|
|
1655
|
+
raise _Unsupported("an id from a call's argument cannot be combined with match.body_child")
|
|
1656
|
+
template = _tree_id(item["id"]) if "id" in item and argument is None else ""
|
|
1657
|
+
pattern = block.get("pattern", "") if template else ""
|
|
1658
|
+
if pattern:
|
|
1659
|
+
_regex(PATTERN_VALUE.sub("", pattern))
|
|
1660
|
+
if not (template or argument) and (
|
|
1661
|
+
nodes or any("this_node" in (e.start, e.finish) for e in edges)
|
|
1662
|
+
):
|
|
1663
|
+
raise _Unsupported("a declaration rule that emits a node or names this_node needs an id")
|
|
1664
|
+
if "name_child" not in match and {"declared_name", "qualified_name"} & set(
|
|
1665
|
+
PLACEHOLDER.findall(template + (f"{{{block['from']}}}" if pattern else ""))
|
|
1666
|
+
):
|
|
1667
|
+
raise _Unsupported("an id from declared_name or qualified_name needs match.name_child")
|
|
1668
|
+
sources = [s for s in block.get("from", ()) if isinstance(s, Mapping)] if template else []
|
|
1669
|
+
docblocks = [d for d in map(_docblock, sources) if d is not None]
|
|
1670
|
+
if len(docblocks) > 1:
|
|
1671
|
+
raise _Unsupported("a declaration rule's id has one docblock source")
|
|
1672
|
+
name_child = match.get("name_child", "")
|
|
1673
|
+
path = tuple(name_child) if isinstance(name_child, list) else ()
|
|
1674
|
+
return Rule(
|
|
1675
|
+
pack=pack,
|
|
1676
|
+
name=str(item["rule"]),
|
|
1677
|
+
supported=True,
|
|
1678
|
+
folders=tuple(item["in"]),
|
|
1679
|
+
glob=match.get("file", "*"),
|
|
1680
|
+
filetype=match["filetype"],
|
|
1681
|
+
node_type=nodes[0] if nodes else "",
|
|
1682
|
+
confidence=item["confidence"],
|
|
1683
|
+
edges=tuple(edges),
|
|
1684
|
+
id_template=template,
|
|
1685
|
+
id_pattern=pattern,
|
|
1686
|
+
id_source=str(block["from"]) if pattern else "",
|
|
1687
|
+
qualified=tuple(s["qualified"] for s in sources if _docblock(s) is None),
|
|
1688
|
+
docblock=docblocks[0][0] if docblocks else None,
|
|
1689
|
+
docblock_qualified=bool(docblocks) and docblocks[0][1],
|
|
1690
|
+
normalize=_read_normalize(item["id"].get("normalize", [])) if "id" in item else (),
|
|
1691
|
+
declaration=match["declaration"],
|
|
1692
|
+
endings=_read_ending(match),
|
|
1693
|
+
name_child="" if path else name_child,
|
|
1694
|
+
name_path=path,
|
|
1695
|
+
body_child=match.get("body_child", ""),
|
|
1696
|
+
has_child=tuple(c["has_child"] for c in where if "has_child" in c),
|
|
1697
|
+
id_argument=argument or "",
|
|
1698
|
+
name_matches=_name_matches(where, None if path else name_child or None),
|
|
1699
|
+
)
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _build_map(pack: str, item: Mapping[str, Any]) -> Rule:
|
|
1703
|
+
"""A rule that reads the map: it fires on each node of ``match.node_type`` or a type below it
|
|
1704
|
+
that its ``where`` holds for, once for each node its ``follow`` steps reach."""
|
|
1705
|
+
match, emits = item["match"], item["emits"]
|
|
1706
|
+
if (
|
|
1707
|
+
"in" in item
|
|
1708
|
+
or not set(match) <= {"node_type", "where", "follow"}
|
|
1709
|
+
or "node_type" not in match
|
|
1710
|
+
):
|
|
1711
|
+
raise _Unsupported(
|
|
1712
|
+
"a map rule is not a rule with match.node_type, optional where and follow, and no in"
|
|
1713
|
+
)
|
|
1714
|
+
where = _read_where(match.get("where", []))
|
|
1715
|
+
if not all(isinstance(condition, IdMatches) for condition in where):
|
|
1716
|
+
raise _Unsupported("the match.where of a map rule holds only id_matches conditions")
|
|
1717
|
+
follow: list[tuple[str, Any]] = []
|
|
1718
|
+
for step in match.get("follow", []):
|
|
1719
|
+
((name, value),) = step.items()
|
|
1720
|
+
if name == "id_matches":
|
|
1721
|
+
value = _regex(value)
|
|
1722
|
+
elif name == "ancestors":
|
|
1723
|
+
value = tuple(value)
|
|
1724
|
+
follow.append((name, value))
|
|
1725
|
+
nodes = node_types(item)
|
|
1726
|
+
edges = [_read_map_edge(emit["edge"], bool(follow)) for emit in emits if "edge" in emit]
|
|
1727
|
+
block = item.get("id")
|
|
1728
|
+
if (
|
|
1729
|
+
len(nodes) + len(edges) != len(emits)
|
|
1730
|
+
or len(nodes) > 1
|
|
1731
|
+
or any(emit["node"].keys() != {"type"} for emit in emits if "node" in emit)
|
|
1732
|
+
or (edges and block is not None)
|
|
1733
|
+
or (nodes and not (isinstance(block, Mapping) and set(block) == {"template"}))
|
|
1734
|
+
):
|
|
1735
|
+
raise _Unsupported(
|
|
1736
|
+
"a map rule emits one node with a type and an id from one template, or edges and no id"
|
|
1737
|
+
)
|
|
1738
|
+
return Rule(
|
|
1739
|
+
pack=pack,
|
|
1740
|
+
name=str(item["rule"]),
|
|
1741
|
+
supported=True,
|
|
1742
|
+
folders=(),
|
|
1743
|
+
glob="",
|
|
1744
|
+
filetype="",
|
|
1745
|
+
node_type=nodes[0] if nodes else "",
|
|
1746
|
+
confidence=item["confidence"],
|
|
1747
|
+
where=where,
|
|
1748
|
+
edges=tuple(edges),
|
|
1749
|
+
id_template=str(block["template"]) if nodes and block is not None else "",
|
|
1750
|
+
matches_type=str(match["node_type"]),
|
|
1751
|
+
follow=tuple(follow),
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1754
|
+
|
|
1755
|
+
#: The ends of a map rule's edge that name a node: the node it matched, and the node its follow
|
|
1756
|
+
#: steps reached.
|
|
1757
|
+
_MAP_ENDS = ("this_node", "matched_node")
|
|
1758
|
+
|
|
1759
|
+
|
|
1760
|
+
def _read_map_edge(edge: Mapping[str, Any], followed: bool) -> EdgeSpec:
|
|
1761
|
+
"""An edge of a map rule: between two node ends, or a node end and one composed end."""
|
|
1762
|
+
start, end = edge.get("from"), edge.get("to")
|
|
1763
|
+
composed = [e for e in (start, end) if isinstance(e, Mapping)]
|
|
1764
|
+
named = [e for e in (start, end) if isinstance(e, str) and e in _MAP_ENDS]
|
|
1765
|
+
if (
|
|
1766
|
+
set(edge) != {"kind", "from", "to"}
|
|
1767
|
+
or len(named) + len(composed) != 2
|
|
1768
|
+
or len(composed) > 1
|
|
1769
|
+
or ("matched_node" in named and not followed)
|
|
1770
|
+
or any(
|
|
1771
|
+
not {"template"} <= set(c) <= {"template", "type", "types", "on_miss"}
|
|
1772
|
+
or not isinstance(c["template"], str)
|
|
1773
|
+
or not _types_ok(c)
|
|
1774
|
+
for c in composed
|
|
1775
|
+
)
|
|
1776
|
+
):
|
|
1777
|
+
raise _Unsupported(
|
|
1778
|
+
"an edge of a map rule does not run between this_node or matched_node and one of them "
|
|
1779
|
+
"or a template with type or types and on_miss; matched_node needs match.follow"
|
|
1780
|
+
)
|
|
1781
|
+
if not composed:
|
|
1782
|
+
return EdgeSpec(edge["kind"], "", False, None, None, None, (), str(start), str(end))
|
|
1783
|
+
part, flipped = composed[0], isinstance(start, Mapping)
|
|
1784
|
+
return EdgeSpec(
|
|
1785
|
+
edge["kind"],
|
|
1786
|
+
"",
|
|
1787
|
+
False,
|
|
1788
|
+
None,
|
|
1789
|
+
None,
|
|
1790
|
+
part["template"],
|
|
1791
|
+
(),
|
|
1792
|
+
start="" if flipped else str(start),
|
|
1793
|
+
finish=str(end) if flipped else "",
|
|
1794
|
+
to_type=part.get("type", next(iter(part.get("types", [])), "")),
|
|
1795
|
+
on_miss="on_miss" in part,
|
|
1796
|
+
types=tuple(part.get("types", ())),
|
|
1797
|
+
)
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
def _read_ending(match: Mapping[str, Any]) -> tuple[str, ...]:
|
|
1801
|
+
"""A rule's ``match.ending``, one ending or a list of them; empty when absent."""
|
|
1802
|
+
ending = match.get("ending", [])
|
|
1803
|
+
return (ending,) if isinstance(ending, str) else tuple(ending)
|
|
1804
|
+
|
|
1805
|
+
|
|
1806
|
+
def _template_end(end: object) -> bool:
|
|
1807
|
+
"""Whether an edge end is ``{template: <text>}`` and nothing else."""
|
|
1808
|
+
return (
|
|
1809
|
+
isinstance(end, Mapping) and set(end) == {"template"} and isinstance(end["template"], str)
|
|
1810
|
+
)
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
def _types_ok(end: Mapping[str, Any]) -> bool:
|
|
1814
|
+
"""Whether an end gives ``type`` or a list of ``types``, not both."""
|
|
1815
|
+
listed = end.get("types", [])
|
|
1816
|
+
return not {"type", "types"} <= set(end) and all(isinstance(t, str) for t in listed)
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
def _build_reference(pack: str, item: Mapping[str, Any], tables: frozenset[str]) -> Rule:
|
|
1820
|
+
"""A rule that fires on each tree node of the ``reference`` type: an edge from the node the
|
|
1821
|
+
nearest enclosing declaration was given to the name its ``name_child`` writes."""
|
|
1822
|
+
match, emits = item["match"], item["emits"]
|
|
1823
|
+
child = match.get("name_child")
|
|
1824
|
+
fields = [child] if isinstance(child, str) else child if isinstance(child, list) else []
|
|
1825
|
+
if (
|
|
1826
|
+
set(item) != _RULE_KEYS - {"id"}
|
|
1827
|
+
or item["reads"] != "file"
|
|
1828
|
+
or not set(match)
|
|
1829
|
+
<= {
|
|
1830
|
+
*("reference", "name_child", "filetype", "file", "ending"),
|
|
1831
|
+
*("skip_names", "whole_written", "where"),
|
|
1832
|
+
}
|
|
1833
|
+
or "filetype" not in match
|
|
1834
|
+
or not fields
|
|
1835
|
+
or not all(isinstance(field, str) for field in fields)
|
|
1836
|
+
or any("edge" not in emit for emit in emits)
|
|
1837
|
+
):
|
|
1838
|
+
raise _Unsupported(
|
|
1839
|
+
"a reference rule is not a file rule with in, match.reference, match.name_child, "
|
|
1840
|
+
"match.filetype and edges only, and no id"
|
|
1841
|
+
)
|
|
1842
|
+
where = match.get("where", [])
|
|
1843
|
+
if not isinstance(where, list) or not all(
|
|
1844
|
+
isinstance(c, Mapping) and set(c) == {"name_matches"} for c in where
|
|
1845
|
+
):
|
|
1846
|
+
raise _Unsupported("the match.where of a reference rule holds only name_matches conditions")
|
|
1847
|
+
name_matches = _name_matches(where, fields[0])
|
|
1848
|
+
groups = {group for _, pattern in name_matches for group in pattern.groupindex}
|
|
1849
|
+
specs = []
|
|
1850
|
+
for emit in emits:
|
|
1851
|
+
edge = emit["edge"]
|
|
1852
|
+
end = edge.get("to")
|
|
1853
|
+
argument = _argument(end.get("from"), "argument") if isinstance(end, Mapping) else None
|
|
1854
|
+
start = edge.get("from")
|
|
1855
|
+
if (
|
|
1856
|
+
set(edge) != {"kind", "from", "to"}
|
|
1857
|
+
or (start != "enclosing_declaration" and not _template_end(start))
|
|
1858
|
+
or not isinstance(end, Mapping)
|
|
1859
|
+
or not set(end)
|
|
1860
|
+
<= {"type", "types", "on_miss", "resolve", "separator", "from", "template"}
|
|
1861
|
+
or ("from" in end and argument is None)
|
|
1862
|
+
or not _types_ok(end)
|
|
1863
|
+
):
|
|
1864
|
+
raise _Unsupported(
|
|
1865
|
+
"an edge of a reference rule does not run from enclosing_declaration or a template "
|
|
1866
|
+
"to an end of type or types, resolve, separator and on_miss, and from a call's "
|
|
1867
|
+
"argument or a template"
|
|
1868
|
+
)
|
|
1869
|
+
to_template = end.get("template")
|
|
1870
|
+
from_template = start["template"] if isinstance(start, Mapping) else None
|
|
1871
|
+
names = set(PLACEHOLDER.findall(f"{to_template or ''}{from_template or ''}"))
|
|
1872
|
+
if (
|
|
1873
|
+
(to_template is not None and ("from" in end or not isinstance(to_template, str)))
|
|
1874
|
+
or (to_template is not None and from_template is not None)
|
|
1875
|
+
or not names <= _REFERENCE_VALUES | groups
|
|
1876
|
+
):
|
|
1877
|
+
raise _Unsupported(
|
|
1878
|
+
"a template end of a reference rule is one end of an edge, naming only "
|
|
1879
|
+
"enclosing_type, enclosing_method and the named groups of its name_matches "
|
|
1880
|
+
"conditions"
|
|
1881
|
+
)
|
|
1882
|
+
specs.append(
|
|
1883
|
+
EdgeSpec(
|
|
1884
|
+
edge["kind"],
|
|
1885
|
+
argument or "",
|
|
1886
|
+
False,
|
|
1887
|
+
None,
|
|
1888
|
+
None,
|
|
1889
|
+
to_template,
|
|
1890
|
+
(),
|
|
1891
|
+
start="enclosing_declaration" if from_template is None else "template",
|
|
1892
|
+
to_type=end.get("type", next(iter(end.get("types", [])), "")),
|
|
1893
|
+
on_miss="on_miss" in end,
|
|
1894
|
+
resolve=_read_steps(end.get("resolve", []), tables),
|
|
1895
|
+
separator=str(end.get("separator", "")),
|
|
1896
|
+
# The one-type form is a list of that one type.
|
|
1897
|
+
types=tuple(end.get("types", ())) or tuple(end.get("type", "").split()),
|
|
1898
|
+
from_template=from_template,
|
|
1899
|
+
)
|
|
1900
|
+
)
|
|
1901
|
+
return Rule(
|
|
1902
|
+
pack=pack,
|
|
1903
|
+
name=str(item["rule"]),
|
|
1904
|
+
supported=True,
|
|
1905
|
+
folders=tuple(item["in"]),
|
|
1906
|
+
glob=match.get("file", "*"),
|
|
1907
|
+
filetype=match["filetype"],
|
|
1908
|
+
node_type="",
|
|
1909
|
+
confidence=item["confidence"],
|
|
1910
|
+
edges=tuple(specs),
|
|
1911
|
+
id_template="",
|
|
1912
|
+
name_child=fields[0],
|
|
1913
|
+
reference=match["reference"],
|
|
1914
|
+
endings=_read_ending(match),
|
|
1915
|
+
member_child=fields[1] if len(fields) > 1 else "",
|
|
1916
|
+
skip_names=frozenset(match.get("skip_names", ())),
|
|
1917
|
+
whole_written=match.get("whole_written") is True,
|
|
1918
|
+
name_matches=name_matches,
|
|
1919
|
+
)
|
|
1920
|
+
|
|
1921
|
+
|
|
1922
|
+
def _build_attribute(pack: str, item: Mapping[str, Any]) -> Rule:
|
|
1923
|
+
"""A rule that fires on each attribute of one class, or each docblock annotation of one name,
|
|
1924
|
+
written on one kind of declaration."""
|
|
1925
|
+
match, emits = item["match"], item["emits"]
|
|
1926
|
+
where = match.get("where")
|
|
1927
|
+
shape = "annotation" if "annotation" in match else "attribute"
|
|
1928
|
+
key = "annotation_key" if shape == "annotation" else "attribute_argument"
|
|
1929
|
+
if (
|
|
1930
|
+
set(item) | {"id"} != _RULE_KEYS
|
|
1931
|
+
or item["reads"] != "file"
|
|
1932
|
+
or not set(match) <= {shape, "filetype", "where"}
|
|
1933
|
+
or "filetype" not in match
|
|
1934
|
+
or not (isinstance(where, list) and len(where) == 1 and set(where[0]) == {"applies_to"})
|
|
1935
|
+
):
|
|
1936
|
+
raise _Unsupported(
|
|
1937
|
+
f"an {shape} rule is not a file rule with in, match.filetype and one where condition, "
|
|
1938
|
+
"applies_to"
|
|
1939
|
+
)
|
|
1940
|
+
nodes = node_types(item)
|
|
1941
|
+
attributes = [emit["attribute"] for emit in emits if "attribute" in emit]
|
|
1942
|
+
edges = [_read_attribute_edge(emit["edge"]) for emit in emits if "edge" in emit]
|
|
1943
|
+
if (
|
|
1944
|
+
len(nodes) + len(attributes) + len(edges) != len(emits)
|
|
1945
|
+
or len(nodes) > 1
|
|
1946
|
+
or any(emit["node"].keys() != {"type"} for emit in emits if "node" in emit)
|
|
1947
|
+
or (edges and (nodes or attributes))
|
|
1948
|
+
or (not edges and not nodes)
|
|
1949
|
+
):
|
|
1950
|
+
raise _Unsupported(f"an {shape} rule emits one node with a type and attributes, or edges")
|
|
1951
|
+
if shape == "annotation" and any({e.start, e.finish} - _TREE_ENDS for e in edges):
|
|
1952
|
+
raise _Unsupported("an annotation rule's edge runs between this_node and enclosing_class")
|
|
1953
|
+
block = item.get("id", {})
|
|
1954
|
+
source = block.get("from") if isinstance(block, Mapping) else None
|
|
1955
|
+
if "id" in item and (not set(block) <= {"from", "must_be"} or _argument(source, key) is None):
|
|
1956
|
+
raise _Unsupported(f"the id block of an {shape} rule is not from one {key}")
|
|
1957
|
+
if "id" not in item and (nodes or any("this_node" in (e.start, e.finish) for e in edges)):
|
|
1958
|
+
raise _Unsupported(f"an {shape} rule that emits a node or names this_node needs an id")
|
|
1959
|
+
read = [(a.get("name"), _argument(a.get("from"), key)) for a in attributes]
|
|
1960
|
+
if any(not isinstance(name, str) or argument is None for name, argument in read):
|
|
1961
|
+
raise _Unsupported(f"an attribute of an {shape} rule is not a name and an {key}")
|
|
1962
|
+
return Rule(
|
|
1963
|
+
pack=pack,
|
|
1964
|
+
name=str(item["rule"]),
|
|
1965
|
+
supported=True,
|
|
1966
|
+
folders=tuple(item["in"]),
|
|
1967
|
+
glob="*",
|
|
1968
|
+
filetype=match["filetype"],
|
|
1969
|
+
node_type=nodes[0] if nodes else "",
|
|
1970
|
+
confidence=item["confidence"],
|
|
1971
|
+
attributes=tuple((str(name), str(argument)) for name, argument in read),
|
|
1972
|
+
edges=tuple(edges),
|
|
1973
|
+
id_template="",
|
|
1974
|
+
attribute=match.get("attribute", ""),
|
|
1975
|
+
applies_to=where[0]["applies_to"],
|
|
1976
|
+
id_argument=_argument(source, key) or "",
|
|
1977
|
+
must_be_literal=block.get("must_be") == "string_literal",
|
|
1978
|
+
annotation=match.get("annotation", ""),
|
|
1979
|
+
)
|
|
1980
|
+
|
|
1981
|
+
|
|
1982
|
+
def _name_matches(
|
|
1983
|
+
where: list[Mapping[str, Any]], name_child: str | None
|
|
1984
|
+
) -> tuple[tuple[str, re.Pattern[str]], ...]:
|
|
1985
|
+
"""The child and pattern of each ``name_matches`` condition: a ``{child, pattern}`` mapping, or
|
|
1986
|
+
a pattern alone, which tests ``name_child`` and so needs it."""
|
|
1987
|
+
found = []
|
|
1988
|
+
for condition in (c["name_matches"] for c in where if "name_matches" in c):
|
|
1989
|
+
if isinstance(condition, Mapping):
|
|
1990
|
+
if set(condition) != {"child", "pattern"} or not isinstance(condition["child"], str):
|
|
1991
|
+
raise _Unsupported("a name_matches mapping is not a child and a pattern")
|
|
1992
|
+
found.append((condition["child"], _regex(condition["pattern"])))
|
|
1993
|
+
elif name_child is None:
|
|
1994
|
+
raise _Unsupported("a name_matches condition needs match.name_child")
|
|
1995
|
+
else:
|
|
1996
|
+
found.append((name_child, _regex(condition)))
|
|
1997
|
+
return tuple(found)
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
def _argument(source: object, key: str = "attribute_argument") -> str | None:
|
|
2001
|
+
"""The argument or annotation key ``{key: name or position}`` names, or ``None``."""
|
|
2002
|
+
if not isinstance(source, Mapping) or set(source) != {key}:
|
|
2003
|
+
return None
|
|
2004
|
+
found = source[key]
|
|
2005
|
+
return str(found) if isinstance(found, (str, int)) and not isinstance(found, bool) else None
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
def _read_attribute_edge(edge: Mapping[str, Any]) -> EdgeSpec:
|
|
2009
|
+
"""An edge of an attribute rule: between node ends, or to the id an argument's value names."""
|
|
2010
|
+
start, end = edge["from"], edge["to"]
|
|
2011
|
+
argument = (
|
|
2012
|
+
_argument(end.get("from")) if isinstance(end, Mapping) and set(end) == {"from"} else None
|
|
2013
|
+
)
|
|
2014
|
+
if (
|
|
2015
|
+
set(edge) != {"kind", "from", "to"}
|
|
2016
|
+
or not (isinstance(start, str) and start in _ATTRIBUTE_ENDS)
|
|
2017
|
+
or (argument is None and not (isinstance(end, str) and end in _ATTRIBUTE_ENDS))
|
|
2018
|
+
):
|
|
2019
|
+
raise _Unsupported(
|
|
2020
|
+
"an edge of an attribute rule does not run from this_node, enclosing_class or "
|
|
2021
|
+
"enclosing_method to one of them or to {from: {attribute_argument: ...}}"
|
|
2022
|
+
)
|
|
2023
|
+
if argument is not None:
|
|
2024
|
+
return EdgeSpec(
|
|
2025
|
+
edge["kind"], argument, False, None, None, None, (), start, "attribute_argument"
|
|
2026
|
+
)
|
|
2027
|
+
return EdgeSpec(edge["kind"], "", False, None, None, None, (), start, end)
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
def _tree_id(block: Mapping[str, Any]) -> str:
|
|
2031
|
+
"""The id block of a declaration rule as one template over its sources: a name the engine
|
|
2032
|
+
fills, a path value, a settings value or a field of the matched node."""
|
|
2033
|
+
source, template = block.get("from"), block.get("template")
|
|
2034
|
+
if "pattern" in block and set(block) <= {"from", "pattern", "template", "normalize"}:
|
|
2035
|
+
if not (isinstance(source, str) and PLACEHOLDER.fullmatch(f"{{{source}}}")):
|
|
2036
|
+
raise _Unsupported(
|
|
2037
|
+
"an id pattern reads one source, a name and not a list"
|
|
2038
|
+
if isinstance(source, list)
|
|
2039
|
+
else "an id pattern has no `from` naming the one source it reads"
|
|
2040
|
+
if source is None
|
|
2041
|
+
else "an id pattern reads one source, and its `from` is not a name"
|
|
2042
|
+
)
|
|
2043
|
+
return f"{{{source}}}" if template is None else str(template)
|
|
2044
|
+
if set(block) <= {"from", "template", "normalize"}:
|
|
2045
|
+
if isinstance(source, str) and PLACEHOLDER.fullmatch(f"{{{source}}}") and template is None:
|
|
2046
|
+
return f"{{{source}}}"
|
|
2047
|
+
gates: set[str] = set()
|
|
2048
|
+
if isinstance(source, list):
|
|
2049
|
+
found = [_docblock(s) for s in source if isinstance(s, Mapping)]
|
|
2050
|
+
gates = {g for d in found if d is not None for g in _groups(d[0])}
|
|
2051
|
+
source = [_qualified(s) if isinstance(s, Mapping) else s for s in source]
|
|
2052
|
+
if (
|
|
2053
|
+
isinstance(source, list)
|
|
2054
|
+
and isinstance(template, str)
|
|
2055
|
+
and set(PLACEHOLDER.findall(template)) | gates == set(source)
|
|
2056
|
+
):
|
|
2057
|
+
return template
|
|
2058
|
+
raise _Unsupported(
|
|
2059
|
+
"the id block of a declaration rule is not from one source, or from several with a "
|
|
2060
|
+
"template naming each, and an optional normalize"
|
|
2061
|
+
)
|
|
2062
|
+
|
|
2063
|
+
|
|
2064
|
+
def _qualified(source: Mapping[str, Any]) -> str:
|
|
2065
|
+
"""The field a source ``{qualified: <field>}`` names, or the value a docblock source's group
|
|
2066
|
+
names."""
|
|
2067
|
+
docblock = _docblock(source)
|
|
2068
|
+
if docblock is not None:
|
|
2069
|
+
return next(iter(_groups(docblock[0])))
|
|
2070
|
+
if set(source) != {"qualified"} or not isinstance(source["qualified"], str):
|
|
2071
|
+
raise _Unsupported("a source of a declaration rule's id is a name or {qualified: <field>}")
|
|
2072
|
+
return source["qualified"]
|
|
2073
|
+
|
|
2074
|
+
|
|
2075
|
+
def _docblock(source: Mapping[str, Any]) -> tuple[re.Pattern[str], bool] | None:
|
|
2076
|
+
"""The pattern of a source ``{docblock: <pattern>}``, or of one wrapped in ``qualified``, and
|
|
2077
|
+
whether it is wrapped; ``None`` for any other source. The pattern has one named group."""
|
|
2078
|
+
inner = source.get("qualified")
|
|
2079
|
+
wrapped = isinstance(inner, Mapping) and set(source) == {"qualified"}
|
|
2080
|
+
found: Mapping[str, Any] = inner if wrapped and isinstance(inner, Mapping) else source
|
|
2081
|
+
if set(found) != {"docblock"}:
|
|
2082
|
+
return None
|
|
2083
|
+
pattern = _regex(found["docblock"])
|
|
2084
|
+
if len(pattern.groupindex) != 1:
|
|
2085
|
+
raise _Unsupported("the pattern of a docblock source has not exactly one named group")
|
|
2086
|
+
return pattern, wrapped
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
def _groups(pattern: re.Pattern[str] | None) -> tuple[str, ...]:
|
|
2090
|
+
"""The names of the pattern's named groups; none for no pattern."""
|
|
2091
|
+
return tuple(pattern.groupindex) if pattern is not None else ()
|
|
2092
|
+
|
|
2093
|
+
|
|
2094
|
+
def _read_tree_edge(edge: Mapping[str, Any]) -> EdgeSpec:
|
|
2095
|
+
"""An edge of a declaration rule: from a node end to a node end or to the written names. A node
|
|
2096
|
+
end ``{declared: <type>}`` is the node of that type another rule declares at the match."""
|
|
2097
|
+
start, end = edge["from"], edge["to"]
|
|
2098
|
+
declared = (_declared(start), _declared(end))
|
|
2099
|
+
start = "declared" if declared[0] else start
|
|
2100
|
+
finish = "declared" if declared[1] else end if isinstance(end, str) else ""
|
|
2101
|
+
if end == {"from": "qualified_name"}:
|
|
2102
|
+
finish = "qualified_name"
|
|
2103
|
+
if (
|
|
2104
|
+
set(edge) != {"kind", "from", "to"}
|
|
2105
|
+
or not (declared[0] or isinstance(start, str) and start in _DECLARATION_ENDS)
|
|
2106
|
+
or not (declared[1] or finish in {*_DECLARATION_ENDS, "qualified_name"})
|
|
2107
|
+
):
|
|
2108
|
+
raise _Unsupported(
|
|
2109
|
+
"an edge of a declaration rule does not run from this_node, enclosing_class, "
|
|
2110
|
+
"enclosing_file, enclosing_method or {declared: <type>} to one of them or to "
|
|
2111
|
+
"{from: qualified_name}"
|
|
2112
|
+
)
|
|
2113
|
+
return EdgeSpec(edge["kind"], "", False, None, None, None, (), start, finish, declared=declared)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def _declared(end: object) -> str:
|
|
2117
|
+
"""The node type an end ``{declared: <type>}`` names; ``""`` for any other end."""
|
|
2118
|
+
if isinstance(end, Mapping) and set(end) == {"declared"} and isinstance(end["declared"], str):
|
|
2119
|
+
return end["declared"]
|
|
2120
|
+
return ""
|
|
2121
|
+
|
|
2122
|
+
|
|
2123
|
+
def _read_each(path: object) -> str:
|
|
2124
|
+
"""The key path of the mapping whose entries a rule fires on; ``""`` for the top level."""
|
|
2125
|
+
if not isinstance(path, str) or path.count("*") != 1 or path.split(".")[-1] != "*":
|
|
2126
|
+
raise _Unsupported("match.each is not a key path whose last segment alone is a star")
|
|
2127
|
+
return path.removesuffix("*").removesuffix(".")
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def _id_template(block: Mapping[str, Any], entries: bool) -> str:
|
|
2131
|
+
"""The id block as one template over ``{file_stem}`` and ``{key}``."""
|
|
2132
|
+
source, template = block.get("from"), block.get("template")
|
|
2133
|
+
if source == "file_stem" and template is None:
|
|
2134
|
+
return "{file_stem}"
|
|
2135
|
+
if source == "key" and template is None and entries:
|
|
2136
|
+
return "{key}"
|
|
2137
|
+
named = [source] if isinstance(source, str) else source if source is not None else []
|
|
2138
|
+
allowed = {"file_stem", "key"} if entries else {"file_stem"}
|
|
2139
|
+
# A placeholder beyond the sources must be a path value, checked once the file types are read.
|
|
2140
|
+
if (
|
|
2141
|
+
isinstance(named, list)
|
|
2142
|
+
and all(isinstance(name, str) for name in named)
|
|
2143
|
+
and set(named) <= allowed
|
|
2144
|
+
and isinstance(template, str)
|
|
2145
|
+
and set(named) <= set(PLACEHOLDER.findall(template))
|
|
2146
|
+
):
|
|
2147
|
+
return template
|
|
2148
|
+
raise _Unsupported(
|
|
2149
|
+
"the id block is not from file_stem, from key with match.each, or a template naming its "
|
|
2150
|
+
"sources, file_stem, key with match.each, and path values"
|
|
2151
|
+
)
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
def _regex(expression: object) -> re.Pattern[str]:
|
|
2155
|
+
if not isinstance(expression, str):
|
|
2156
|
+
raise _Unsupported("a regular expression is not a string")
|
|
2157
|
+
try:
|
|
2158
|
+
return compile_pattern(expression)
|
|
2159
|
+
except re.error as error:
|
|
2160
|
+
raise _Unsupported(f"the expression {expression} does not compile: {error}") from error
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
def _key_path(source: object) -> str:
|
|
2164
|
+
if not isinstance(source, dict) or set(source) != {"key"} or not isinstance(source["key"], str):
|
|
2165
|
+
raise _Unsupported("a source is not a single key path")
|
|
2166
|
+
return source["key"]
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
def _read_where(where: object) -> tuple[Condition, ...]:
|
|
2170
|
+
conditions: list[Condition] = []
|
|
2171
|
+
for condition in where if isinstance(where, list) else [None]:
|
|
2172
|
+
name, found = (
|
|
2173
|
+
next(iter(condition.items()))
|
|
2174
|
+
if isinstance(condition, dict) and len(condition) == 1
|
|
2175
|
+
else (None, None)
|
|
2176
|
+
)
|
|
2177
|
+
if (
|
|
2178
|
+
name == "key"
|
|
2179
|
+
and isinstance(found, dict)
|
|
2180
|
+
and set(found) in ({"name", "equals"}, {"name", "not_equals"})
|
|
2181
|
+
and isinstance(found["name"], str)
|
|
2182
|
+
and not isinstance(found.get("equals", found.get("not_equals")), (dict, list))
|
|
2183
|
+
):
|
|
2184
|
+
negate = "not_equals" in found
|
|
2185
|
+
value = found["not_equals" if negate else "equals"]
|
|
2186
|
+
conditions.append(KeyEquals(found["name"], value, negate))
|
|
2187
|
+
elif (
|
|
2188
|
+
name == "has_key"
|
|
2189
|
+
and isinstance(found, dict)
|
|
2190
|
+
and set(found) == {"path"}
|
|
2191
|
+
and isinstance(found["path"], str)
|
|
2192
|
+
):
|
|
2193
|
+
conditions.append(HasKey(found["path"]))
|
|
2194
|
+
elif name == "id_matches":
|
|
2195
|
+
conditions.append(IdMatches(_regex(found)))
|
|
2196
|
+
elif name == "is_mapping" and isinstance(found, bool):
|
|
2197
|
+
conditions.append(IsMapping(found))
|
|
2198
|
+
else:
|
|
2199
|
+
raise _Unsupported(
|
|
2200
|
+
"a match.where condition is not a key with name and equals or not_equals, "
|
|
2201
|
+
"a has_key with one path, an id_matches, or an is_mapping of true or false"
|
|
2202
|
+
)
|
|
2203
|
+
return tuple(conditions)
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
def _read_normalize(normalize: object) -> tuple[tuple[re.Pattern[str], str], ...]:
|
|
2207
|
+
steps = []
|
|
2208
|
+
for step in normalize if isinstance(normalize, list) else [None]:
|
|
2209
|
+
if not isinstance(step, dict) or set(step) != {"replace", "with"}:
|
|
2210
|
+
raise _Unsupported("a normalize step is not a replace and a with")
|
|
2211
|
+
if not isinstance(step["with"], str):
|
|
2212
|
+
raise _Unsupported("a normalize step has a replacement that is not a string")
|
|
2213
|
+
steps.append((_regex(step["replace"]), step["with"]))
|
|
2214
|
+
return tuple(steps)
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
def _read_attribute(attribute: object) -> tuple[str, str]:
|
|
2218
|
+
if (
|
|
2219
|
+
not isinstance(attribute, dict)
|
|
2220
|
+
or set(attribute) != {"name", "from"}
|
|
2221
|
+
or not isinstance(attribute["name"], str)
|
|
2222
|
+
):
|
|
2223
|
+
raise _Unsupported("an attribute is not a name and a key source")
|
|
2224
|
+
return attribute["name"], _key_path(attribute["from"])
|
|
2225
|
+
|
|
2226
|
+
|
|
2227
|
+
def _read_edge(edge: object) -> EdgeSpec:
|
|
2228
|
+
"""An edge of a data rule: between this_node and a composed end, in either direction."""
|
|
2229
|
+
flipped = isinstance(edge, dict) and edge.get("to") == "this_node" != edge.get("from")
|
|
2230
|
+
end = (edge.get("from") if flipped else edge.get("to")) if isinstance(edge, dict) else None
|
|
2231
|
+
on_miss = isinstance(end, dict) and "on_miss" in end
|
|
2232
|
+
if isinstance(edge, dict) and isinstance(end, dict):
|
|
2233
|
+
end = {key: value for key, value in end.items() if key != "on_miss"}
|
|
2234
|
+
edge = {**edge, "from": "this_node", "to": end}
|
|
2235
|
+
spec = _read_forward(edge)
|
|
2236
|
+
if flipped:
|
|
2237
|
+
spec = replace(spec, start="", finish="this_node")
|
|
2238
|
+
return replace(spec, on_miss=on_miss)
|
|
2239
|
+
|
|
2240
|
+
|
|
2241
|
+
def _read_forward(edge: object) -> EdgeSpec:
|
|
2242
|
+
end = edge.get("to") if isinstance(edge, dict) else None
|
|
2243
|
+
if (
|
|
2244
|
+
not isinstance(edge, dict)
|
|
2245
|
+
or set(edge) != {"kind", "from", "to"}
|
|
2246
|
+
or not isinstance(edge["kind"], str)
|
|
2247
|
+
or edge["from"] != "this_node"
|
|
2248
|
+
or not isinstance(end, dict)
|
|
2249
|
+
or not {"from"} <= set(end) <= {"from", "each", "where", "pattern", "template", "type"}
|
|
2250
|
+
):
|
|
2251
|
+
raise _Unsupported("an edge is not from this_node to a from end")
|
|
2252
|
+
template, to_type = end.get("template"), end.get("type", "")
|
|
2253
|
+
if template is not None and not isinstance(template, str):
|
|
2254
|
+
raise _Unsupported("an edge template is not a string")
|
|
2255
|
+
source = end["from"]
|
|
2256
|
+
if isinstance(source, list):
|
|
2257
|
+
parts = tuple(_read_part(part) for part in source) or (("file", ""),)
|
|
2258
|
+
where = end.get("where", [{"listed_at": None}])
|
|
2259
|
+
conditions = where if isinstance(where, list) else [None]
|
|
2260
|
+
listed: object = None
|
|
2261
|
+
seen = False
|
|
2262
|
+
matches: str | None = None
|
|
2263
|
+
not_matches: list[str] = []
|
|
2264
|
+
refused = "an edge end takes a key, or a list source with a template and at most one each, "
|
|
2265
|
+
refused += "or from: [] with a template alone, and where conditions "
|
|
2266
|
+
refused += "listed_at, matches (a glob on each key) and not_matches (a glob on each key)"
|
|
2267
|
+
for condition in conditions:
|
|
2268
|
+
if not isinstance(condition, dict) or len(condition) != 1:
|
|
2269
|
+
raise _Unsupported(refused)
|
|
2270
|
+
((name, value),) = condition.items()
|
|
2271
|
+
if name == "listed_at" and not seen:
|
|
2272
|
+
listed, seen = value, True
|
|
2273
|
+
elif name == "matches" and matches is None and isinstance(value, str):
|
|
2274
|
+
matches = value
|
|
2275
|
+
elif name == "not_matches" and isinstance(value, str):
|
|
2276
|
+
not_matches.append(value)
|
|
2277
|
+
else:
|
|
2278
|
+
raise _Unsupported(refused)
|
|
2279
|
+
eaches = sum(mode == "each" for mode, _ in parts)
|
|
2280
|
+
if (
|
|
2281
|
+
eaches > 1
|
|
2282
|
+
or (eaches == 0 and (matches is not None or not_matches))
|
|
2283
|
+
or template is None
|
|
2284
|
+
or not {"from", "template"} <= set(end) <= {"from", "template", "where", "type"}
|
|
2285
|
+
or not (listed is None or (isinstance(listed, str) and listed))
|
|
2286
|
+
):
|
|
2287
|
+
raise _Unsupported(refused)
|
|
2288
|
+
return EdgeSpec(
|
|
2289
|
+
edge["kind"],
|
|
2290
|
+
"",
|
|
2291
|
+
False,
|
|
2292
|
+
matches,
|
|
2293
|
+
None,
|
|
2294
|
+
template,
|
|
2295
|
+
parts,
|
|
2296
|
+
listed_at=listed,
|
|
2297
|
+
not_matches=tuple(not_matches),
|
|
2298
|
+
to_type=to_type,
|
|
2299
|
+
)
|
|
2300
|
+
where = end.get("where")
|
|
2301
|
+
matches = None
|
|
2302
|
+
if where is not None:
|
|
2303
|
+
found = where[0].get("matches") if isinstance(where, list) and len(where) == 1 else None
|
|
2304
|
+
if not isinstance(found, str) or set(where[0]) != {"matches"}:
|
|
2305
|
+
raise _Unsupported("an edge where is not one matches glob")
|
|
2306
|
+
matches = found
|
|
2307
|
+
pattern = _regex(end["pattern"]) if "pattern" in end else None
|
|
2308
|
+
if not isinstance(end.get("each", False), bool):
|
|
2309
|
+
raise _Unsupported("an edge each is not true or false")
|
|
2310
|
+
path = TEXT if source == "text" else _key_path(source)
|
|
2311
|
+
return EdgeSpec(
|
|
2312
|
+
edge["kind"], path, end.get("each", False), matches, pattern, template, (), to_type=to_type
|
|
2313
|
+
)
|
|
2314
|
+
|
|
2315
|
+
|
|
2316
|
+
def _read_part(part: object) -> tuple[str, str]:
|
|
2317
|
+
if isinstance(part, dict) and set(part) == {"each"} and isinstance(part["each"], str):
|
|
2318
|
+
return "each", part["each"]
|
|
2319
|
+
if part == "key":
|
|
2320
|
+
return "entry", ""
|
|
2321
|
+
return "key", _key_path(part)
|