Skip to content

Commit

Permalink
Add support for canonical casing
Browse files Browse the repository at this point in the history
  • Loading branch information
BlankSpruce committed May 27, 2024
1 parent 267e366 commit 8ff9872
Show file tree
Hide file tree
Showing 38 changed files with 336 additions and 147 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## [0.13.0] 2024-05-24
### Added
- support for using canonical casing of custom commands (#21)

### Changed
- official CMake commands will be formatted with their canonical casing (like `FetchContent_Declare`) instead of lower case version with the following deliberate exceptions:
- `check_fortran_function_exists`
- `check_include_file_cxx`
- `check_include_file`
- `check_include_files`
- `check_library_exists`
- `check_struct_has_member`
- `check_variable_exists`

### Fixed
- use specialized formatting of some previously omitted official commands
- improve consistency of `set_package_properties` with similar commands

## [0.12.1] 2024-03-27
- improve `find_package` formatting around `REQUIRED` keyword (#20)

Expand Down
35 changes: 30 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,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.12.1
rev: 0.13.0
hooks:
- id: gersemi
```
Expand All @@ -89,10 +89,12 @@ Update `rev` to relevant version used in your repository. For more details refer

## Formatting

The key goal is for the tool to "just work" and to have as little configuration as possible so that you don't have to worry about fine-tuning formatter to your needs - as long as you embrace the `gersemi` style of formatting, similarly as `black` or `gofmt` do their job. Currently only line length can be changed with `80` as default value. Currently the basic assumption is that code to format is valid CMake language code - `gersemi` might be able to format some particular cases of invalid code but it's not guaranteed and it shouldn't be relied upon. Moreover only commands from CMake 3.0 onwards are supported and will be formatted properly - for instance [`exec_program` has been deprecated since CMake 3.0](https://cmake.org/cmake/help/latest/command/exec_program.html) so it won't be formatted. Be warned though it's not production ready so the changes to code might be destructive and you should always have a backup (version control helps a lot).
The key goal is for the tool to "just work" and to have as little configuration as possible so that you don't have to worry about fine-tuning formatter to your needs - as long as you embrace the `gersemi` style of formatting, similarly as `black` or `gofmt` do their job. The basic assumption is that code to format is valid CMake language code - `gersemi` might be able to format some particular cases of invalid code but it's not guaranteed and it shouldn't be relied upon. Moreover only commands from CMake 3.0 onwards are supported and will be formatted properly - for instance [`exec_program` has been deprecated since CMake 3.0](https://cmake.org/cmake/help/latest/command/exec_program.html) so it won't be formatted. Changes to code might be destructive and you should always have a backup (version control helps a lot).

### Style

`gersemi` in general will use canonical casing as it's defined in official CMake documentation like `FetchContent_Declare`. There are a few deliberate exceptions for which lower case name was chosen to provide broader consistency with other CMake commands. In case of unknown commands, not provided through `definitions`, lower case will be used.

#### Default style `favour-inlining`

`gersemi` will try to format the code in a way that respects set character limit for single line and only break line whenever necessary with one exception. The commands that have a group of parameters that aren't attached to any specific keyword (like `set` or `list(APPEND)`) will be broken into multiple lines when there are more than 4 arguments in that group. The exception to the rule is made as a heuristic to avoid large local diff when the given command won't fit into maximum line length.
Expand Down Expand Up @@ -385,9 +387,9 @@ add_custom_command(

### Let's make a deal

It's possible to provide reasonable formatting for custom commands. However on language level there are no hints available about supported keywords for given command so `gersemi` has to generate specialized formatter. To do that custom command definition is necessary which should be provided with `--definitions`. There are limitations though since it'd probably require full-blown CMake language interpreter to do it in every case so let's make a deal: if your custom command definition (function or macro) uses `cmake_parse_arguments` and does it in obvious manner such specialized formatter will be generated. For instance this definition is okay (you can find other examples in `tests/custom_command_formatting/`):
It's possible to provide reasonable formatting for custom commands. However on language level there are no hints available about supported keywords for given command so `gersemi` has to generate specialized formatter. To do that custom command definition is necessary which should be provided with `--definitions`. There are limitations though since it'd probably require full-blown CMake language interpreter to do it in every case so let's make a deal: if your custom command definition (function or macro) uses `cmake_parse_arguments` and does it in obvious manner such specialized formatter will be generated. Name casing used in command definition will be considered canonical for custom command (in the example below canonical casing will be `Seven_Samurai`). For instance this definition is okay (you can find other examples in `tests/custom_command_formatting/`):
```cmake
function(SEVEN_SAMURAI some standalone arguments)
function(Seven_Samurai some standalone arguments)
set(options KAMBEI KATSUSHIRO)
set(oneValueArgs GOROBEI HEIHACHI KYUZO)
set(multiValueArgs SHICHIROJI KIKUCHIYO)
Expand All @@ -406,7 +408,7 @@ endfunction()

With this definition available it's possible to format code like so:
```cmake
seven_samurai(
Seven_Samurai(
three
standalone
arguments
Expand All @@ -422,6 +424,29 @@ seven_samurai(

Otherwise `gersemi` will fallback to only fixing indentation and preserving original formatting. If you find these limitations too strict let me know about your case.

#### How to format custom commands for which path to definition can't be guaranteed to be stable? (e.g external dependencies not managed by CMake)

You can provide stub definitions that will be used only as an input for gersemi. Example:
```yaml
# ./.gersemirc
definitions: [./src/cmake/stubs, ...] # ... other paths that might contain actual definitions
line_length: 120
list_expansion: favour-expansion
```

```cmake
# ./src/cmake/stubs/try_to_win_best_picture_academy_award.cmake
# A stub for some external command out of our control
function(try_to_win_best_picture_academy_award)
# gersemi: hint { CAST: pairs, SUMMARY: command_line }
set(options FOREIGN_LANGUAGE)
set(oneValueArgs GENRE YEAR)
set(multiValueArgs DIRECTORS CAST SUMMARY)
cmake_parse_arguments(_ "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
endfunction()
```

#### `gersemi: ignore`
If your definition should be ignored for purposes of generating specialized formatter you can use `# gersemi: ignore` at the beginning of the custom command:
```cmake
Expand Down
2 changes: 1 addition & 1 deletion gersemi/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
__license__ = "MPL 2.0"
__title__ = "gersemi"
__url__ = "https://github.com/BlankSpruce/gersemi"
__version__ = "0.12.1"
__version__ = "0.13.0"
5 changes: 3 additions & 2 deletions gersemi/base_command_invocation_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ def _inlining_condition(self, arguments):
return all(size <= 4 for size in group_sizes)

def format_command(self, tree):
identifier, arguments = tree.children
raw_identifier, arguments = tree.children
identifier = self.format_command_name(raw_identifier)
arguments = self._preprocess_arguments(arguments)
begin = f"{identifier}("
end = ")"
if self._inlining_condition(arguments):
result = self._try_to_format_into_single_line(
arguments.children, separator=" ", prefix=f"{identifier}(", postfix=")"
arguments.children, separator=" ", prefix=begin, postfix=end
)
if result is not None:
return result
Expand Down
3 changes: 3 additions & 0 deletions gersemi/base_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,6 @@ def select_inlining_strategy(self):
yield self
finally:
self.favour_expansion = old

def format_command_name(self, identifier):
return identifier.lower()
58 changes: 42 additions & 16 deletions gersemi/builtin_commands
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#### Legend
#### (&): canonical name used different than in the documentation

# cmake-commands
## Scripting Commands
block
Expand Down Expand Up @@ -163,6 +166,9 @@ fixup_bundle_item
verify_bundle_prerequisites
verify_bundle_symlinks

### CheckCompilerFlag
check_compiler_flag

### CheckCCompilerFlag
check_c_compiler_flag

Expand All @@ -188,10 +194,12 @@ check_cxx_symbol_exists
check_fortran_compiler_flag

### CheckFortranFunctionExists
# (&)
check_fortran_function_exists

### CheckFortranSourceCompiles
check_fortran_source_compiles

### CheckFortranSourceRuns
check_fortran_source_runs

Expand All @@ -202,18 +210,22 @@ check_function_exists
check_ipo_supported

### CheckIncludeFileCXX
# (&)
check_include_file_cxx

### CheckIncludeFile
# (&)
check_include_file

### CheckIncludeFiles
# (&)
check_include_files

### CheckLanguage
check_language

### CheckLibraryExists
# (&)
check_library_exists

### CheckLinkerFlag
Expand Down Expand Up @@ -243,7 +255,14 @@ check_pie_supported
### CheckPrototypeDefinition
check_prototype_definition

### CheckSourceCompiles
check_source_compiles

### CheckSourceRuns
check_source_runs

### CheckStructHasMember
# (&)
check_struct_has_member

### CheckSymbolExists
Expand All @@ -253,6 +272,7 @@ check_symbol_exists
check_type_size

### CheckVariableExists
# (&)
check_variable_exists

### CMakeAddFortranSubdirectory
Expand Down Expand Up @@ -340,37 +360,43 @@ install_qt4_plugin_path
### Documentation

### ExternalData
externaldata_expand_arguments
externaldata_add_test
externaldata_add_target
ExternalData_Expand_Arguments
ExternalData_Add_Test
ExternalData_Add_Target

### ExternalProject
externalproject_add
externalproject_add_step
externalproject_add_stepdependencies
externalproject_add_steptargets
externalproject_get_property
ExternalProject_Add
ExternalProject_Add_Step
ExternalProject_Add_Stepdependencies
ExternalProject_Add_Steptargets
ExternalProject_Get_Property

### FeatureSummary
add_feature_info
feature_summary
print_disabled_features
print_enabled_features
set_feature_info
set_package_info
set_package_properties
add_feature_info

### FetchContent
fetchcontent_declare
fetchcontent_getproperties
fetchcontent_makeavailable
fetchcontent_populate
FetchContent_Declare
FetchContent_GetProperties
FetchContent_MakeAvailable
FetchContent_Populate
FetchContent_SetPopulated

### FindPackageHandleStandardArgs
find_package_check_version
find_package_handle_standard_args

### FindPackageMessage
find_package_message

### FortranCInterface
fortrancinterface_header
fortrancinterface_verify
FortranCInterface_HEADER
FortranCInterface_VERIFY

### GenerateExportHeader
generate_export_header
Expand All @@ -387,7 +413,7 @@ gp_resolved_file_type
gp_resolve_item

### GNUInstallDirs
gnuinstalldirs_get_absolute_install_dir
GNUInstallDirs_get_absolute_install_dir

### GoogleTest
gtest_add_tests
Expand Down
18 changes: 18 additions & 0 deletions gersemi/builtin_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os


def is_not_comment_or_empty(s):
return not (s.startswith("#") or len(s) == 0)


def get_builtin_commands():
HERE = os.path.dirname(os.path.realpath(__file__))

with open(os.path.join(HERE, "builtin_commands"), "r", encoding="utf-8") as f:
return {
item.lower(): item
for item in filter(is_not_comment_or_empty, f.read().splitlines())
}


BUILTIN_COMMANDS = get_builtin_commands()
3 changes: 1 addition & 2 deletions gersemi/cmake.lark
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ block : _foreach_block
| _block_block
block_body : (newline_or_gap _file_element)* newline_or_gap

terminal_as_rule{term}: term
command_template{term}: terminal_as_rule{term} _invocation_part -> command_invocation
command_template{term}: term _invocation_part -> command_invocation
element_template{term}: command_template{term} [line_comment] -> command_element

_block_template{start_term, end_term}: element_template{start_term} block_body element_template{end_term}
Expand Down
36 changes: 28 additions & 8 deletions gersemi/command_invocation_dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from functools import lru_cache
from lark import Tree
from gersemi.base_command_invocation_dumper import BaseCommandInvocationDumper
from gersemi.builtin_commands import BUILTIN_COMMANDS
from gersemi.command_invocation_dumpers.ctest_command_dumpers import (
ctest_command_mapping,
)
Expand All @@ -20,8 +21,10 @@
from gersemi.command_invocation_dumpers.specialized_dumpers import (
create_specialized_dumper,
)
from gersemi.keywords import Keywords

BUILTIN_COMMAND_MAPPING = {

BUILTIN_COMMAND_DUMPERS = {
**scripting_command_mapping,
**project_command_mapping,
**ctest_command_mapping,
Expand All @@ -37,6 +40,13 @@ class Impl(patch, old_class):
return Impl


def add_canonical_name(dumper_class, desired_canonical_name):
class Impl(dumper_class):
canonical_name = desired_canonical_name

return Impl


class CommandInvocationDumper(
PreservingCommandInvocationDumper, BaseCommandInvocationDumper
):
Expand All @@ -50,13 +60,21 @@ def patched(self, patch):
finally:
self.__class__ = old_class # pylint: disable=invalid-class-object,

def _get_patch(self, command_name):
if command_name in BUILTIN_COMMAND_MAPPING:
return BUILTIN_COMMAND_MAPPING[command_name]
def _get_patch(self, raw_command_name):
command_name = raw_command_name.lower()
if command_name in BUILTIN_COMMANDS:
canonical_name = BUILTIN_COMMANDS[command_name]

if command_name in BUILTIN_COMMAND_DUMPERS:
return add_canonical_name(
BUILTIN_COMMAND_DUMPERS[command_name], canonical_name
)

return create_specialized_dumper(canonical_name, (), Keywords())

if command_name in self.custom_command_definitions:
arguments = self.custom_command_definitions[command_name]
return create_specialized_dumper(*arguments)
canonical_name, arguments = self.custom_command_definitions[command_name]
return create_specialized_dumper(canonical_name, *arguments)

return None

Expand All @@ -70,6 +88,8 @@ def command_invocation(self, tree):

def custom_command(self, tree):
_, command_name, arguments, *_ = tree.children
if command_name in self.custom_command_definitions:
return self.visit(Tree("command_invocation", [command_name, arguments]))
if command_name.lower() in self.custom_command_definitions:
return self.visit(
Tree("command_invocation", [command_name.lower(), arguments])
)
return super().custom_command(tree)
Loading

0 comments on commit 8ff9872

Please sign in to comment.