diff --git a/.gersemirc.example b/.gersemirc.example index 429629a..cd4e34c 100644 --- a/.gersemirc.example +++ b/.gersemirc.example @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/0.18.0/gersemi/configuration.schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/BlankSpruce/gersemi/0.18.1/gersemi/configuration.schema.json definitions: [] disable_formatting: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ea1b4..9ffdefc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## [0.18.1] 2025-01-13 +### Fixed +- Proper formatting of first `...` group in `INSTALL(TARGETS)` command. (#51) + ## [0.18.0] 2025-01-10 ### Added - Add support for extensions and provide example extension as a template. diff --git a/README.md b/README.md index ad9efc1..ae88f86 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ You can use gersemi with a pre-commit hook by adding the following to `.pre-comm ```yaml repos: - repo: https://github.com/BlankSpruce/gersemi - rev: 0.18.0 + rev: 0.18.1 hooks: - id: gersemi ``` @@ -145,7 +145,7 @@ If you want to use extensions with pre-commit list them with [`additional_depend ```yaml repos: - repo: https://github.com/BlankSpruce/gersemi - rev: 0.18.0 + rev: 0.18.1 hooks: - id: gersemi additional_dependencies: diff --git a/extension-example/code_to_format/favour-expansion.cmake b/extension-example/code_to_format/favour-expansion.cmake index 3a11482..0d701f8 100644 --- a/extension-example/code_to_format/favour-expansion.cmake +++ b/extension-example/code_to_format/favour-expansion.cmake @@ -97,6 +97,35 @@ example_unnamed_positional_arguments( back_2 ) +# command with common keywords in standalone and in section mode +example_common_keywords_in_standalone_and_in_section( + front_1 + front_2 + OPTION_1 + OPTION_2 + ONE_VALUE_KEYWORD_2 foo + ONE_VALUE_KEYWORD_1 foo + MULTI_VALUE_KEYWORD_1 + foo + bar + baz + SECTION_KEYWORD + section_front_1 + OPTION_1 + OPTION_2 + ONE_VALUE_KEYWORD_2 foo + ONE_VALUE_KEYWORD_1 foo + MULTI_VALUE_KEYWORD_1 + foo + bar + baz + section_back_1 + section_back_2 + back_1 + back_2 + back_3 +) + # command with nested sections example_nested_sections( foo # level_0_arg_1 diff --git a/extension-example/code_to_format/favour-inlining.cmake b/extension-example/code_to_format/favour-inlining.cmake index 9954130..02b735c 100644 --- a/extension-example/code_to_format/favour-inlining.cmake +++ b/extension-example/code_to_format/favour-inlining.cmake @@ -78,6 +78,29 @@ example_unnamed_positional_arguments( back_2 ) +# command with common keywords in standalone and in section mode +example_common_keywords_in_standalone_and_in_section( + front_1 + front_2 + OPTION_1 + OPTION_2 + ONE_VALUE_KEYWORD_2 foo + ONE_VALUE_KEYWORD_1 foo + MULTI_VALUE_KEYWORD_1 foo bar baz + SECTION_KEYWORD + section_front_1 + OPTION_1 + OPTION_2 + ONE_VALUE_KEYWORD_2 foo + ONE_VALUE_KEYWORD_1 foo + MULTI_VALUE_KEYWORD_1 foo bar baz + section_back_1 + section_back_2 + back_1 + back_2 + back_3 +) + # command with nested sections example_nested_sections( foo # level_0_arg_1 diff --git a/extension-example/extension/gersemi_extension_example/__init__.py b/extension-example/extension/gersemi_extension_example/__init__.py index 4fb9355..83ed6f1 100644 --- a/extension-example/extension/gersemi_extension_example/__init__.py +++ b/extension-example/extension/gersemi_extension_example/__init__.py @@ -104,6 +104,42 @@ }, }, # + # Sometimes keywords have broad scope when used standalone but narrow scope + # when used in section. Example: + # + # install(TARGETS ... [EXPORT ] + # [RUNTIME_DEPENDENCIES ...|RUNTIME_DEPENDENCY_SET ] + # [...] + # [ ...]... + # [INCLUDES DESTINATION [ ...]] + # ) + # + # """ + # The first ... group applies to target Output + # Artifacts that do not have a dedicated group specified later + # in the same call. + # """ + # + "example_common_keywords_in_standalone_and_in_section": { + "front_positional_arguments": ["front_1", "front_2"], + "back_positional_arguments": ["back_1", "back_2", "back_3"], + "options": ["OPTION_1", "OPTION_2"], + "one_value_keywords": ["ONE_VALUE_KEYWORD_1", "ONE_VALUE_KEYWORD_2"], + "multi_value_keywords": [ + "MULTI_VALUE_KEYWORD_1", + "SECTION_KEYWORD", + ], + "sections": { + "SECTION_KEYWORD": { + "front_positional_arguments": ["section_front_1"], + "back_positional_arguments": ["section_back_1", "section_back_2"], + "options": ["OPTION_1", "OPTION_2"], + "one_value_keywords": ["ONE_VALUE_KEYWORD_1", "ONE_VALUE_KEYWORD_2"], + "multi_value_keywords": ["MULTI_VALUE_KEYWORD_1"], + } + }, + }, + # # Nested sections are supported but perhaps it's better to avoid designing # such commands. # diff --git a/gersemi/__version__.py b/gersemi/__version__.py index 70377c1..b9f5ff0 100644 --- a/gersemi/__version__.py +++ b/gersemi/__version__.py @@ -4,4 +4,4 @@ __license__ = "MPL 2.0" __title__ = "gersemi" __url__ = "https://github.com/BlankSpruce/gersemi" -__version__ = "0.18.0" +__version__ = "0.18.1" diff --git a/gersemi/ast_helpers.py b/gersemi/ast_helpers.py index cbdd65b..7aed4a7 100644 --- a/gersemi/ast_helpers.py +++ b/gersemi/ast_helpers.py @@ -94,3 +94,13 @@ def __call__(self, other): def is_one_of_keywords(keywords): return KeywordMatcher(keywords) + + +def make_tree(name: str): + return lambda children: Tree(name, children) + + +option_argument = make_tree("option_argument") +one_value_argument = make_tree("one_value_argument") +multi_value_argument = make_tree("multi_value_argument") +positional_arguments = make_tree("positional_arguments") diff --git a/gersemi/builtin_commands.py b/gersemi/builtin_commands.py index f4ac919..b7ec7c1 100644 --- a/gersemi/builtin_commands.py +++ b/gersemi/builtin_commands.py @@ -41,6 +41,11 @@ _FILE_SET_Any, "CXX_MODULES_BMI", ] +_Install_TARGETS_artifact_option_group = { + "options": ["OPTIONAL", "EXCLUDE_FROM_ALL", "NAMELINK_ONLY", "NAMELINK_SKIP"], + "one_value_keywords": ["DESTINATION", "COMPONENT", "NAMELINK_COMPONENT"], + "multi_value_keywords": ["PERMISSIONS", "CONFIGURATIONS"], +} _Install_IMPORTED_RUNTIME_ARTIFACTS_kinds = [ "LIBRARY", "RUNTIME", @@ -1275,27 +1280,17 @@ "signatures": { "TARGETS": { "sections": { - kind: { - "options": [ - "OPTIONAL", - "EXCLUDE_FROM_ALL", - "NAMELINK_ONLY", - "NAMELINK_SKIP", - ], - "one_value_keywords": [ - "DESTINATION", - "COMPONENT", - "NAMELINK_COMPONENT", - ], - "multi_value_keywords": ["PERMISSIONS", "CONFIGURATIONS"], - } + kind: _Install_TARGETS_artifact_option_group for kind in _Install_TARGETS_kinds }, + "options": _Install_TARGETS_artifact_option_group["options"], "one_value_keywords": [ + *_Install_TARGETS_artifact_option_group["one_value_keywords"], "EXPORT", "RUNTIME_DEPENDENCY_SET", ], "multi_value_keywords": [ + *_Install_TARGETS_artifact_option_group["multi_value_keywords"], "TARGETS", _INCLUDES_DESTINATION, "RUNTIME_DEPENDENCIES", @@ -1398,11 +1393,6 @@ } for kind in _Install_RUNTIME_DEPENDENCY_SET_kinds }, - "options": [ - "LIBRARY", - "RUNTIME", - "FRAMEWORK", - ], "multi_value_keywords": [ "PRE_INCLUDE_REGEXES", "PRE_EXCLUDE_REGEXES", @@ -1411,6 +1401,7 @@ "POST_INCLUDE_FILES", "POST_EXCLUDE_FILES", "DIRECTORIES", + "RUNTIME_DEPENDENCY_SET", *_Install_RUNTIME_DEPENDENCY_SET_kinds, ], }, diff --git a/gersemi/specializations/argument_aware_command_invocation_dumper.py b/gersemi/specializations/argument_aware_command_invocation_dumper.py index 953abf9..a542157 100644 --- a/gersemi/specializations/argument_aware_command_invocation_dumper.py +++ b/gersemi/specializations/argument_aware_command_invocation_dumper.py @@ -9,6 +9,10 @@ is_option_argument, is_positional_arguments, is_section, + option_argument, + one_value_argument, + multi_value_argument, + positional_arguments, ) from gersemi.base_command_invocation_dumper import BaseCommandInvocationDumper from gersemi.keywords import KeywordMatcher @@ -28,16 +32,6 @@ def is_non_empty_group(group: Sized) -> bool: return is_non_empty(group) -def make_tree(name: str): - return lambda children: Tree(name, children) - - -option_argument = make_tree("option_argument") -one_value_argument = make_tree("one_value_argument") -multi_value_argument = make_tree("multi_value_argument") -positional_arguments = make_tree("positional_arguments") - - class PositionalArguments(list): pass diff --git a/gersemi/specializations/section_aware_command_invocation_dumper.py b/gersemi/specializations/section_aware_command_invocation_dumper.py index 50c8952..dfd6b5b 100644 --- a/gersemi/specializations/section_aware_command_invocation_dumper.py +++ b/gersemi/specializations/section_aware_command_invocation_dumper.py @@ -2,9 +2,12 @@ from typing import Iterable, Mapping from lark import Tree from gersemi.ast_helpers import ( + is_one_value_argument, is_multi_value_argument, is_one_of_keywords, is_positional_arguments, + is_section, + positional_arguments, ) from gersemi.keywords import KeywordMatcher from .argument_aware_command_invocation_dumper import ( @@ -68,16 +71,75 @@ def _split_multi_value_argument(self, tree): return Tree("section", subarguments) + def _is_among_section_keywords(self, section_matcher, argument): + if section_matcher is None: + return False + + with self._update_section_characteristics(section_matcher): + for keywords in ( + self.options, + self.one_value_keywords, + self.multi_value_keywords, + ): + if is_one_of_keywords(keywords)(argument.children[0]): + return True + + return False + + def _fix_back_positional_arguments(self, section): + if not is_section(section): + return section + + pivot = len(self.back_positional_arguments) + if pivot == 0: + return section + + *front, last = section.children + if is_one_value_argument(last) or is_multi_value_argument(last): + last_front, *last_rest = last.children + left_in_place, back_positional_arguments = ( + last_rest[:-pivot], + last_rest[-pivot:], + ) + last.children = [last_front, *left_in_place] + section.children = [ + *front, + last, + positional_arguments(back_positional_arguments), + ] + + return section + + def _form_sections(self, arguments): + result = [] + section_matcher = None + for argument in arguments: + if self._is_among_section_keywords(section_matcher, argument): + result[-1].children.append(argument) + else: + if section_matcher is not None: + with self._update_section_characteristics(section_matcher): + result[-1] = self._fix_back_positional_arguments(result[-1]) + + if len(argument.children) > 0: + section_matcher = self._get_matcher(argument.children[0]) + else: + section_matcher = None + + result.append(argument) + + return self._fix_back_positional_arguments(result) + def _split_arguments(self, arguments): preprocessed = super()._split_arguments(arguments) - return [ + return self._form_sections( ( self._split_multi_value_argument(child) if is_multi_value_argument(child) else child ) for child in preprocessed - ] + ) def section(self, tree): result = self._try_to_format_into_single_line(tree.children, separator=" ") diff --git a/tests/formatter/issue_0051_install_targets.in.cmake b/tests/formatter/issue_0051_install_targets.in.cmake new file mode 100644 index 0000000..b95f189 --- /dev/null +++ b/tests/formatter/issue_0051_install_targets.in.cmake @@ -0,0 +1,25 @@ +### {list_expansion: favour-expansion} +# https://cmake.org/cmake/help/latest/command/install.html#signatures +# +# The first ... group applies to target Output Artifacts +# that do not have a dedicated group specified later in the same call. + +install(TARGETS ${LIB_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib) + +install(TARGETS ${LIB_NAME} ${LIB2_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib) + +install(TARGETS ${LIB_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib PERMISSIONS foo bar baz) + +install(TARGETS ${LIB_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib RUNTIME COMPONENT TARGET_COMPONENT DESTINATION lib) + +install(TARGETS ${LIB_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib PERMISSIONS foo bar baz RUNTIME COMPONENT TARGET_COMPONENT DESTINATION lib PERMISSIONS foo bar baz) + +install(TARGETS ${LIB_NAME} ${LIB2_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib RUNTIME COMPONENT TARGET_COMPONENT DESTINATION lib) + +install(TARGETS ${LIB_NAME} ${LIB2_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib RUNTIME COMPONENT TARGET_COMPONENT DESTINATION lib LIBRARY COMPONENT TARGET_COMPONENT DESTINATION lib) + +install(RUNTIME_DEPENDENCY_SET deps + RUNTIME DESTINATION bin COMPONENT bin2 + LIBRARY DESTINATION lib COMPONENT lib2 + FRAMEWORK DESTINATION fw COMPONENT fw2 + ) diff --git a/tests/formatter/issue_0051_install_targets.out.cmake b/tests/formatter/issue_0051_install_targets.out.cmake new file mode 100644 index 0000000..1178971 --- /dev/null +++ b/tests/formatter/issue_0051_install_targets.out.cmake @@ -0,0 +1,92 @@ +# https://cmake.org/cmake/help/latest/command/install.html#signatures +# +# The first ... group applies to target Output Artifacts +# that do not have a dedicated group specified later in the same call. + +install(TARGETS ${LIB_NAME} COMPONENT TARGET_COMPONENT DESTINATION lib) + +install( + TARGETS + ${LIB_NAME} + ${LIB2_NAME} + COMPONENT TARGET_COMPONENT + DESTINATION lib +) + +install( + TARGETS + ${LIB_NAME} + COMPONENT TARGET_COMPONENT + DESTINATION lib + PERMISSIONS + foo + bar + baz +) + +install( + TARGETS + ${LIB_NAME} + COMPONENT TARGET_COMPONENT + DESTINATION lib + RUNTIME + COMPONENT TARGET_COMPONENT + DESTINATION lib +) + +install( + TARGETS + ${LIB_NAME} + COMPONENT TARGET_COMPONENT + DESTINATION lib + PERMISSIONS + foo + bar + baz + RUNTIME + COMPONENT TARGET_COMPONENT + DESTINATION lib + PERMISSIONS + foo + bar + baz +) + +install( + TARGETS + ${LIB_NAME} + ${LIB2_NAME} + COMPONENT TARGET_COMPONENT + DESTINATION lib + RUNTIME + COMPONENT TARGET_COMPONENT + DESTINATION lib +) + +install( + TARGETS + ${LIB_NAME} + ${LIB2_NAME} + COMPONENT TARGET_COMPONENT + DESTINATION lib + RUNTIME + COMPONENT TARGET_COMPONENT + DESTINATION lib + LIBRARY + COMPONENT TARGET_COMPONENT + DESTINATION lib +) + +install( + RUNTIME_DEPENDENCY_SET + deps + RUNTIME + DESTINATION bin + COMPONENT bin2 + LIBRARY + DESTINATION lib + COMPONENT lib2 + FRAMEWORK + DESTINATION fw + COMPONENT fw2 +)