├── .github └── workflows │ ├── pythonapp-linux.yml │ └── pythonapp-windows.yml ├── .gitignore ├── LICENSE ├── README.adoc ├── cmake_tidy ├── __init__.py ├── __main__.py ├── commands │ ├── __init__.py │ ├── analyze │ │ ├── __init__.py │ │ └── analyze_command.py │ ├── command.py │ └── format │ │ ├── __init__.py │ │ ├── format_command.py │ │ ├── format_configuration.py │ │ └── output_writer.py ├── formatting │ ├── __init__.py │ ├── cmake_format_dispatcher.py │ ├── cmake_formatter.py │ ├── settings.json │ ├── settings_reader.py │ └── utils │ │ ├── __init__.py │ │ ├── format_arguments.py │ │ ├── format_bracket_argument.py │ │ ├── format_command_invocation.py │ │ ├── format_end_command_invocation.py │ │ ├── format_file.py │ │ ├── format_newline.py │ │ ├── format_spaces.py │ │ ├── format_start_command_invocation.py │ │ ├── format_unquoted_argument.py │ │ ├── invocation │ │ ├── __init__.py │ │ ├── command_formatter.py │ │ ├── command_realign_modifier.py │ │ ├── command_splitter.py │ │ ├── condition_formatter.py │ │ ├── invocation_formatter.py │ │ ├── invocation_wrapper.py │ │ └── line_comments_formatter.py │ │ ├── line_length_calculator.py │ │ ├── single_indent.py │ │ ├── tokens.py │ │ └── updaters │ │ ├── __init__.py │ │ ├── command_invocatin_state_updater.py │ │ └── keyword_state_updater.py ├── lexical_data │ ├── __init__.py │ ├── elements.py │ ├── keyword_list.json │ └── keyword_verifier.py ├── parsing │ ├── __init__.py │ ├── cmake_lexer.py │ └── cmake_parser.py ├── run.py ├── utils │ ├── __init__.py │ ├── app_configuration │ │ ├── __init__.py │ │ └── configuration.py │ ├── command_line_handling │ │ ├── __init__.py │ │ ├── arguments.py │ │ └── command_line_parser.py │ ├── diff.py │ ├── exit_codes.py │ └── proxy_visitor.py └── version.py ├── doc └── config.adoc ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── integration ├── __init__.py ├── approvaltests_config.json ├── approved_files │ ├── TestCMakeTidy.test_incorrect_command_should_print_error_with_usage_help.approved.txt │ ├── TestCMakeTidy.test_version_argument_should_provide_correct_tool_version.approved.txt │ ├── TestCMakeTidyAnalyze.test_analyze_command_help_shown.approved.txt │ ├── TestCMakeTidyAnalyze.test_analyze_should_correctly_print_version.approved.txt │ ├── TestCMakeTidyFormat.test_format_command_help_shown.approved.txt │ ├── TestCMakeTidyFormat.test_format_inplace_simple_file_with_verbose_option.approved.txt │ ├── TestCMakeTidyFormat.test_format_inplace_with_error_should_inform_about_failure_and_keep_initial_file.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_correctly_print_version.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_dump_config_only_configuration_to_stdout_by_default.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_dump_full_config_even_if_file_overrides_only_one.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_fail_with_warning_about_incorrect_settings_when_dump_invoked.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_fail_with_warning_about_incorrect_settings_when_trying_to_format.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_provide_unified_diff_to_stdout.approved.txt │ ├── TestCMakeTidyFormat.test_format_should_return_error_when_file_is_read_only_and_inplace_param_is_used.approved.txt │ ├── TestCMakeTidyFormat.test_incorrect_command_should_print_error_with_usage_help.approved.txt │ ├── TestFileFormatting.test_condition_formatting_with_different_kinds_of_parentheses.approved.txt │ ├── TestFileFormatting.test_format_against_newline_violations.approved.txt │ ├── TestFileFormatting.test_format_against_newline_violations_with_custom_settings.approved.txt │ ├── TestFileFormatting.test_format_bracket_arguments_handling.approved.txt │ ├── TestFileFormatting.test_format_command_should_print_file_to_output.approved.txt │ ├── TestFileFormatting.test_format_indentation_of_basic_invocations.approved.txt │ ├── TestFileFormatting.test_format_indentation_when_spaces_after_command_name_are_present.approved.txt │ ├── TestFileFormatting.test_format_line_splitting.approved.txt │ ├── TestFileFormatting.test_format_tabs_with_spaces_replacement.approved.txt │ ├── TestFileFormatting.test_formatting_complicated_conditions.approved.txt │ ├── TestFileFormatting.test_formatting_complicated_conditions_splitting_after_operator.approved.txt │ ├── TestFileFormatting.test_formatting_file_with_multiple_settings.approved.txt │ ├── TestFileFormatting.test_formatting_of_install_commands.approved.txt │ ├── TestFileFormatting.test_formatting_with_tabs.approved.txt │ ├── TestFileFormatting.test_handling_of_single_line_comments_within_different_parts_of_cmake_file.approved.txt │ ├── TestFileFormatting.test_real_implementation_of_feature_in_cmake.approved.txt │ └── TestFileFormatting.test_real_implementation_of_feature_in_cmake_split_keywords_and_values.approved.txt ├── input_files │ ├── arguments.cmake │ ├── comments.cmake │ ├── complicated_conditions.cmake │ ├── conditions_with_parentheses.cmake │ ├── first_example.cmake │ ├── incorrect_file.cmake │ ├── indentations.cmake │ ├── install.cmake │ ├── line_length_handling.cmake │ ├── newlines_violations.cmake │ ├── set_of_functions.cmake │ ├── spaces_violations.cmake │ └── target_setting.cmake ├── test_cmake_tidy.py ├── test_cmake_tidy_analyze.py ├── test_cmake_tidy_format.py ├── test_cmake_tidy_format_discover_config_file.py ├── test_file_formatting.py ├── test_integration_base.py └── utils.py └── unit ├── __init__.py ├── parser_composite_elements.py ├── test_cmake_format_dispatcher.py ├── test_cmake_formatter.py ├── test_cmake_formatter_arguments.py ├── test_cmake_formatter_comments_within_arguments.py ├── test_cmake_formatter_conditional_invocation.py ├── test_cmake_formatter_conditionals_with_parentheses.py ├── test_cmake_formatter_elements_interactions.py ├── test_cmake_formatter_invocation_splitting.py ├── test_cmake_formatter_invocation_wrapping.py ├── test_cmake_formatter_invocations.py ├── test_cmake_parser.py ├── test_cmake_parser_command_invocation.py ├── test_configuration.py ├── test_elements.py ├── test_formatting_settings_reader.py ├── test_keyword_verifier.py ├── test_line_length_calculator.py ├── test_proxy_visitor.py └── test_tokens.py /.github/workflows/pythonapp-linux.yml: -------------------------------------------------------------------------------- 1 | name: linux 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 24 | - name: Test with pytest 25 | run: | 26 | pip install pytest 27 | pytest 28 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp-windows.yml: -------------------------------------------------------------------------------- 1 | name: windows 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt 20 | - name: Lint with flake8 21 | run: | 22 | pip install flake8 23 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 24 | - name: Test with pytest 25 | run: | 26 | pip install pytest 27 | pytest 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # ply 107 | *parser.out 108 | *parsetab.py 109 | 110 | # approvaltests 111 | *.received.txt 112 | 113 | # pycharm 114 | .idea 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maciej Patro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = cmake-tidy 2 | :toc: 3 | 4 | :numbered: 5 | 6 | == Introduction 7 | The goal of project is to provide a set of tools that will ease the development process for build systems code written 8 | in `CMake`. Main ideas of the tool are but not limited to: 9 | 10 | - `CMake` code formatting - based on language syntax analysis 11 | - modern `CMake` static analysis - basic code analysis with possible auto-fixing. 12 | - large flexibility of configuration - as coding styles differ quite a lot. 13 | - easy to use - mimic usage and parameters with other state-of-the-art tools to reduce learning curve for users. 14 | 15 | == Application usage 16 | 17 | `cmake-tidy` is a command-line application written in `python3`. 18 | 19 | Available subcommands: 20 | [source,text] 21 | ---- 22 | usage: cmake-tidy [-h] {format} ... 23 | 24 | optional arguments: 25 | -h, --help show this help message and exit 26 | 27 | sub-commands: 28 | {format} see "cmake-tidy --help" to read more about a specific 29 | sub-command. 30 | format format file to align it to standard 31 | ---- 32 | 33 | === `format` subcommand 34 | 35 | ==== Usage 36 | 37 | [source,text] 38 | ---- 39 | usage: cmake-tidy format [-h] [--dump-config] [-i] [--diff] [--verbose] 40 | [input [input ...]] 41 | 42 | positional arguments: 43 | input CMake file to be formatted 44 | 45 | optional arguments: 46 | -h, --help show this help message and exit 47 | --dump-config Dump to stdout current settings. Script tries to read 48 | settings from `.cmake-tidy.json` or provides default 49 | settings. Precedence of searching `.cmake-tidy.json` is 50 | described on github 51 | -i, --inplace Inplace edit specified file 52 | --diff Print to stdout unified diff between original file and 53 | formatted version. 54 | --verbose Print to stdout information about formatted file 55 | ---- 56 | 57 | ==== link:doc/config.adoc[Configuration] 58 | 59 | User can provide alternative settings to an application by providing custom `.cmake-tidy.json` file. 60 | For more information about available parameters and values visit link:doc/config.adoc[*here*]. 61 | 62 | === `analyze` subcommand 63 | 64 | Introduction is ongoing. 65 | 66 | == Known limitations/bugs 67 | 68 | * Application can format only valid cmake files (Syntax errors cause application to exit without 69 | modifying content of formatted file). 70 | * Support for `deprecated` keywords/properties for older version of cmake than specified in `--version` 71 | command might be limited. If you need to support them please provide pull request or specify them as custom 72 | keywords in `.cmake-tidy.json` config file. 73 | * Line continuation `\` handling is not implemented. 74 | 75 | == Other information 76 | 77 | === Installation 78 | 79 | The tool is available throught python package index using command: 80 | 81 | [source,shell] 82 | ---- 83 | python3 -m pip install cmake-tidy 84 | ---- 85 | 86 | To confirm installation was successful you can run: 87 | 88 | [source,shell] 89 | ---- 90 | cmake-tidy -v 91 | ---- 92 | 93 | === CI status 94 | 95 | Automated testing is done with workflows: 96 | 97 | * `ubuntu-lastest, python-3.8` image:https://github.com/MaciejPatro/cmake-tidy/workflows/linux/badge.svg[Status] 98 | * `windows-lastest, python-3.8` image:https://github.com/MaciejPatro/cmake-tidy/workflows/windows/badge.svg[Status] 99 | -------------------------------------------------------------------------------- /cmake_tidy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaciejPatro/cmake-tidy/ddab3d9c6dd1a6c9cfa47bff5a9f120defea9e6a/cmake_tidy/__init__.py -------------------------------------------------------------------------------- /cmake_tidy/__main__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.run import main 8 | 9 | if __name__ == "__main__": 10 | main() 11 | -------------------------------------------------------------------------------- /cmake_tidy/commands/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.commands.command import Command 8 | -------------------------------------------------------------------------------- /cmake_tidy/commands/analyze/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Maciej Patro (maciej.patro@gmail.com) 2 | # MIT License 3 | 4 | 5 | -------------------------------------------------------------------------------- /cmake_tidy/commands/analyze/analyze_command.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.commands import Command 8 | from cmake_tidy.utils import ExitCodes 9 | 10 | 11 | class AnalyzeCommand(Command): 12 | __DESCRIPTION = 'analyze file to find violations against selected rules' 13 | 14 | def __init__(self, parser): 15 | super().__init__(parser, 'analyze', AnalyzeCommand.__DESCRIPTION) 16 | 17 | def execute_command(self, args) -> int: 18 | return ExitCodes.SUCCESS 19 | -------------------------------------------------------------------------------- /cmake_tidy/commands/command.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import sys 8 | from abc import ABC, abstractmethod 9 | 10 | from cmake_tidy.utils import ExitCodes 11 | 12 | 13 | class Command(ABC): 14 | def __init__(self, parser, command_name: str, description: str): 15 | self._command_parser = parser.add_parser(name=command_name, help=description) 16 | self._command_parser.set_defaults(func=self.execute_command) 17 | self._command_name = command_name 18 | 19 | @abstractmethod 20 | def execute_command(self, args) -> int: 21 | pass 22 | 23 | def _handle_error(self, raised_error: Exception) -> int: 24 | print(f'cmake-tidy {self._command_name}: {str(raised_error)}', file=sys.stderr) 25 | return ExitCodes.FAILURE 26 | -------------------------------------------------------------------------------- /cmake_tidy/commands/format/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.commands.format.format_configuration import FormatConfiguration 8 | from cmake_tidy.commands.format.output_writer import OutputWriter 9 | 10 | 11 | def try_create_configuration(arguments) -> FormatConfiguration: 12 | return FormatConfiguration(arguments=dict(vars(arguments))) 13 | -------------------------------------------------------------------------------- /cmake_tidy/commands/format/format_command.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import json 8 | from copy import copy 9 | from pathlib import Path 10 | 11 | from cmake_tidy.commands import Command 12 | from cmake_tidy.commands.format import try_create_configuration, FormatConfiguration, OutputWriter 13 | from cmake_tidy.formatting import try_read_settings, CMakeFormatter 14 | from cmake_tidy.parsing import CMakeParser 15 | from cmake_tidy.utils.command_line_handling import arguments 16 | from cmake_tidy.utils import ExitCodes 17 | from cmake_tidy.utils.diff import get_unified_diff 18 | 19 | 20 | class FormatCommand(Command): 21 | __DESCRIPTION = 'format file to align it to standard' 22 | 23 | def __init__(self, parser): 24 | super().__init__(parser, 'format', FormatCommand.__DESCRIPTION) 25 | 26 | arguments.dump_config(self._command_parser) 27 | arguments.inplace(self._command_parser) 28 | arguments.diff(self._command_parser) 29 | arguments.verbose(self._command_parser) 30 | arguments.input_data(self._command_parser) 31 | 32 | def execute_command(self, args) -> int: 33 | if args.dump_config: 34 | return self.__dump_config(args) 35 | return self.__format_files(args) 36 | 37 | def __format_files(self, args): 38 | try: 39 | self.__try_formatting_all_files(args) 40 | except Exception as error: 41 | return self._handle_error(error) 42 | return ExitCodes.SUCCESS 43 | 44 | def __dump_config(self, args): 45 | try: 46 | self.__try_dump_config(args) 47 | except Exception as error: 48 | return self._handle_error(error) 49 | return ExitCodes.SUCCESS 50 | 51 | def __try_formatting_all_files(self, args): 52 | if not args.input: 53 | raise ValueError('Error - incorrect \"input\" - please specify existing file to be formatted') 54 | 55 | current_args = copy(args) 56 | for filename in args.input: 57 | current_args.input = filename 58 | self.__try_formatting_a_file(current_args) 59 | 60 | def __try_formatting_a_file(self, current_args): 61 | config = try_create_configuration(current_args) 62 | self.__print_filename_if_needed(config) 63 | formatted_file = self.__try_format_file(config) 64 | if current_args.diff: 65 | print(get_unified_diff(config.input, formatted_file, config.file)) 66 | else: 67 | self.__try_output_formatted_file(config, formatted_file) 68 | 69 | @staticmethod 70 | def __print_filename_if_needed(config): 71 | if config.verbose: 72 | print(f'Formatting file: {config.file}') 73 | 74 | @staticmethod 75 | def __try_dump_config(args): 76 | filepath = Path(args.input[0]) if args.input else None 77 | print(json.dumps(try_read_settings(filepath), indent=2)) 78 | 79 | @staticmethod 80 | def __try_format_file(configuration: FormatConfiguration) -> str: 81 | parsed_input = CMakeParser().parse(configuration.input) 82 | format_settings = try_read_settings(configuration.file) 83 | return CMakeFormatter(format_settings).format(parsed_input) 84 | 85 | @staticmethod 86 | def __try_output_formatted_file(configuration: FormatConfiguration, formatted_file: str) -> None: 87 | OutputWriter(configuration).write(formatted_file) 88 | -------------------------------------------------------------------------------- /cmake_tidy/commands/format/format_configuration.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from pathlib import Path 8 | 9 | from cmake_tidy.utils.app_configuration.configuration import Configuration, ConfigurationError 10 | 11 | 12 | class FormatConfiguration(Configuration): 13 | def __init__(self, arguments: dict): 14 | super().__init__(arguments) 15 | self.__input_data = self.__initialize_input(arguments) 16 | 17 | @property 18 | def input(self) -> str: 19 | return self.__input_data 20 | 21 | @property 22 | def inplace(self) -> str: 23 | return self._config.get(self._property_name()) is True 24 | 25 | @property 26 | def file(self) -> Path: 27 | return Path(self._config['input']) 28 | 29 | @property 30 | def verbose(self) -> bool: 31 | return self._config.get(self._property_name()) is True 32 | 33 | @property 34 | def command(self) -> str: 35 | return 'format' 36 | 37 | def __initialize_input(self, arguments) -> str: 38 | return self.__load_input_data(arguments) 39 | 40 | @staticmethod 41 | def __load_input_data(arguments) -> str: 42 | try: 43 | return Path(arguments['input']).read_text() 44 | except Exception: 45 | raise ConfigurationError('Error - incorrect \"input\" - please specify existing file to be formatted') 46 | -------------------------------------------------------------------------------- /cmake_tidy/commands/format/output_writer.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from pathlib import Path 8 | 9 | from cmake_tidy.commands.format import FormatConfiguration 10 | 11 | 12 | class WriterError(Exception): 13 | pass 14 | 15 | 16 | def write_to_file(file: Path, data: str): 17 | with file.open('w') as file: 18 | file.write(data) 19 | 20 | 21 | class OutputWriter: 22 | def __init__(self, configuration: FormatConfiguration): 23 | self.__config = configuration 24 | 25 | def write(self, data: str) -> None: 26 | if self.__config.inplace: 27 | self.__try_writing_to_file(data) 28 | else: 29 | print(data) 30 | 31 | def __try_writing_to_file(self, data: str) -> None: 32 | try: 33 | write_to_file(self.__config.file, data) 34 | except PermissionError: 35 | raise WriterError(f'File {self.__config.file} is read-only!') 36 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from pathlib import Path 8 | from typing import Optional 9 | 10 | from cmake_tidy.formatting.cmake_formatter import CMakeFormatter 11 | from cmake_tidy.formatting.settings_reader import SettingsReader 12 | 13 | 14 | def try_read_settings(filepath: Optional[Path]) -> dict: 15 | return SettingsReader().try_loading_format_settings(filepath) 16 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/cmake_format_dispatcher.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | class _Executor: 8 | def __init__(self, state: dict, name: str, to_be_called): 9 | self.__state = state 10 | self.__name = name 11 | self.__to_be_called = to_be_called 12 | 13 | def __call__(self, arguments=None): 14 | if arguments is None: 15 | value = self.__to_be_called() 16 | else: 17 | value = self.__to_be_called(arguments) 18 | self.__state['last'] = self.__name 19 | return value 20 | 21 | 22 | class CMakeFormatDispatcher: 23 | def __init__(self, state: dict): 24 | self.__state = state 25 | self.__dict = {} 26 | 27 | def __setitem__(self, key, value): 28 | if not callable(value): 29 | raise TypeError('Only callable values accepted') 30 | self.__dict[key] = _Executor(self.__state, key, value) 31 | 32 | def __getitem__(self, item): 33 | return self.__dict[item] 34 | 35 | def __contains__(self, item): 36 | return item in self.__dict 37 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/cmake_formatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting import utils 8 | from cmake_tidy.formatting.cmake_format_dispatcher import CMakeFormatDispatcher 9 | from cmake_tidy.lexical_data.elements import Element 10 | from cmake_tidy.utils.proxy_visitor import ProxyVisitor 11 | 12 | 13 | class CMakeFormatter: 14 | __settings: dict 15 | 16 | def __init__(self, format_settings: dict): 17 | self.__state = {'indent': 0, 'last': None, 'keyword_argument': False, 'has_first_class_keyword': False} 18 | self.__settings = format_settings 19 | 20 | self.__formatters = CMakeFormatDispatcher(self.__state) 21 | self.__formatters['newlines'] = utils.FormatNewline(self.__state, self.__settings) 22 | self.__formatters['unquoted_argument'] = utils.FormatUnquotedArgument(self.__state, self.__settings) 23 | self.__formatters['start_cmd_invoke'] = utils.FormatStartCommandInvocation(self.__state, self.__settings) 24 | self.__formatters['command_invocation'] = utils.FormatCommandInvocation(self.__state, self.__settings) 25 | self.__formatters['file'] = utils.FormatFile(self.__settings) 26 | self.__formatters['spaces'] = utils.FormatSpaces(self.__settings, self.__state) 27 | self.__formatters['arguments'] = utils.FormatArguments(self.__state) 28 | self.__formatters['end_cmd_invoke'] = utils.FormatEndCommandInvocation(self.__state) 29 | self.__formatters['bracket_argument'] = utils.FormatBracketArgument() 30 | self.__formatters['line_comment'] = lambda data: data 31 | self.__formatters['bracket_start'] = lambda data: data 32 | self.__formatters['bracket_end'] = lambda data: data 33 | self.__formatters['parenthesis_start'] = lambda data: data 34 | self.__formatters['parenthesis_end'] = lambda data: data 35 | self.__formatters['bracket_argument_content'] = lambda data: data 36 | self.__formatters['line_ending'] = lambda data: ''.join(data) 37 | self.__formatters['quoted_argument'] = lambda data: f'"{data}"' 38 | self.__formatters['parentheses'] = lambda data: data 39 | 40 | def format(self, data: Element) -> str: 41 | visitor = ProxyVisitor(self.__formatters) 42 | formatted_elements = data.accept(visitor) 43 | return ''.join(formatted_elements) 44 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "JSON schema for the cmake-tidy settings", 3 | "$schema": "http://json-schema.org/draft-06/schema#", 4 | "type": "object", 5 | "properties": { 6 | "succeeding_newlines": { 7 | "type": "integer", 8 | "description": "Describes the maximum number of succeeding newlines that can appear in formatted file" 9 | }, 10 | "tabs_as_spaces": { 11 | "type": "boolean", 12 | "description": "Indentation in the code should be done by spaces (True) or tabs (False)" 13 | }, 14 | "tab_size": { 15 | "type": "integer", 16 | "description": "Size of one tab in spaces" 17 | }, 18 | "force_command_lowercase": { 19 | "type": "boolean", 20 | "description": "Convert all command invocations to lowercase eg. \"SET()\" to \"set()\"" 21 | }, 22 | "space_between_command_and_begin_parentheses": { 23 | "type": "boolean", 24 | "description": "Insert space between command name and begin parentheses eg. \"if()\" to \"if ()\"" 25 | }, 26 | "line_length": { 27 | "type": "integer", 28 | "description": "Maximum line length that will not be splitted" 29 | }, 30 | "wrap_short_invocations_to_single_line": { 31 | "type": "boolean", 32 | "description": "Wrap multiline command invocations that fit into single line" 33 | }, 34 | "closing_parentheses_in_newline_when_split": { 35 | "type": "boolean", 36 | "description": "Move closing parentheses of command invocation to new line when split" 37 | }, 38 | "unquoted_uppercase_as_keyword": { 39 | "type": "boolean", 40 | "description": "Treat unquoted uppercase arguments as keywords" 41 | }, 42 | "space_after_loop_condition": { 43 | "type": "boolean", 44 | "description": "Introduce spaces after all conditional invocations only" 45 | }, 46 | "keep_property_and_value_in_one_line": { 47 | "type": "boolean", 48 | "description": "Keep properties and its value within single line - doesn't split over multiple line" 49 | }, 50 | "keyword_and_single_value_in_one_line": { 51 | "type": "boolean", 52 | "description": "Keep in single line property/keyword with single value" 53 | }, 54 | "keep_command_in_single_line": { 55 | "type": "boolean", 56 | "description": "add_custom_target COMMAND will be forced to stay in one line" 57 | }, 58 | "condition_splitting_move_and_or_to_newline": { 59 | "type": "boolean", 60 | "description": "When splitting conditional invocation - move AND/OR to newline" 61 | }, 62 | "keywords": { 63 | "type": "array", 64 | "description": "Array of unquoted arguments to be treated as keywords" 65 | } 66 | }, 67 | "propertyNames": { 68 | "type": "string", 69 | "enum": [ 70 | "succeeding_newlines", 71 | "tabs_as_spaces", 72 | "tab_size", 73 | "force_command_lowercase", 74 | "space_between_command_and_begin_parentheses", 75 | "line_length", 76 | "wrap_short_invocations_to_single_line", 77 | "closing_parentheses_in_newline_when_split", 78 | "unquoted_uppercase_as_keyword", 79 | "space_after_loop_condition", 80 | "keep_property_and_value_in_one_line", 81 | "keyword_and_single_value_in_one_line", 82 | "keep_command_in_single_line", 83 | "condition_splitting_move_and_or_to_newline", 84 | "keywords" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/settings_reader.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import json 8 | from json import JSONDecodeError 9 | from pathlib import Path 10 | from typing import Optional 11 | 12 | from jsonschema import Draft6Validator, SchemaError, ValidationError 13 | 14 | 15 | class InvalidSchemaError(Exception): 16 | pass 17 | 18 | 19 | class SchemaValidationError(ValidationError): 20 | pass 21 | 22 | 23 | class SettingsReader: 24 | def __init__(self): 25 | self.__schema = self.__try_reading_schema() 26 | self.__settings = self.get_default_format_settings() 27 | 28 | def try_loading_format_settings(self, filepath: Optional[Path]) -> dict: 29 | self.__settings.update(self.__try_reading_settings(filepath)) 30 | return self.__settings 31 | 32 | def __try_reading_settings(self, filepath: Optional[Path]) -> dict: 33 | try: 34 | user_define_settings = self._read_settings(filepath) 35 | self.__schema.validate(user_define_settings) 36 | return user_define_settings 37 | except (ValidationError, JSONDecodeError) as error: 38 | raise SchemaValidationError(str(error)) 39 | 40 | def __try_reading_schema(self) -> Draft6Validator: 41 | try: 42 | schema = self._read_schema_file() 43 | Draft6Validator.check_schema(schema) 44 | return Draft6Validator(schema) 45 | except (FileNotFoundError, OSError, SchemaError, JSONDecodeError): 46 | raise InvalidSchemaError('JSON schema validation error - please raise issue on github!') 47 | 48 | @staticmethod 49 | def _read_schema_file() -> dict: 50 | schema_file = Path(__file__).parent / 'settings.json' 51 | with schema_file.open() as file: 52 | return json.load(file) 53 | 54 | def _read_settings(self, filepath: Optional[Path]) -> dict: 55 | if filepath: 56 | return self.__try_reading_settings_recursively(filepath) 57 | else: 58 | return self.__read_settings_from_filepath(Path.cwd()) 59 | 60 | def __try_reading_settings_recursively(self, filepath: Path) -> dict: 61 | content = dict() 62 | for path in filepath.parents: 63 | content = self.__read_settings_from_filepath(path) 64 | if content: 65 | break 66 | return content 67 | 68 | def __read_settings_from_filepath(self, filepath: Path) -> dict: 69 | self.__settings_file = filepath / '.cmake-tidy.json' 70 | if self.__settings_file.exists() and self.__settings_file.stat().st_size > 0: 71 | with self.__settings_file.open() as file: 72 | return json.load(file) 73 | return dict() 74 | 75 | @staticmethod 76 | def get_default_format_settings() -> dict: 77 | settings = dict() 78 | settings['succeeding_newlines'] = 2 79 | settings['tabs_as_spaces'] = False 80 | settings['tab_size'] = 4 81 | settings['force_command_lowercase'] = True 82 | settings['space_between_command_and_begin_parentheses'] = False 83 | settings['line_length'] = 100 84 | settings['wrap_short_invocations_to_single_line'] = True 85 | settings['closing_parentheses_in_newline_when_split'] = True 86 | settings['unquoted_uppercase_as_keyword'] = False 87 | settings['space_after_loop_condition'] = False 88 | settings['keep_property_and_value_in_one_line'] = True 89 | settings['keyword_and_single_value_in_one_line'] = True 90 | settings['keep_command_in_single_line'] = True 91 | settings['condition_splitting_move_and_or_to_newline'] = True 92 | settings['keywords'] = [] 93 | return settings 94 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.format_arguments import FormatArguments 8 | from cmake_tidy.formatting.utils.format_command_invocation import FormatCommandInvocation 9 | from cmake_tidy.formatting.utils.format_end_command_invocation import FormatEndCommandInvocation 10 | from cmake_tidy.formatting.utils.format_file import FormatFile 11 | from cmake_tidy.formatting.utils.format_newline import FormatNewline 12 | from cmake_tidy.formatting.utils.format_spaces import FormatSpaces 13 | from cmake_tidy.formatting.utils.format_start_command_invocation import FormatStartCommandInvocation 14 | from cmake_tidy.formatting.utils.format_unquoted_argument import FormatUnquotedArgument 15 | from cmake_tidy.formatting.utils.format_bracket_argument import FormatBracketArgument 16 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_arguments.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | from iteration_utilities import deepflatten 9 | 10 | 11 | class FormatArguments: 12 | __spacing = r'^[ \t]+$' 13 | 14 | def __init__(self, state: dict): 15 | self.__state = state 16 | 17 | def __call__(self, data: list) -> list: 18 | return self.__format_arguments(data) if data[0] else [] 19 | 20 | def __format_arguments(self, data: list) -> list: 21 | data = list(deepflatten(data, types=list)) 22 | data = self.__replace_spacings_between_arguments_with_single_space(data) 23 | data = self.__remove_spacing_from_first_element(data) 24 | data = self.__remove_spacing_from_last_element(data) 25 | return data 26 | 27 | @staticmethod 28 | def __replace_spacings_between_arguments_with_single_space(data: list) -> list: 29 | return [re.sub(FormatArguments.__spacing, ' ', element) for element in data] 30 | 31 | @staticmethod 32 | def __remove_spacing_from_first_element(data: list) -> list: 33 | return data[1:] if re.match(FormatArguments.__spacing, data[0]) else data 34 | 35 | @staticmethod 36 | def __remove_spacing_from_last_element(data: list) -> list: 37 | return data[:-1] if re.match(FormatArguments.__spacing, data[-1]) else data 38 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_bracket_argument.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.tokens import Tokens 8 | 9 | 10 | class FormatBracketArgument: 11 | def __call__(self, data: list) -> str: 12 | formatted = ''.join(data) 13 | if '\n' in data[1]: 14 | formatted = Tokens.reindent(99) + formatted 15 | return formatted 16 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_command_invocation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.invocation import format_command_invocation 8 | from cmake_tidy.formatting.utils.updaters.command_invocatin_state_updater import CommandInvocationStateUpdater 9 | 10 | 11 | class FormatCommandInvocation: 12 | def __init__(self, state: dict, settings: dict): 13 | self.__state = state 14 | self.__state_handler = CommandInvocationStateUpdater(state) 15 | self.__settings = settings 16 | 17 | def __call__(self, data: list) -> str: 18 | command_invocation = self.__prepare_data(data) 19 | formatted = format_command_invocation(self.__state, self.__settings, command_invocation) 20 | self.__state_handler.update_state(command_invocation['function_name']) 21 | return formatted 22 | 23 | @staticmethod 24 | def __prepare_data(data: list) -> dict: 25 | return {'function_name': data[0], 26 | 'arguments': data[1] if len(data) == 3 else [], 27 | 'closing': data[2] if len(data) == 3 else data[1]} 28 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_end_command_invocation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.tokens import Tokens 8 | 9 | 10 | class FormatEndCommandInvocation: 11 | def __init__(self, state: dict): 12 | self.__state = state 13 | 14 | def __call__(self, data: str) -> str: 15 | self.__update_state() 16 | return self.__format(data) 17 | 18 | def __update_state(self): 19 | if self.__state['keyword_argument']: 20 | self.__state['indent'] -= 1 21 | 22 | def __format(self, data): 23 | if self.__state['has_first_class_keyword']: 24 | return Tokens.reindent(3) + data 25 | if self.__state['keyword_argument']: 26 | return Tokens.reindent(2) + data 27 | return Tokens.reindent(1) + data 28 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_file.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | 9 | from cmake_tidy.formatting.utils.single_indent import get_single_indent 10 | from cmake_tidy.formatting.utils.tokens import Tokens 11 | 12 | 13 | class FormatFile: 14 | def __init__(self, settings: dict): 15 | self.__settings = settings 16 | 17 | def __call__(self, data: list) -> str: 18 | processed = self.__cleanup_end_invocations(''.join(data)) 19 | processed = self.__cleanup_whitespaces_at_line_ends(processed) 20 | return self.__cleanup_reindent_all(processed) 21 | 22 | def __cleanup_end_invocations(self, formatted_file: str) -> str: 23 | for pattern in Tokens.get_reindent_patterns_list(3, get_single_indent(self.__settings)): 24 | formatted_file = re.sub(pattern, '', formatted_file) 25 | return formatted_file 26 | 27 | @staticmethod 28 | def __cleanup_whitespaces_at_line_ends(processed: str) -> str: 29 | return re.sub('[ \t]*' + Tokens.remove_spaces(), '', processed) 30 | 31 | @staticmethod 32 | def __cleanup_reindent_all(data: str) -> str: 33 | return re.sub('[ \t]*' + Tokens.reindent(), '', data) 34 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_newline.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.single_indent import get_single_indent 8 | from cmake_tidy.formatting.utils.tokens import Tokens 9 | 10 | 11 | class FormatNewline: 12 | def __init__(self, state: dict, settings: dict): 13 | self.__state = state 14 | self.__settings = settings 15 | 16 | def __call__(self, data) -> str: 17 | return self.__format_newlines(data) + self.__prepare_initial_newline_indent() 18 | 19 | def __prepare_initial_newline_indent(self) -> str: 20 | return self.__state['indent'] * get_single_indent(self.__settings) 21 | 22 | def __format_newlines(self, number_of_newlines: int) -> str: 23 | return Tokens.remove_spaces() + '\n' * min(self.__settings['succeeding_newlines'], number_of_newlines) 24 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_spaces.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.single_indent import get_single_indent 8 | 9 | 10 | class FormatSpaces: 11 | def __init__(self, settings: dict, state: dict): 12 | self.__settings = settings 13 | self.__state = state 14 | 15 | def __call__(self, data: str) -> str: 16 | if self.__state['last'] == 'line_ending': 17 | return '' 18 | elif self.__state['last'] == 'command_invocation': 19 | return ' ' 20 | return data.replace('\t', get_single_indent(self.__settings)) 21 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_start_command_invocation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | 9 | from cmake_tidy.lexical_data import KeywordVerifier 10 | 11 | 12 | class FormatStartCommandInvocation: 13 | def __init__(self, state: dict, settings: dict): 14 | self.__state = state 15 | self.__settings = settings 16 | 17 | def __call__(self, data: str) -> str: 18 | self.__update_state(data) 19 | return self.__format_data(data) 20 | 21 | def __update_state(self, data: str): 22 | if KeywordVerifier.is_conditional_invocation(data): 23 | self.__state['indent'] += 1 24 | self.__state['indent'] += 1 25 | 26 | def __format_data(self, original: str) -> str: 27 | formatted = self.__remove_whitespaces_after_name(original) 28 | formatted = self.__unify_command_name(formatted) 29 | return self.__add_spacing_if_needed(formatted) 30 | 31 | def __unify_command_name(self, formatted: str) -> str: 32 | if self.__settings.get('force_command_lowercase'): 33 | return formatted.lower() 34 | return formatted 35 | 36 | def __add_spacing_if_needed(self, formatted: str) -> str: 37 | if self.__is_spacing_needed(formatted): 38 | return formatted.replace('(', ' (') 39 | return formatted 40 | 41 | def __is_spacing_needed(self, formatted: str) -> bool: 42 | return self.__settings.get('space_between_command_and_begin_parentheses') or \ 43 | self.__should_add_space_for_conditional(formatted) 44 | 45 | def __should_add_space_for_conditional(self, formatted: str) -> bool: 46 | return self.__settings.get('space_after_loop_condition') and \ 47 | KeywordVerifier.is_conditional_invocation(formatted) 48 | 49 | @staticmethod 50 | def __remove_whitespaces_after_name(original: str) -> str: 51 | formatted = re.sub(r'\s+', '', original) 52 | return formatted 53 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/format_unquoted_argument.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.updaters.keyword_state_updater import KeywordStateUpdater 8 | from cmake_tidy.formatting.utils.tokens import Tokens 9 | from cmake_tidy.lexical_data import KeywordVerifier 10 | 11 | 12 | class FormatUnquotedArgument: 13 | def __init__(self, state: dict, settings: dict): 14 | self.__verifier = KeywordVerifier(settings) 15 | self.__state_updater = KeywordStateUpdater(state, settings) 16 | self.__state = state 17 | self.__keyword_argument_already_found = False 18 | 19 | def __call__(self, data: str) -> str: 20 | self.__keyword_argument_already_found = self.__state['keyword_argument'] 21 | self.__state_updater.update_state(data) 22 | return self.__format_data(data) 23 | 24 | def __format_data(self, data: str) -> str: 25 | if self.__keyword_argument_already_found and self.__is_reindent_needed(data): 26 | return Tokens.reindent(1) + data 27 | return data 28 | 29 | def __is_reindent_needed(self, data): 30 | return self.__verifier.is_keyword(data) or self.__verifier.is_property(data) 31 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.invocation.command_formatter import CommandFormatter 8 | from cmake_tidy.formatting.utils.invocation.condition_formatter import ConditionFormatter 9 | from cmake_tidy.lexical_data import KeywordVerifier 10 | 11 | 12 | def format_command_invocation(state: dict, settings: dict, invocation: dict) -> str: 13 | if KeywordVerifier.is_conditional_invocation(invocation['function_name']): 14 | return ConditionFormatter(state, settings).format(invocation) 15 | return CommandFormatter(state, settings).format(invocation) 16 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/command_formatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.invocation.invocation_formatter import InvocationFormatter 8 | from cmake_tidy.formatting.utils.invocation.command_splitter import CommandSplitter 9 | 10 | 11 | class CommandFormatter(InvocationFormatter): 12 | def __init__(self, state: dict, settings: dict): 13 | super().__init__(state, settings) 14 | 15 | def format(self, invocation: dict) -> str: 16 | invocation['arguments'] = self._prepare_arguments(invocation) 17 | if not self._is_fitting_in_line(invocation): 18 | invocation['arguments'] = self.__split_command_to_newlines(invocation) 19 | return self._join_command_invocation(invocation) 20 | 21 | def __split_command_to_newlines(self, invocation: dict) -> list: 22 | return CommandSplitter(self._state, self._settings).split(invocation) 23 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/command_splitter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.format_newline import FormatNewline 8 | from cmake_tidy.formatting.utils.invocation.command_realign_modifier import CommandRealignModifier 9 | from cmake_tidy.formatting.utils.tokens import Tokens 10 | from cmake_tidy.formatting.utils.updaters.keyword_state_updater import KeywordStateUpdater 11 | from cmake_tidy.lexical_data import KeywordVerifier 12 | 13 | 14 | class CommandSplitter: 15 | def __init__(self, state: dict, settings: dict): 16 | self.__prepare_state(state) 17 | self.__state_updater = KeywordStateUpdater(self.__state, settings) 18 | self.__verifier = KeywordVerifier(settings) 19 | self.__settings = settings 20 | 21 | def split(self, invocation: dict) -> list: 22 | invocation['arguments'] = self.__split_args_to_newlines(invocation['arguments']) 23 | invocation['arguments'] = self.__realign(invocation) 24 | return invocation['arguments'] + self.__add_closing_parenthesis_separator(invocation) 25 | 26 | def __realign(self, invocation: dict) -> list: 27 | return CommandRealignModifier(self.__state, self.__settings).realign(invocation) 28 | 29 | def __split_args_to_newlines(self, args: list) -> list: 30 | if self.__verifier.is_keyword_or_property(args[0]): 31 | args = [FormatNewline(self.__state, self.__settings)(1)] + args 32 | return [self.__handle_argument(arg) for arg in args] 33 | 34 | def __handle_argument(self, arg: str) -> str: 35 | self.__state_updater.update_state(arg) 36 | return self.__get_converted_whitespace() if arg == ' ' else arg 37 | 38 | def __add_closing_parenthesis_separator(self, invocation: dict) -> list: 39 | if self.__settings['closing_parentheses_in_newline_when_split'] and \ 40 | not self.__is_last_element_newline(invocation): 41 | return [FormatNewline(self.__state, self.__settings)(1)] 42 | return [] 43 | 44 | @staticmethod 45 | def __is_last_element_newline(invocation: dict) -> bool: 46 | return Tokens.is_spacing_token(invocation['arguments'][-1]) or \ 47 | Tokens.is_line_comment(invocation['arguments'][-1]) 48 | 49 | def __get_converted_whitespace(self) -> str: 50 | return FormatNewline(self.__state, self.__settings)(1) 51 | 52 | def __prepare_state(self, state: dict) -> None: 53 | self.__state = state.copy() 54 | if self.__state['has_first_class_keyword']: 55 | self.__state['indent'] -= 1 56 | self.__state['has_first_class_keyword'] = False 57 | self.__state['keyword_argument'] = False 58 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/condition_formatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from typing import List 8 | 9 | from cmake_tidy.formatting.utils.format_newline import FormatNewline 10 | from cmake_tidy.formatting.utils.invocation.invocation_formatter import InvocationFormatter 11 | from cmake_tidy.formatting.utils.invocation.invocation_wrapper import InvocationWrapper 12 | from cmake_tidy.formatting.utils.tokens import Tokens 13 | 14 | 15 | class ConditionFormatter(InvocationFormatter): 16 | def __init__(self, state: dict, settings: dict): 17 | super().__init__(state, settings) 18 | 19 | def format(self, invocation: dict) -> str: 20 | invocation['arguments'] = self._prepare_arguments(invocation) 21 | invocation['arguments'] = self.__split_invocation_if_needed(invocation) 22 | return self._join_command_invocation(invocation) 23 | 24 | def __split_invocation_if_needed(self, invocation: dict) -> list: 25 | if not self._is_fitting_in_line(invocation): 26 | invocation = InvocationWrapper().wrap(invocation) 27 | return self.__split_invocation(invocation['arguments']) 28 | return invocation['arguments'] 29 | 30 | def __split_invocation(self, args: List[str]) -> list: 31 | for i in range(len(args)): 32 | self.__update_state(args[i]) 33 | self.__replace_token_with_newline_if_needed(args, i) 34 | return args 35 | 36 | def __replace_token_with_newline_if_needed(self, args: List[str], index: int): 37 | argument_diff = -1 if self._settings.get('condition_splitting_move_and_or_to_newline') else 1 38 | if (args[index] == 'OR' or args[index] == 'AND') and self.__is_spacing_token(args, index + argument_diff): 39 | args[index + argument_diff] = FormatNewline(self._state, self._settings)(1) 40 | 41 | def __update_state(self, token: str): 42 | if token == '(': 43 | self._state['indent'] += 1 44 | elif token == ')': 45 | self._state['indent'] -= 1 46 | 47 | @staticmethod 48 | def __is_spacing_token(args: List[str], index: int) -> bool: 49 | try: 50 | return Tokens.is_spacing_token(args[index]) 51 | except IndexError: 52 | return False 53 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/invocation_formatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | from abc import ABC, abstractmethod 9 | from typing import List 10 | 11 | from cmake_tidy.formatting.utils.invocation.invocation_wrapper import InvocationWrapper 12 | from cmake_tidy.formatting.utils.invocation.line_comments_formatter import LineCommentsFormatter 13 | from cmake_tidy.formatting.utils.line_length_calculator import LineLengthCalculator 14 | from cmake_tidy.formatting.utils.single_indent import get_single_indent 15 | from cmake_tidy.formatting.utils.tokens import Tokens 16 | 17 | 18 | class InvocationFormatter(ABC): 19 | def __init__(self, state: dict, settings: dict): 20 | self._settings = settings 21 | self._state = state 22 | 23 | @abstractmethod 24 | def format(self, invocation: dict) -> str: 25 | pass 26 | 27 | def _is_fitting_in_line(self, command_invocation: dict) -> bool: 28 | return self._invocation_length(command_invocation) < self._settings['line_length'] 29 | 30 | def _newline_indent(self) -> str: 31 | indent = max(self._state['indent'] - 1, 0) 32 | return indent * get_single_indent(self._settings) 33 | 34 | def _invocation_length(self, command_invocation: dict) -> int: 35 | invoke = self._join_command_invocation(command_invocation) + self._newline_indent() 36 | return LineLengthCalculator(self._settings).calculate(invoke) 37 | 38 | def _join_command_invocation(self, invocation: dict) -> str: 39 | formatted = invocation['function_name'] + ''.join(invocation['arguments']) + invocation['closing'] 40 | return self.__add_reindent_tokens_where_needed(formatted) 41 | 42 | def _prepare_arguments(self, invocation: dict) -> list: 43 | invocation['arguments'] = self.__remove_empty_arguments(invocation) 44 | invocation['arguments'] = self.__remove_whitespace_at_end_of_line(invocation['arguments']) 45 | invocation['arguments'] = LineCommentsFormatter(self._state, self._settings).format(invocation['arguments']) 46 | if self.__is_wrappable(invocation): 47 | invocation['arguments'] = self.__wrap_arguments_if_possible(invocation) 48 | return invocation['arguments'] 49 | 50 | @staticmethod 51 | def __add_reindent_tokens_where_needed(data: str) -> str: 52 | data_lower = data.lower() 53 | if any(data_lower.startswith(token) for token in Tokens.reindent_commands_tokens()): 54 | return Tokens.reindent(1) + data 55 | return data 56 | 57 | def __is_wrappable(self, invocation: dict) -> bool: 58 | return len(invocation['arguments']) > 0 and self._settings['wrap_short_invocations_to_single_line'] is True 59 | 60 | def __wrap_arguments_if_possible(self, invocation: dict) -> list: 61 | command_invocation = InvocationWrapper().wrap(invocation) 62 | if self._is_fitting_in_line(command_invocation): 63 | return command_invocation['arguments'] 64 | else: 65 | return invocation['arguments'] 66 | 67 | @staticmethod 68 | def __remove_empty_arguments(invocation: dict) -> list: 69 | return list(filter(len, invocation['arguments'])) 70 | 71 | @staticmethod 72 | def __remove_whitespace_at_end_of_line(args: List[str]) -> list: 73 | filtered_arguments = [] 74 | for i in range(len(args) - 1): 75 | if not (Tokens.is_spacing_token(args[i]) and Tokens.is_spacing_token(args[i + 1])): 76 | filtered_arguments.append(args[i]) 77 | return filtered_arguments + [args[-1]] if args else [] 78 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/invocation_wrapper.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.formatting.utils.tokens import Tokens 8 | 9 | 10 | class InvocationWrapper: 11 | def wrap(self, invocation: dict) -> dict: 12 | wrapped_invoke = self.__prepare_wrapped_invocation(invocation) 13 | wrapped_invoke['arguments'] = [e if not Tokens.is_spacing_token(e) else ' ' for e in 14 | wrapped_invoke['arguments']] 15 | return wrapped_invoke 16 | 17 | @staticmethod 18 | def __prepare_wrapped_invocation(invocation: dict) -> dict: 19 | new_invoke = invocation.copy() 20 | if Tokens.is_spacing_token(new_invoke['arguments'][0]): 21 | new_invoke['arguments'] = new_invoke['arguments'][1:] 22 | if Tokens.is_spacing_token(new_invoke['arguments'][-1]): 23 | new_invoke['arguments'] = new_invoke['arguments'][:-1] 24 | return new_invoke 25 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/invocation/line_comments_formatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | from typing import List 9 | 10 | from cmake_tidy.formatting.utils.tokens import Tokens 11 | from cmake_tidy.lexical_data import KeywordVerifier 12 | 13 | 14 | class LineCommentsFormatter: 15 | def __init__(self, state: dict, settings: dict): 16 | self.__state = state 17 | self.__settings = settings 18 | 19 | def format(self, args: List[str]) -> list: 20 | args = self.__reindent_line_comments(args) 21 | return self.__merge_line_comments_with_whitespaces_before(args) 22 | 23 | def __reindent_line_comments(self, args: List[str]) -> list: 24 | args = self.__reindent_line_comments_after_keyword(args) 25 | return self.__reindent_line_comments_at_end_of_invocation(args) 26 | 27 | def __reindent_line_comments_at_end_of_invocation(self, args: List[str]) -> list: 28 | if self.__state['keyword_argument']: 29 | self.__try_reindent_all_previous_comments(args, len(args)) 30 | return args 31 | 32 | def __reindent_line_comments_after_keyword(self, args: List[str]) -> list: 33 | verifier = KeywordVerifier(self.__settings) 34 | for i in reversed(range(len(args))): 35 | if verifier.is_keyword(args[i]) and re.match(Tokens.get_reindent_regex(), args[i]): 36 | self.__try_reindent_all_previous_comments(args, i) 37 | return args 38 | 39 | def __try_reindent_all_previous_comments(self, args, i): 40 | try: 41 | self.__reindent_all_previous_comments(args, i) 42 | except IndexError: 43 | pass 44 | 45 | @staticmethod 46 | def __reindent_all_previous_comments(args: list, start: int) -> None: 47 | for i in reversed(range(start)): 48 | if Tokens.is_line_comment(args[i]): 49 | args[i] = Tokens.reindent(1) + args[i] 50 | else: 51 | break 52 | 53 | @staticmethod 54 | def __merge_line_comments_with_whitespaces_before(args: List[str]) -> list: 55 | merged = [] 56 | for i in range(len(args) - 1): 57 | if Tokens.is_spacing_token(args[i]) and Tokens.is_line_comment(args[i + 1]): 58 | args[i + 1] = args[i] + args[i + 1] 59 | else: 60 | merged.append(args[i]) 61 | return merged + [args[-1]] if args else [] 62 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/line_length_calculator.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | 9 | from cmake_tidy.formatting.utils.tokens import Tokens 10 | 11 | 12 | class LineLengthCalculator: 13 | def __init__(self, settings: dict): 14 | self.__settings = settings 15 | 16 | def calculate(self, invocation: str) -> int: 17 | invocation = invocation.replace('\t', ' ' * self.__settings['tab_size']) 18 | invocation = re.sub(Tokens.get_reindent_regex(), '', invocation) 19 | invocation = re.sub(Tokens.remove_spaces(), '', invocation) 20 | return len(invocation) 21 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/single_indent.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | def get_single_indent(settings: dict) -> str: 8 | if not settings['tabs_as_spaces']: 9 | return '\t' 10 | return settings['tab_size'] * ' ' 11 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/tokens.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | 9 | 10 | class Tokens: 11 | @staticmethod 12 | def start_tokens() -> list: 13 | return ['macro', 'while', 'foreach', 'if', 'function'] 14 | 15 | @staticmethod 16 | def end_tokens() -> list: 17 | return [f'end{token}' for token in Tokens.start_tokens()] 18 | 19 | @staticmethod 20 | def reindent_commands_tokens() -> list: 21 | return ['elseif', 'else'] + Tokens.end_tokens() 22 | 23 | @staticmethod 24 | def conditional_tokens() -> list: 25 | return ['if', 'while', 'elseif'] 26 | 27 | @staticmethod 28 | def reindent(count=99) -> str: 29 | return f'' 30 | 31 | @staticmethod 32 | def remove_spaces() -> str: 33 | return '' 34 | 35 | @staticmethod 36 | def is_spacing_token(data: str) -> bool: 37 | data = data.replace(Tokens.remove_spaces(), '') 38 | return re.match(r'^\s+$', data) is not None 39 | 40 | @staticmethod 41 | def get_reindent_regex() -> str: 42 | return r'' 43 | 44 | @staticmethod 45 | def get_reindent_patterns_list(count: int, indent: str) -> list: 46 | return [f'({indent}){{0,{times}}}{Tokens.reindent(times)}' for times in range(1, count + 1)] 47 | 48 | @staticmethod 49 | def is_line_comment(data: str) -> bool: 50 | data = re.sub(Tokens.get_reindent_regex(), '', data) 51 | data = re.sub(Tokens.remove_spaces(), '', data) 52 | return data.strip().startswith('#') 53 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/updaters/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Maciej Patro (maciej.patro@gmail.com) 2 | # MIT License 3 | 4 | 5 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/updaters/command_invocatin_state_updater.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | 9 | from cmake_tidy.formatting.utils.tokens import Tokens 10 | from cmake_tidy.lexical_data import KeywordVerifier 11 | 12 | 13 | class CommandInvocationStateUpdater: 14 | def __init__(self, state: dict): 15 | self.__state = state 16 | 17 | def update_state(self, function_name: str) -> None: 18 | self.__update_indent_state(function_name) 19 | self.__state['keyword_argument'] = False 20 | self.__state['has_first_class_keyword'] = False 21 | 22 | def __update_indent_state(self, function_name: str) -> None: 23 | if not self.__is_start_of_special_command(function_name): 24 | self.__state['indent'] -= 1 25 | if self.__state['has_first_class_keyword']: 26 | self.__state['indent'] -= 1 27 | if self.__is_end_of_special_command(function_name): 28 | self.__state['indent'] -= 1 29 | if KeywordVerifier.is_conditional_invocation(function_name): 30 | self.__state['indent'] -= 1 31 | 32 | def __is_start_of_special_command(self, original: str) -> bool: 33 | return any([self.__matches(token, original.lower()) for token in Tokens.start_tokens()]) 34 | 35 | def __is_end_of_special_command(self, original: str) -> bool: 36 | return any([self.__matches(token, original.lower()) for token in Tokens.end_tokens()]) 37 | 38 | @staticmethod 39 | def __matches(token: str, data: str) -> bool: 40 | return re.match(r'^' + re.escape(token) + r'\s?\(', data) is not None 41 | -------------------------------------------------------------------------------- /cmake_tidy/formatting/utils/updaters/keyword_state_updater.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.lexical_data import KeywordVerifier 8 | 9 | 10 | class KeywordStateUpdater: 11 | def __init__(self, state: dict, settings: dict): 12 | self.__state = state 13 | self.__verifier = KeywordVerifier(settings) 14 | 15 | def update_state(self, argument: str) -> None: 16 | if self.__verifier.is_first_class_keyword(argument): 17 | self.__state['has_first_class_keyword'] = True 18 | self.__state['indent'] += 1 19 | elif self.__verifier.is_keyword(argument) or self.__should_indent_property(argument): 20 | if not self.__state['keyword_argument']: 21 | self.__state['indent'] += 1 22 | self.__state['keyword_argument'] = True 23 | 24 | def __should_indent_property(self, argument) -> bool: 25 | return self.__state['has_first_class_keyword'] and self.__verifier.is_property(argument) 26 | -------------------------------------------------------------------------------- /cmake_tidy/lexical_data/__init__.py: -------------------------------------------------------------------------------- 1 | from cmake_tidy.lexical_data.keyword_verifier import KeywordVerifier 2 | -------------------------------------------------------------------------------- /cmake_tidy/lexical_data/elements.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from abc import ABC, abstractmethod 8 | 9 | 10 | class Element(ABC): 11 | def __init__(self): 12 | self._parent = None 13 | self._name = '' 14 | 15 | @property 16 | def parent(self) -> 'Element': 17 | return self._parent 18 | 19 | @parent.setter 20 | def parent(self, parent: 'Element'): 21 | self._parent = parent 22 | 23 | @property 24 | def name(self) -> str: 25 | return self._name 26 | 27 | @name.setter 28 | def name(self, name: str): 29 | self._name = name 30 | 31 | @abstractmethod 32 | def add(self, component: 'Element') -> 'Element': 33 | pass 34 | 35 | @abstractmethod 36 | def accept(self, visitor): 37 | pass 38 | 39 | 40 | class PrimitiveElement(Element): 41 | def __init__(self, name='', values=''): 42 | super().__init__() 43 | self._name = name 44 | self._values = values 45 | 46 | def __repr__(self) -> str: 47 | return f'{self.name}: {self.values}' if self.name != '' else '' 48 | 49 | @property 50 | def values(self): 51 | return self._values 52 | 53 | @values.setter 54 | def values(self, values): 55 | self._values = values 56 | 57 | def add(self, component: 'Element') -> 'Element': 58 | pass 59 | 60 | def accept(self, visitor): 61 | return visitor.visit(self.name, self.values) 62 | 63 | 64 | class ComplexElement(Element): 65 | def __init__(self, name='') -> None: 66 | super().__init__() 67 | self._children = [] 68 | self._name = name 69 | 70 | def __repr__(self) -> str: 71 | children_repr = [f'{self.name}.{self.__ident(str(child))}' for child in self._children if str(child) != ''] 72 | return '\n'.join(children_repr) 73 | 74 | def __ident(self, representation: str) -> str: 75 | return representation.replace('\n', '\n{}'.format(' ' * (len(self.name) + 1))) 76 | 77 | def add(self, component: Element) -> 'Element': 78 | if component: 79 | self._children.append(component) 80 | component.parent = self 81 | return self 82 | 83 | def remove(self, component: Element) -> None: 84 | self._children.remove(component) 85 | component.parent = None 86 | 87 | def accept(self, visitor): 88 | values = [child.accept(visitor) for child in self._children] 89 | return visitor.visit(self.name, values) 90 | -------------------------------------------------------------------------------- /cmake_tidy/lexical_data/keyword_verifier.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import json 8 | import re 9 | from pathlib import Path 10 | 11 | from cmake_tidy.formatting.utils.tokens import Tokens 12 | 13 | 14 | class KeywordVerifier: 15 | __FIRST_CLASS_KEYWORDS = ['PROPERTIES', 'PROPERTY'] 16 | __PROPERTIES = dict() 17 | 18 | @staticmethod 19 | def __init_properties(): 20 | if not KeywordVerifier.__PROPERTIES: 21 | with (Path(__file__).parent / 'keyword_list.json').open() as file: 22 | KeywordVerifier.__PROPERTIES = json.load(file) 23 | 24 | def __init__(self, settings: dict): 25 | KeywordVerifier.__init_properties() 26 | self.__settings = settings 27 | 28 | @staticmethod 29 | def is_first_class_keyword(data: str) -> bool: 30 | data = data.replace(Tokens.reindent(1), '') 31 | return data in KeywordVerifier.__FIRST_CLASS_KEYWORDS 32 | 33 | def get_cmake_properties_version(self) -> str: 34 | return self.__PROPERTIES["cmake_version"] 35 | 36 | def is_keyword_or_property(self, data: str) -> bool: 37 | return self.is_property(data) or self.is_keyword(data) 38 | 39 | def is_keyword(self, data: str) -> bool: 40 | data = data.replace(Tokens.reindent(1), '') 41 | return self.__is_one_of_defined_keywords(data) or \ 42 | self.__should_be_handled_as_keyword(data) or \ 43 | self.is_first_class_keyword(data) or \ 44 | self.__is_keyword_in_cmake(data) 45 | 46 | @staticmethod 47 | def is_conditional_invocation(data: str) -> bool: 48 | data = data.lower().replace(' ', '') 49 | return any([token == data[:-1] for token in Tokens.conditional_tokens()]) and data[-1] == '(' 50 | 51 | @staticmethod 52 | def is_double_keyword(first: str, second: str) -> bool: 53 | first = first.replace(Tokens.reindent(1), '') 54 | second = second.replace(Tokens.reindent(1), '') 55 | return any(keyword.startswith(first) and keyword.endswith(second) \ 56 | for keyword in KeywordVerifier.__PROPERTIES['double-keywords']) 57 | 58 | @staticmethod 59 | def is_command_keyword(data: str) -> bool: 60 | data = data.replace(Tokens.reindent(1), '') 61 | return data == 'COMMAND' or data == 'ARGS' 62 | 63 | def __is_one_of_defined_keywords(self, data: str) -> bool: 64 | return self.__settings.get('keywords') and data in self.__settings['keywords'] 65 | 66 | def __should_be_handled_as_keyword(self, data: str) -> bool: 67 | upper_case_regex = r'^([A-Z]+_?)+[A-Z]$' 68 | return self.__settings.get('unquoted_uppercase_as_keyword') and re.match(upper_case_regex, data) 69 | 70 | def is_property(self, data: str) -> bool: 71 | data = data.replace(Tokens.reindent(1), '') 72 | return data in KeywordVerifier.__PROPERTIES["properties_full_names"] or \ 73 | self.__is_property_regex_starting(data) or \ 74 | self.__is_property_ending_with(data) 75 | 76 | @staticmethod 77 | def __is_keyword_in_cmake(data: str) -> bool: 78 | return any([data in KeywordVerifier.__PROPERTIES["keywords"]]) 79 | 80 | @staticmethod 81 | def __is_property_ending_with(data: str) -> bool: 82 | return any([data.endswith(token) for token in KeywordVerifier.__PROPERTIES['properties_ending_with']]) 83 | 84 | @staticmethod 85 | def __is_property_regex_starting(data: str) -> bool: 86 | return any([data.startswith(token) for token in KeywordVerifier.__PROPERTIES['properties_starting_with']]) 87 | -------------------------------------------------------------------------------- /cmake_tidy/parsing/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.parsing.cmake_parser import CMakeParser 8 | -------------------------------------------------------------------------------- /cmake_tidy/parsing/cmake_lexer.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import ply.lex as lex 8 | 9 | 10 | class CMakeLexer: 11 | states = ( 12 | ('commandinvocation', 'exclusive'), 13 | ('bracketargument', 'exclusive'), 14 | ('quotedargument', 'exclusive'), 15 | ('insideparentheses', 'exclusive') 16 | ) 17 | tokens = ['NEWLINES', 18 | 'LINE_COMMENT', 19 | 'SPACES', 20 | 'COMMAND_INVOCATION_START', 21 | 'COMMAND_INVOCATION_END', 22 | 'BRACKET_ARGUMENT_START', 23 | 'BRACKET_ARGUMENT_CONTENT', 24 | 'BRACKET_ARGUMENT_END', 25 | 'QUOTED_ARGUMENT_START', 26 | 'QUOTED_ARGUMENT_CONTENT', 27 | 'QUOTED_ARGUMENT_END', 28 | 'UNQUOTED_ARGUMENT', 29 | 'BEGIN_PARENTHESIS', 30 | 'END_PARENTHESIS'] 31 | 32 | t_INITIAL_commandinvocation_insideparentheses_LINE_COMMENT = r'\#((?=\n)|[^\n]+)' 33 | t_INITIAL_commandinvocation_insideparentheses_SPACES = r'[ \t]+' 34 | t_quotedargument_QUOTED_ARGUMENT_CONTENT = r'(? None: 38 | self.bracket_argument_size = -1 39 | self.lexer = lex.lex(module=self) 40 | 41 | @staticmethod 42 | def t_begin_commandinvocation(t: lex.Token) -> lex.Token: 43 | r"""[A-Za-z_][A-Za-z0-9_]*[ \t]*\(""" 44 | t.type = 'COMMAND_INVOCATION_START' 45 | t.lexer.push_state('commandinvocation') 46 | return t 47 | 48 | def t_commandinvocation_insideparentheses_begin_insideparentheses(self, t: lex.Token) -> lex.Token: 49 | r"""\(""" 50 | t.type = 'BEGIN_PARENTHESIS' 51 | t.lexer.push_state('insideparentheses') 52 | return t 53 | 54 | def t_commandinvocation_insideparentheses_begin_bracketargument(self, t: lex.Token) -> lex.Token: 55 | r"""\[=*\[""" 56 | t.type = 'BRACKET_ARGUMENT_START' 57 | self.bracket_argument_size = len(t.value) 58 | t.lexer.push_state('bracketargument') 59 | return t 60 | 61 | def t_commandinvocation_insideparentheses_begin_quotedargument(self, t: lex.Token) -> lex.Token: 62 | r"""\"""" 63 | t.type = 'QUOTED_ARGUMENT_START' 64 | t.lexer.push_state('quotedargument') 65 | return t 66 | 67 | @staticmethod 68 | def t_commandinvocation_end(t: lex.Token) -> lex.Token: 69 | r"""\)""" 70 | return _end_state(t, 'COMMAND_INVOCATION_END') 71 | 72 | @staticmethod 73 | def t_insideparentheses_end(t: lex.Token) -> lex.Token: 74 | r"""\)""" 75 | return _end_state(t, 'END_PARENTHESIS') 76 | 77 | @staticmethod 78 | def t_quotedargument_end(t: lex.Token) -> lex.Token: 79 | r"""\"""" 80 | return _end_state(t, 'QUOTED_ARGUMENT_END') 81 | 82 | def t_bracketargument_end(self, t: lex.Token) -> lex.Token: 83 | r"""\]=*\]""" 84 | if self.bracket_argument_size == len(t.value): 85 | t.lexer.pop_state() 86 | t.type = 'BRACKET_ARGUMENT_END' 87 | else: 88 | t.type = 'BRACKET_ARGUMENT_CONTENT' 89 | return t 90 | 91 | @staticmethod 92 | def t_INITIAL_commandinvocation_insideparentheses_NEWLINES(t: lex.LexToken) -> lex.LexToken: 93 | r"""\n+""" 94 | t.lexer.lineno += len(t.value) 95 | return t 96 | 97 | @staticmethod 98 | def t_INITIAL_commandinvocation_insideparentheses_error(t: lex.LexToken) -> lex.LexToken: 99 | return _skip_one_and_return_with(t, '?') 100 | 101 | @staticmethod 102 | def t_bracketargument_error(t: lex.LexToken) -> lex.LexToken: 103 | return _skip_one_and_return_with(t, 'BRACKET_ARGUMENT_CONTENT') 104 | 105 | @staticmethod 106 | def t_quotedargument_error(t: lex.LexToken) -> lex.LexToken: 107 | return _skip_one_and_return_with(t, 'QUOTED_ARGUMENT_CONTENT') 108 | 109 | def analyze(self, data: str) -> list: 110 | self.lexer.input(data) 111 | data = [] 112 | 113 | while True: 114 | tok = self.lexer.token() 115 | if not tok: 116 | break 117 | data.append(tok) 118 | 119 | return data 120 | 121 | 122 | def _skip_one_and_return_with(element: lex.LexToken, token_type: str) -> lex.LexToken: 123 | element.lexer.skip(1) 124 | element.type = token_type 125 | element.value = element.value[0] 126 | return element 127 | 128 | 129 | def _end_state(element: lex.LexToken, token_type: str) -> lex.LexToken: 130 | element.lexer.pop_state() 131 | element.type = token_type 132 | return element 133 | -------------------------------------------------------------------------------- /cmake_tidy/parsing/cmake_parser.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import ply.yacc as yacc 8 | 9 | from cmake_tidy.parsing.cmake_lexer import CMakeLexer 10 | from cmake_tidy.lexical_data.elements import PrimitiveElement, ComplexElement 11 | 12 | 13 | class ParsingError(Exception): 14 | pass 15 | 16 | 17 | class CMakeParser: 18 | def __init__(self) -> None: 19 | self.lexer = CMakeLexer() 20 | self.tokens = self.lexer.tokens 21 | self.parser = yacc.yacc(module=self) 22 | self.__lineno = 1 23 | 24 | @staticmethod 25 | def p_file(p): 26 | """file : file file_element 27 | | file_element 28 | | empty""" 29 | if p[1].name == 'file': 30 | p[0] = p[1].add(p[2]) 31 | else: 32 | p[0] = ComplexElement('file').add(p[1]) 33 | 34 | @staticmethod 35 | def p_file_element(p): 36 | """file_element : line_ending 37 | | spaces 38 | | command_invocation""" 39 | p[0] = p[1] 40 | 41 | @staticmethod 42 | def p_command_invocation(p): 43 | """command_invocation : start_cmd_invoke arguments end_cmd_invoke""" 44 | p[0] = ComplexElement('command_invocation') 45 | for element in p[1:]: 46 | p[0].add(element) 47 | 48 | @staticmethod 49 | def p_parentheses(p): 50 | """parentheses : start_parenthesis arguments end_parenthesis""" 51 | p[0] = ComplexElement('parentheses') 52 | for element in p[1:]: 53 | p[0].add(element) 54 | 55 | @staticmethod 56 | def p_arguments(p): 57 | """arguments : arguments argument 58 | | argument""" 59 | if p[1].name == 'arguments': 60 | p[0] = p[1].add(p[2]) 61 | else: 62 | p[0] = ComplexElement('arguments').add(p[1]) 63 | 64 | @staticmethod 65 | def p_argument(p): 66 | """argument : bracket_argument 67 | | quoted_argument 68 | | unquoted_argument 69 | | arguments_separation 70 | | parentheses 71 | | empty""" 72 | p[0] = p[1] 73 | 74 | @staticmethod 75 | def p_arguments_separation(p): 76 | """arguments_separation : spaces 77 | | line_ending""" 78 | p[0] = p[1] 79 | 80 | @staticmethod 81 | def p_quoted_argument(p): 82 | """quoted_argument : QUOTED_ARGUMENT_START quoted_argument_content QUOTED_ARGUMENT_END""" 83 | p[0] = PrimitiveElement('quoted_argument', p[2].values) 84 | 85 | @staticmethod 86 | def p_bracket_argument(p): 87 | """bracket_argument : BRACKET_ARGUMENT_START bracket_argument_content BRACKET_ARGUMENT_END""" 88 | p[0] = ComplexElement('bracket_argument') \ 89 | .add(PrimitiveElement('bracket_start', p[1])) \ 90 | .add(PrimitiveElement('bracket_argument_content', p[2].values)) \ 91 | .add(PrimitiveElement('bracket_end', p[3])) 92 | 93 | @staticmethod 94 | def p_line_ending(p): 95 | """line_ending : line_comment newlines 96 | | newlines""" 97 | p[0] = ComplexElement('line_ending') 98 | for element in p[1:]: 99 | p[0].add(element) 100 | 101 | @staticmethod 102 | def p_unquoted_argument(p): 103 | """unquoted_argument : UNQUOTED_ARGUMENT""" 104 | p[0] = PrimitiveElement('unquoted_argument', p[1]) 105 | 106 | @staticmethod 107 | def p_line_comment(p): 108 | """line_comment : LINE_COMMENT""" 109 | p[0] = PrimitiveElement('line_comment', p[1]) 110 | 111 | def p_newlines(self, p): 112 | """newlines : NEWLINES""" 113 | newlines = len(p[1]) 114 | self.__lineno += newlines 115 | p[0] = PrimitiveElement('newlines', newlines) 116 | 117 | @staticmethod 118 | def p_empty(p): 119 | """empty :""" 120 | p[0] = PrimitiveElement() 121 | 122 | @staticmethod 123 | def p_bracket_argument_content(p): 124 | """bracket_argument_content : bracket_argument_content bracket_argument_content_element 125 | | bracket_argument_content_element""" 126 | _create_content(p, 'bracket_argument_content') 127 | 128 | @staticmethod 129 | def p_quoted_argument_content(p): 130 | """quoted_argument_content : quoted_argument_content quoted_argument_content_element 131 | | quoted_argument_content_element 132 | | empty""" 133 | _create_content(p, 'quoted_argument_content') 134 | 135 | @staticmethod 136 | def p_bracket_argument_content_element(p): 137 | """bracket_argument_content_element : BRACKET_ARGUMENT_CONTENT""" 138 | p[0] = _get_content_element(p[1]) 139 | 140 | @staticmethod 141 | def p_quoted_argument_content_element(p): 142 | """quoted_argument_content_element : QUOTED_ARGUMENT_CONTENT""" 143 | p[0] = _get_content_element(p[1]) 144 | 145 | @staticmethod 146 | def p_spaces(p): 147 | """spaces : SPACES""" 148 | p[0] = PrimitiveElement('spaces', p[1]) 149 | 150 | @staticmethod 151 | def p_start_parenthesis(p): 152 | """start_parenthesis : BEGIN_PARENTHESIS""" 153 | p[0] = PrimitiveElement('parenthesis_start', p[1]) 154 | 155 | @staticmethod 156 | def p_end_parenthesis(p): 157 | """end_parenthesis : END_PARENTHESIS""" 158 | p[0] = PrimitiveElement('parenthesis_end', p[1]) 159 | 160 | @staticmethod 161 | def p_start_cmd_invoke(p): 162 | """start_cmd_invoke : COMMAND_INVOCATION_START""" 163 | p[0] = PrimitiveElement('start_cmd_invoke', p[1]) 164 | 165 | @staticmethod 166 | def p_end_cmd_invoke(p): 167 | """end_cmd_invoke : COMMAND_INVOCATION_END""" 168 | p[0] = PrimitiveElement('end_cmd_invoke', p[1]) 169 | 170 | def p_error(self, p): 171 | if p is not None: 172 | raise ParsingError(f'Illegal symbol in line {self.__lineno}: \"{p.value}\"') 173 | raise ParsingError('Missing symbol - some invocation is not properly finished') 174 | 175 | def parse(self, data: str): 176 | return self.parser.parse(data) 177 | 178 | 179 | def _get_content_element(data) -> PrimitiveElement: 180 | return PrimitiveElement('content_element', str(data)) 181 | 182 | 183 | def _create_content(p, name: str): 184 | if p[1].name is name: 185 | p[0] = p[1] 186 | p[0].values += p[2].values 187 | else: 188 | p[0] = PrimitiveElement(name, p[1].values) 189 | -------------------------------------------------------------------------------- /cmake_tidy/run.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import sys 8 | 9 | from cmake_tidy.commands.analyze.analyze_command import AnalyzeCommand 10 | from cmake_tidy.commands.format.format_command import FormatCommand 11 | from cmake_tidy.utils import ExitCodes 12 | from cmake_tidy.utils.command_line_handling.command_line_parser import CommandLineParser 13 | from cmake_tidy.version import show_version 14 | 15 | 16 | def run(): 17 | main() 18 | 19 | 20 | def main(args=sys.argv[1:]): 21 | parser = __init_parser() 22 | arguments = parser.parse(args) 23 | sys.exit(__execute(arguments, parser)) 24 | 25 | 26 | def __init_parser() -> CommandLineParser: 27 | parser = CommandLineParser() 28 | parser.add_command(FormatCommand) 29 | parser.add_command(AnalyzeCommand) 30 | return parser 31 | 32 | 33 | def __execute(arguments, parser) -> int: 34 | if arguments.version: 35 | return show_version() 36 | elif arguments.sub_command: 37 | return arguments.func(arguments) 38 | parser.print_help() 39 | return ExitCodes.SUCCESS 40 | -------------------------------------------------------------------------------- /cmake_tidy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.utils.exit_codes import ExitCodes 8 | -------------------------------------------------------------------------------- /cmake_tidy/utils/app_configuration/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.utils.app_configuration.configuration import ConfigurationError 8 | -------------------------------------------------------------------------------- /cmake_tidy/utils/app_configuration/configuration.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import inspect 8 | 9 | 10 | class ConfigurationError(Exception): 11 | pass 12 | 13 | 14 | class Configuration: 15 | def __init__(self, arguments): 16 | self._config = {} 17 | for name in self.__get_all_property_names(): 18 | if arguments.get(name) is not None: 19 | self._config[name] = arguments.get(name) 20 | 21 | @property 22 | def all_properties(self): 23 | return {name: getattr(self, name) for name in self.__get_all_property_names()} 24 | 25 | @staticmethod 26 | def _property_name(): 27 | return inspect.stack()[1][3] 28 | 29 | def __get_all_property_names(self) -> list: 30 | properties = [name for name, func in inspect.getmembers(self.__class__, self.__is_property)] 31 | properties.remove('all_properties') 32 | return properties 33 | 34 | @staticmethod 35 | def __is_property(v) -> bool: 36 | return isinstance(v, property) 37 | -------------------------------------------------------------------------------- /cmake_tidy/utils/command_line_handling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaciejPatro/cmake-tidy/ddab3d9c6dd1a6c9cfa47bff5a9f120defea9e6a/cmake_tidy/utils/command_line_handling/__init__.py -------------------------------------------------------------------------------- /cmake_tidy/utils/command_line_handling/arguments.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | def input_data(parser): 8 | parser.add_argument('input', 9 | type=str, 10 | nargs='*', 11 | help='CMake file to be formatted') 12 | 13 | 14 | def dump_config(parser): 15 | parser.add_argument('--dump-config', 16 | action='store_true', 17 | help='Dump to stdout current settings. Script tries to read settings from `.cmake-tidy.json` ' 18 | 'or provides default settings. Precedence of searching `.cmake-tidy.json` is described ' 19 | 'on github') 20 | 21 | 22 | def inplace(parser): 23 | parser.add_argument('-i', '--inplace', 24 | action='store_true', 25 | help='Inplace edit specified file') 26 | 27 | 28 | def diff(parser): 29 | parser.add_argument('--diff', 30 | action='store_true', 31 | help='Print to stdout unified diff between original file and formatted version.') 32 | 33 | 34 | def verbose(parser): 35 | parser.add_argument('--verbose', 36 | action='store_true', 37 | help='Print to stdout information about formatted file') 38 | -------------------------------------------------------------------------------- /cmake_tidy/utils/command_line_handling/command_line_parser.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import argparse 8 | 9 | 10 | class CommandLineParser: 11 | def __init__(self): 12 | self.__parser = argparse.ArgumentParser(prog='cmake-tidy', 13 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 14 | 15 | self.__parser.add_argument('-v', '--version', action='store_true', default=False) 16 | 17 | self.__sub_parser = self.__parser.add_subparsers(title='sub-commands', 18 | dest='sub_command', 19 | help='see "cmake-tidy --help" to read more ' 20 | 'about a specific sub-command.') 21 | self.__commands = [] 22 | 23 | def add_command(self, class_name): 24 | self.__commands.append(class_name(self.__sub_parser)) 25 | 26 | def parse(self, args=None): 27 | return self.__parser.parse_args(args) 28 | 29 | def print_help(self): 30 | self.__parser.print_help() 31 | -------------------------------------------------------------------------------- /cmake_tidy/utils/diff.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import difflib 8 | from pathlib import Path 9 | 10 | 11 | def get_unified_diff(original: str, modified: str, filename: Path) -> str: 12 | diff = difflib.unified_diff(a=original.splitlines(keepends=True), 13 | b=modified.splitlines(keepends=True), 14 | fromfile=str(filename), 15 | tofile=str(filename)) 16 | return ''.join(diff) 17 | -------------------------------------------------------------------------------- /cmake_tidy/utils/exit_codes.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from enum import IntEnum, unique 8 | 9 | 10 | @unique 11 | class ExitCodes(IntEnum): 12 | SUCCESS = 0 13 | FAILURE = -1 14 | -------------------------------------------------------------------------------- /cmake_tidy/utils/proxy_visitor.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | class ProxyVisitor: 8 | def __init__(self, proxies: dict): 9 | self.__proxies = proxies 10 | 11 | def visit(self, name: str, values=None): 12 | if name in self.__proxies: 13 | return self.__proxies[name](values) 14 | -------------------------------------------------------------------------------- /cmake_tidy/version.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.lexical_data import KeywordVerifier 8 | from cmake_tidy.utils import ExitCodes 9 | 10 | VERSION = '0.5.0' 11 | 12 | 13 | def show_version() -> int: 14 | print(f'cmake-tidy: {VERSION} | supported CMake: {KeywordVerifier(dict()).get_cmake_properties_version()}') 15 | return ExitCodes.SUCCESS 16 | -------------------------------------------------------------------------------- /doc/config.adoc: -------------------------------------------------------------------------------- 1 | = `format` subcommand configuration 2 | 3 | :numbered: 4 | 5 | == Usage 6 | 7 | There is a number of configurable settings possible to provide in `.cmake-tidy.json` configuration file. 8 | The file is searched in following locations and order of precedence 9 | 10 | * location of `input_file` to be formatted 11 | * parent locations of `input_file` 12 | * current working directory of the tool (`cwd`) 13 | 14 | == Settings and default values 15 | 16 | [cols="2,1,1, 6a", options="header"] 17 | .Settings 18 | |=== 19 | |Name 20 | |Type 21 | |Default Value 22 | |Detailed description 23 | 24 | |`succeeding_newlines` 25 | |`integer` 26 | |`2` 27 | | Describes the maximum number of succeeding newlines that can appear in formatted file. 28 | 29 | |`tabs_as_spaces` 30 | |`boolean` 31 | |`False` 32 | | Dictates whether code indentation should be done using spaces (`True`) or tabs (`False`). 33 | 34 | |`tab_size` 35 | |`integer` 36 | |`4` 37 | | When `tabs_as_spaces` is `True` this option defines the amount of spaces used for a single `TAB`. 38 | 39 | |`force_command_lowercase` 40 | |`boolean` 41 | |`True` 42 | | Convert all command invocations to lowercase (including keywords) eg. `SOME_FUNC()` to `some_func()` and `IF(...)` to `if(...)`. 43 | 44 | |`space_between_command_and_begin_parentheses` 45 | |`boolean` 46 | |`False` 47 | | Insert space between command name and begin parentheses eg. `if()` to ``\"``if ()` or `set()` to `set ()`. 48 | 49 | |`line_length` 50 | |`integer` 51 | |`100` 52 | | Maximum line length that will not cause `command invocation` splitting. 53 | 54 | |`wrap_short_invocations_to_single_line` 55 | |`boolean` 56 | |`True` 57 | | Wrap command invocations into single line when they fit in `line_length`. 58 | from: 59 | 60 | [source,cmake] 61 | ---- 62 | target_sources(${PROJECT_NAME} 63 | PRIVATE 64 | file1.cpp 65 | ) 66 | ---- 67 | 68 | to: 69 | 70 | [source,cmake] 71 | ---- 72 | target_sources(${PROJECT_NAME} PRIVATE file1.cpp) 73 | ---- 74 | 75 | |`closing_parentheses_in_newline_when_split` 76 | |`boolean` 77 | |`True` 78 | | Force closing parentheses to new line when command invocation splitting is needed. 79 | from: 80 | 81 | [source,cmake] 82 | ---- 83 | target_sources(${PROJECT_NAME} 84 | PRIVATE 85 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/file1.cpp 86 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/file2.cpp 87 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/file3.cpp) 88 | ---- 89 | 90 | to: 91 | 92 | [source,cmake] 93 | ---- 94 | target_sources(${PROJECT_NAME} 95 | PRIVATE 96 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/file1.cpp 97 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/file2.cpp 98 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/file3.cpp 99 | ) 100 | ---- 101 | 102 | |`unquoted_uppercase_as_keyword` 103 | |`boolean` 104 | |`False` 105 | | Consider all unquoted uppercase arguments as keywords. eg. 106 | `keywords`: 107 | [source,cmake] 108 | ---- 109 | SOME 110 | TEXT_IS_KEYWORD 111 | ---- 112 | 113 | treated as normal arguments: 114 | [source,cmake] 115 | ---- 116 | _SOME 117 | TEXT__IS_KEYWORD 118 | NOT_ 119 | a_ARGUMENT 120 | SOMeARG 121 | ---- 122 | 123 | 124 | |`space_after_loop_condition` 125 | |`boolean` 126 | |`False` 127 | | Introduces spaces after all conditional invocations only eg. 128 | 129 | [source,cmake] 130 | ---- 131 | if (Linux STREQUAL ${CMAKE_SYSTEM_NAME}) 132 | target_sources(${PROJECT_NAME} PRIVATE file.cpp) 133 | elseif (Windows STREQUAL ${CMAKE_SYSTEM_NAME}) 134 | target_sources(${PROJECT_NAME} PRIVATE different.cpp) 135 | endif() 136 | ---- 137 | 138 | |`keep_property_and_value_in_one_line` 139 | |`boolean` 140 | |`True` 141 | | Forces no splitting between property and value even if the line is too long. eg. 142 | 143 | [source,cmake] 144 | ---- 145 | set_property( 146 | SOURCE 147 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 148 | PROPERTY 149 | COMPILE_FLAGS $<$:/Y-> # ignore precompiled headers 150 | ) 151 | ---- 152 | 153 | |`keep_command_in_single_line` 154 | |`boolean` 155 | |`True` 156 | | Keeps `COMMAND` within single line. Enabled: 157 | 158 | [source,cmake] 159 | ---- 160 | add_custom_target(${target}-resources 161 | ALL 162 | COMMAND 163 | ${CMAKE_COMMAND} -E echo "Copy resource files for ${target}" 164 | ---- 165 | Disabled: 166 | [source,cmake] 167 | ---- 168 | add_custom_target(${target}-resources 169 | ALL 170 | COMMAND 171 | ${CMAKE_COMMAND} 172 | -E 173 | echo 174 | "Copy resource files for ${target}" 175 | ---- 176 | 177 | |`keyword_and_single_value_in_one_line` 178 | |`boolean` 179 | |`True` 180 | | Enforces keyword and its value to be in single line. Disabled: 181 | 182 | [source,cmake] 183 | ---- 184 | install( 185 | FILES 186 | file.cpp 187 | file.hpp 188 | DESTINATION 189 | "include/folder" 190 | ) 191 | ---- 192 | Enabled: 193 | [source,cmake] 194 | ---- 195 | install( 196 | FILES 197 | file.cpp 198 | file.hpp 199 | DESTINATION "include/folder" 200 | ) 201 | ---- 202 | 203 | |`condition_splitting_move_and_or_to_newline` 204 | |`boolean` 205 | |`True` 206 | | When splitting conditional invocation when enabled splits invocation 207 | before AND/OR operators: 208 | 209 | [source,cmake] 210 | ---- 211 | if(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" 212 | OR CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019") 213 | ---- 214 | Disabled: 215 | [source,cmake] 216 | ---- 217 | if(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" OR 218 | CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019") 219 | ---- 220 | 221 | |`keywords` 222 | |`array` 223 | |`[]` 224 | | Consider list of provided arguments as keywords when formatting. 225 | 226 | |=== -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | approvaltests~=0.2.6 2 | mock~=4.0.1 3 | ply~=3.11 4 | iteration_utilities~=0.10.1 5 | jsonschema~=3.2.0 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from pip.req import parse_requirements 5 | from setuptools import setup, find_packages 6 | 7 | install_requirements = parse_requirements('./requirements.txt', session=False) 8 | install_requirements = [str(ir.req) for ir in install_requirements] 9 | 10 | 11 | def load_version(): 12 | filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "cmake_tidy", "version.py")) 13 | with open(filename, "rt") as version_file: 14 | return re.search(r"VERSION = '([0-9a-z.-]+)'", version_file.read()).group(1) 15 | 16 | 17 | setup( 18 | name='cmake-tidy', 19 | version=load_version(), 20 | python_requires='>=3.6.0', 21 | packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), 22 | url='https://github.com/MaciejPatro/cmake-tidy', 23 | license='LICENSE', 24 | author='Maciej Patro', 25 | author_email='maciejpatro@gmail.com', 26 | install_requires=install_requirements, 27 | description='cmake-tidy is a tool to format/analyze cmake source files.', 28 | long_description='For More information visit https://github.com/MaciejPatro/cmake-tidy', 29 | long_description_content_type='text/plain', 30 | package_data={ 31 | '': ['*.json', '*.adoc', '*.txt'] 32 | }, 33 | classifiers=[ 34 | 'Intended Audience :: Developers', 35 | 'Topic :: Software Development :: Build Tools', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Programming Language :: Python :: 3', 38 | 'Programming Language :: Python :: 3.6', 39 | 'Programming Language :: Python :: 3.7', 40 | 'Programming Language :: Python :: 3.8' 41 | ], 42 | keywords=['cmake', 'format', 'static-analysis', 'developer', 'tool'], 43 | entry_points={ 44 | 'console_scripts': [ 45 | 'cmake-tidy=cmake_tidy.run:run', 46 | ], 47 | }, 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaciejPatro/cmake-tidy/ddab3d9c6dd1a6c9cfa47bff5a9f120defea9e6a/tests/__init__.py -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaciejPatro/cmake-tidy/ddab3d9c6dd1a6c9cfa47bff5a9f120defea9e6a/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/approvaltests_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "subdirectory": "approved_files" 3 | } -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidy.test_incorrect_command_should_print_error_with_usage_help.approved.txt: -------------------------------------------------------------------------------- 1 | usage: cmake-tidy [-h] [-v] {format,analyze} ... 2 | cmake-tidy: error: argument sub_command: invalid choice: 'invalid' (choose from 'format', 'analyze') 3 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidy.test_version_argument_should_provide_correct_tool_version.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy: 0.5.0 | supported CMake: 3.18.0 2 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyAnalyze.test_analyze_command_help_shown.approved.txt: -------------------------------------------------------------------------------- 1 | usage: cmake-tidy analyze [-h] 2 | 3 | optional arguments: 4 | -h, --help show this help message and exit 5 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyAnalyze.test_analyze_should_correctly_print_version.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy: X.X.X | supported CMake: X.X.X 2 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_command_help_shown.approved.txt: -------------------------------------------------------------------------------- 1 | usage: cmake-tidy format [-h] [--dump-config] [-i] [--diff] [--verbose] 2 | [input [input ...]] 3 | 4 | positional arguments: 5 | input CMake file to be formatted 6 | 7 | optional arguments: 8 | -h, --help show this help message and exit 9 | --dump-config Dump to stdout current settings. Script tries to read 10 | settings from `.cmake-tidy.json` or provides default 11 | settings. Precedence of searching `.cmake-tidy.json` is 12 | described on github 13 | -i, --inplace Inplace edit specified file 14 | --diff Print to stdout unified diff between original file and 15 | formatted version. 16 | --verbose Print to stdout information about formatted file 17 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_inplace_simple_file_with_verbose_option.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set([==[ currently a weird bracket argument introduced 10 | some 2839697%%*^$& text ]===] fake close and stuff]==] 11 | some 12 | other 13 | [===[www]===] 14 | [======[this 15 | should 16 | be 17 | indented differently 18 | ]======] 19 | "quoted argument with \" escaped quote" 20 | ) 21 | 22 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 23 | 24 | add_subdirectory(sum_of_non_adjacent) 25 | add_subdirectory(record_last_n_logs) 26 | add_subdirectory(max_values_subarrays) 27 | add_subdirectory(string_distance) 28 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_inplace_with_error_should_inform_about_failure_and_keep_initial_file.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy format: Illegal symbol in line 4: ")" 2 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_correctly_print_version.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy: X.X.X | supported CMake: X.X.X 2 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_dump_config_only_configuration_to_stdout_by_default.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "succeeding_newlines": 2, 3 | "tabs_as_spaces": false, 4 | "tab_size": 4, 5 | "force_command_lowercase": true, 6 | "space_between_command_and_begin_parentheses": false, 7 | "line_length": 100, 8 | "wrap_short_invocations_to_single_line": true, 9 | "closing_parentheses_in_newline_when_split": true, 10 | "unquoted_uppercase_as_keyword": false, 11 | "space_after_loop_condition": false, 12 | "keep_property_and_value_in_one_line": true, 13 | "keyword_and_single_value_in_one_line": true, 14 | "keep_command_in_single_line": true, 15 | "condition_splitting_move_and_or_to_newline": true, 16 | "keywords": [] 17 | } 18 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_dump_full_config_even_if_file_overrides_only_one.approved.txt: -------------------------------------------------------------------------------- 1 | { 2 | "succeeding_newlines": 2, 3 | "tabs_as_spaces": false, 4 | "tab_size": 4, 5 | "force_command_lowercase": true, 6 | "space_between_command_and_begin_parentheses": false, 7 | "line_length": 100, 8 | "wrap_short_invocations_to_single_line": true, 9 | "closing_parentheses_in_newline_when_split": true, 10 | "unquoted_uppercase_as_keyword": false, 11 | "space_after_loop_condition": false, 12 | "keep_property_and_value_in_one_line": true, 13 | "keyword_and_single_value_in_one_line": true, 14 | "keep_command_in_single_line": true, 15 | "condition_splitting_move_and_or_to_newline": true, 16 | "keywords": [ 17 | "CUSTOM_KEYWORD" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_fail_with_warning_about_incorrect_settings_when_dump_invoked.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy format: 33 is not of type 'boolean' 2 | 3 | Failed validating 'type' in schema['properties']['tabs_as_spaces']: 4 | {'description': 'Indentation in the code should be done by spaces ' 5 | '(True) or tabs (False)', 6 | 'type': 'boolean'} 7 | 8 | On instance['tabs_as_spaces']: 9 | 33 10 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_fail_with_warning_about_incorrect_settings_when_trying_to_format.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy format: 33 is not of type 'boolean' 2 | 3 | Failed validating 'type' in schema['properties']['tabs_as_spaces']: 4 | {'description': 'Indentation in the code should be done by spaces ' 5 | '(True) or tabs (False)', 6 | 'type': 'boolean'} 7 | 8 | On instance['tabs_as_spaces']: 9 | 33 10 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_provide_unified_diff_to_stdout.approved.txt: -------------------------------------------------------------------------------- 1 | --- /arguments.cmake 2 | +++ /arguments.cmake 3 | @@ -7,15 +7,17 @@ 4 | 5 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 6 | set([==[ currently a weird bracket argument introduced 7 | -some 2839697%%*^$& text ]===] fake close and stuff]==] some 8 | - other 9 | - [===[www]===] 10 | - [======[this 11 | +some 2839697%%*^$& text ]===] fake close and stuff]==] 12 | + some 13 | + other 14 | + [===[www]===] 15 | +[======[this 16 | should 17 | be 18 | indented differently 19 | ]======] 20 | - "quoted argument with \" escaped quote") 21 | + "quoted argument with \" escaped quote" 22 | +) 23 | 24 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_format_should_return_error_when_file_is_read_only_and_inplace_param_is_used.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy format: File arguments.cmake is read-only! 2 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestCMakeTidyFormat.test_incorrect_command_should_print_error_with_usage_help.approved.txt: -------------------------------------------------------------------------------- 1 | cmake-tidy format: Error - incorrect "input" - please specify existing file to be formatted 2 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_condition_formatting_with_different_kinds_of_parentheses.approved.txt: -------------------------------------------------------------------------------- 1 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" 2 | AND (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9" 3 | OR (CMAKE_SIZE_OF_VOID_P EQUAL 4 4 | OR (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm")))) # TODO: implict definition of A_PLATFORM 5 | endif() 6 | 7 | if((((${CMAKE_CXX_COMPILER_ID}) STREQUAL "GNU" 8 | AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9") 9 | OR CMAKE_SIZE_OF_VOID_P EQUAL 4) 10 | OR ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm") # TODO: implict definition of A_PLATFORM 11 | endif() 12 | 13 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" 14 | AND (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9" 15 | OR CMAKE_SIZE_OF_VOID_P EQUAL 4) 16 | OR ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm") 17 | endif() 18 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_against_newline_violations.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | 5 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 6 | 7 | find_package(GTest REQUIRED) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | add_subdirectory(string_distance) 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_against_newline_violations_with_custom_settings.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | 4 | 5 | include(CTest) 6 | 7 | 8 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 9 | 10 | find_package(GTest REQUIRED) 11 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 12 | 13 | add_subdirectory(string_distance) 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_bracket_arguments_handling.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set([==[ currently a weird bracket argument introduced 10 | some 2839697%%*^$& text ]===] fake close and stuff]==] 11 | some 12 | other 13 | [===[www]===] 14 | [======[this 15 | should 16 | be 17 | indented differently 18 | ]======] 19 | "quoted argument with \" escaped quote" 20 | ) 21 | 22 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 23 | 24 | add_subdirectory(sum_of_non_adjacent) 25 | add_subdirectory(record_last_n_logs) 26 | add_subdirectory(max_values_subarrays) 27 | add_subdirectory(string_distance) 28 | 29 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_command_should_print_file_to_output.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | add_subdirectory(sum_of_non_adjacent) 13 | add_subdirectory(record_last_n_logs) 14 | add_subdirectory(max_values_subarrays) 15 | add_subdirectory(string_distance) 16 | 17 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_indentation_of_basic_invocations.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | function(doubleEach) 4 | foreach(ARG ${ARGN}) # Iterate over each argument 5 | if(${ARG} MATCHES "\w+" "") 6 | math(EXPR N "${ARG} * 2") 7 | else() 8 | message("${N}") # Print N 9 | endif() 10 | endforeach() 11 | endfunction() 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_indentation_when_spaces_after_command_name_are_present.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.10) 2 | 3 | function (doubleEach) 4 | foreach (ARG ${ARGN}) # Iterate over each argument 5 | if (${ARG} MATCHES "\w+" "") 6 | math (EXPR N "${ARG} * 2") 7 | else () 8 | message ("${N}") # Print N 9 | endif () 10 | endforeach () 11 | endfunction () 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_line_splitting.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | foreach(ARG ${ARGN}) # Iterate over each argument 8 | if(${ARG} (MATCHES ("\w+"))) 9 | math(EXPR N "${ARG} * 2") 10 | else() 11 | set(a_very_long_invocation_that 12 | should_be_just_out 13 | the_border_of__80 14 | chars 15 | ) 16 | set(a_very_long_invocation_that 17 | should_be_just_out 18 | the_border_of__80 19 | chars_clearly_so 20 | ) 21 | set(a very long invocation that is just below the limit of chars supp) 22 | message("${N}") # Print N 23 | this(should be wrapped "adewa") 24 | endif() 25 | endforeach() 26 | 27 | foreach(X 28 | aaaa 29 | bbbb 30 | cccc 31 | dddd 32 | eeee 33 | ffff 34 | gggg 35 | hhhh 36 | jjjj 37 | iiii 38 | kkkk 39 | llll 40 | mmmm 41 | nnnn 42 | oooo 43 | pppp 44 | rrrr 45 | ) 46 | message("${X}") 47 | endforeach() 48 | 49 | foreach(X 50 | IN LISTS 51 | aaaa 52 | bbbb 53 | cccc 54 | dddd 55 | eeee 56 | ffff 57 | gggg 58 | hhhh 59 | jjjj 60 | iiii 61 | kkkk 62 | llll 63 | mmmm 64 | nnnn 65 | oooo 66 | pppp 67 | rrrr 68 | ) 69 | message("${X}") 70 | endforeach() 71 | 72 | foreach(loop_count RANGE ${very_long_start_name} ${even_longer_finish_name}) 73 | message("${X}") 74 | endforeach() 75 | 76 | foreach(first_name 77 | second_name 78 | IN ZIP_LISTS 79 | long_list_of_first_names 80 | long_list_of_second_names 81 | ) 82 | message(STATUS "en=${en}, ba=${ba}") 83 | endforeach() 84 | 85 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_format_tabs_with_spaces_replacement.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | foreach(ARG ${ARGN}) # Iterate over each argument 13 | if(${ARG} (MATCHES ("\w+"))) 14 | math(EXPR N "${ARG} * 2") 15 | else() 16 | message("${N}") # Print N 17 | endif() 18 | endforeach() 19 | 20 | add_subdirectory(sum_of_non_adjacent) 21 | add_subdirectory(record_last n_logs) 22 | add_subdirectory(max_values_subarrays) 23 | add_subdirectory(string_distance) 24 | add_subdirectory(new_dir) 25 | 26 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_formatting_complicated_conditions.approved.txt: -------------------------------------------------------------------------------- 1 | 2 | if(NOT Value) 3 | if(GoogleTest_FOUND AND NOT (TARGET GoogleTest::GoogleTest)) 4 | set(SOME_VALUE ON) 5 | elseif(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" 6 | OR CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019") 7 | set(SOME_VALUE ON) 8 | if(EXISTS ${CMAKE_HOME_DIRECTORY}/ABC/${GTest_DIR}/${GTest_COMPONENT}/source) 9 | set(SOME_VALUE ON) 10 | endif() 11 | if(BUILD_SHARED_LIBS 12 | AND ((${CMAKE_SYSTEM_NAME} STREQUAL Windows) 13 | OR (${CMAKE_SYSTEM_NAME} STREQUAL WindowsCE 14 | AND NOT ${CMAKE_SYSTEM_VERSION} VERSION_EQUAL 6.00))) 15 | set(SOME_VALUE ON) 16 | if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") 17 | AND ((${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9") 18 | OR (CMAKE_SIZE_OF_VOID_P EQUAL 4) 19 | OR (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm"))) # TODO: implict definition of A_PLATFORM 20 | set(SOME_VALUE ON) 21 | 22 | endif() 23 | endif() 24 | 25 | endif() 26 | endif() 27 | 28 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_formatting_complicated_conditions_splitting_after_operator.approved.txt: -------------------------------------------------------------------------------- 1 | 2 | if(NOT Value) 3 | if(GoogleTest_FOUND AND NOT (TARGET GoogleTest::GoogleTest)) 4 | set(SOME_VALUE ON) 5 | elseif(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" OR 6 | CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019") 7 | set(SOME_VALUE ON) 8 | if(EXISTS ${CMAKE_HOME_DIRECTORY}/ABC/${GTest_DIR}/${GTest_COMPONENT}/source) 9 | set(SOME_VALUE ON) 10 | endif() 11 | if(BUILD_SHARED_LIBS AND 12 | ((${CMAKE_SYSTEM_NAME} STREQUAL Windows) OR 13 | (${CMAKE_SYSTEM_NAME} STREQUAL WindowsCE AND 14 | NOT ${CMAKE_SYSTEM_VERSION} VERSION_EQUAL 6.00))) 15 | set(SOME_VALUE ON) 16 | if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") AND 17 | ((${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9") OR 18 | (CMAKE_SIZE_OF_VOID_P EQUAL 4) OR 19 | (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm"))) # TODO: implict definition of A_PLATFORM 20 | set(SOME_VALUE ON) 21 | 22 | endif() 23 | endif() 24 | 25 | endif() 26 | endif() 27 | 28 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_formatting_file_with_multiple_settings.approved.txt: -------------------------------------------------------------------------------- 1 | ################################################ 2 | # BLABLA 3 | ################################################ 4 | 5 | cmake_minimum_required(VERSION 3.13 FATAL_ERROR) 6 | project(A_PROJECT) 7 | 8 | find_package(Library REQUIRED) 9 | 10 | add_library(${PROJECT_NAME}) 11 | add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 12 | 13 | set_target_properties(${PROJECT_NAME} 14 | PROPERTIES 15 | FOLDER Components 16 | ) 17 | 18 | target_sources(${PROJECT_NAME} 19 | PRIVATE 20 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 21 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 22 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 23 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 24 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 25 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 26 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 27 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 28 | ) 29 | 30 | if(${CMAKE_SYSTEM_NAME} STREQUAL Windows OR ${CMAKE_SYSTEM_NAME} STREQUAL Linux) 31 | target_sources(${PROJECT_NAME} 32 | PRIVATE 33 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 34 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 35 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 36 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 37 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 38 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 39 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 40 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 41 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 42 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 43 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 44 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 45 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 46 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 47 | ) 48 | endif() 49 | 50 | set_property( 51 | SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 52 | PROPERTY 53 | COMPILE_FLAGS $<$:/Y-> # ignore precompiled headers 54 | ) 55 | 56 | target_link_libraries(${PROJECT_NAME} 57 | PUBLIC 58 | boost::boost 59 | PRIVATE 60 | $<$:pthread> 61 | ) 62 | 63 | target_include_directories(${PROJECT_NAME} 64 | PUBLIC 65 | $ 66 | PRIVATE 67 | $ 68 | ) 69 | 70 | include(CMakePackage OPTIONAL) 71 | if(COMMAND target_set) 72 | target_set(${PROJECT_NAME} 73 | GROUP 74 | File.cpp 75 | DIRECTORY 76 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 77 | ) 78 | endif() 79 | 80 | ################################################ 81 | # BLABLA 82 | ################################################ 83 | add_custom_command( 84 | TARGET 85 | ${_TARGET} 86 | POST_BUILD 87 | ) 88 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_formatting_of_install_commands.approved.txt: -------------------------------------------------------------------------------- 1 | 2 | install( 3 | TARGETS 4 | gtest 5 | gtest_main 6 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 7 | ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 8 | LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" 9 | ) 10 | 11 | install(TARGETS mylibapp RUNTIME DESTINATION bin COMPONENT applications) 12 | install( 13 | FILES "${PYLON_BINARY_DIRS}/PylonUsb_MD_VC120_V5_0_TL.dll" 14 | DESTINATION ${CMAKE_INSTALL_BINARY} 15 | CONFIGURATIONS 16 | Release 17 | RelWithDebInfo 18 | ) 19 | 20 | install( 21 | EXPORT graphicsmagick-targets 22 | FILE unofficial-graphicsmagick-targets.cmake 23 | NAMESPACE unofficial::graphicsmagick:: 24 | DESTINATION share/unofficial-graphicsmagick 25 | ) 26 | 27 | install(FILES $.genex DESTINATION $<1:lib>$<0:/wrong>) 28 | 29 | install( 30 | TARGETS tinyfiledialogs 31 | EXPORT tinyfiledialogsConfig 32 | ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 33 | LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" 34 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 35 | INCLUDES DESTINATION "${CMAKE_INSTALL_BINDIR}" 36 | FILES_MATCHING PATTERN ".*" 37 | FILES_MATCHING EXCLUDE abc.cpp 38 | ) 39 | install(FILES tinyfiledialogs.h DESTINATION "${CMAKE_INSTALL_PREFIX}/include/tinyfiledialogs") 40 | 41 | export( 42 | TARGETS tinyfiledialogs 43 | NAMESPACE tinyfiledialogs:: 44 | FILE "${CMAKE_CURRENT_BINARY_DIR}/tinyfiledialogsConfig.cmake" 45 | ) 46 | 47 | install( 48 | EXPORT tinyfiledialogsConfig 49 | NAMESPACE tinyfiledialogs:: 50 | DESTINATION "${CMAKE_INSTALL_PREFIX}/share/tinyfiledialogs" 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_formatting_with_tabs.approved.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | foreach(ARG ${ARGN}) # Iterate over each argument 8 | if(${ARG} (MATCHES ("\w+"))) 9 | math(EXPR N "${ARG} * 2") 10 | else() 11 | set(a_very_long_invocation_that 12 | should_be_just_out 13 | the_border_of__80 14 | chars 15 | ) 16 | set(a_very_long_invocation_that 17 | should_be_just_out 18 | the_border_of__80 19 | chars_clearly_so 20 | ) 21 | set(a very long invocation that is just below the limit of chars supp) 22 | message("${N}") # Print N 23 | this(should be wrapped "adewa") 24 | endif() 25 | endforeach() 26 | 27 | foreach(X 28 | aaaa 29 | bbbb 30 | cccc 31 | dddd 32 | eeee 33 | ffff 34 | gggg 35 | hhhh 36 | jjjj 37 | iiii 38 | kkkk 39 | llll 40 | mmmm 41 | nnnn 42 | oooo 43 | pppp 44 | rrrr 45 | ) 46 | message("${X}") 47 | endforeach() 48 | 49 | foreach(X 50 | IN LISTS 51 | aaaa 52 | bbbb 53 | cccc 54 | dddd 55 | eeee 56 | ffff 57 | gggg 58 | hhhh 59 | jjjj 60 | iiii 61 | kkkk 62 | llll 63 | mmmm 64 | nnnn 65 | oooo 66 | pppp 67 | rrrr 68 | ) 69 | message("${X}") 70 | endforeach() 71 | 72 | foreach(loop_count RANGE ${very_long_start_name} ${even_longer_finish_name}) 73 | message("${X}") 74 | endforeach() 75 | 76 | foreach(first_name 77 | second_name 78 | IN ZIP_LISTS 79 | long_list_of_first_names 80 | long_list_of_second_names 81 | ) 82 | message(STATUS "en=${en}, ba=${ba}") 83 | endforeach() 84 | 85 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_handling_of_single_line_comments_within_different_parts_of_cmake_file.approved.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | # a very long comment with some text and stuff 4 | # at the beginning of the file - absolutely no need for reading this text - everyone ignores comments either way so please 5 | # do it also in this particular case 6 | if(NOT Value) 7 | # here the comment should be indented 8 | if(GoogleTest_FOUND AND NOT (TARGET GoogleTest::GoogleTest)) 9 | set(SOME_VALUE ON) 10 | 11 | install( 12 | TARGETS mylibapp 13 | RUNTIME 14 | # TODO: there is some very important information 15 | # why this silly line of code beneath is really here 16 | DESTINATION bin 17 | COMPONENT applications 18 | ) 19 | install( 20 | FILES "${PYLON_BINARY_DIRS}/PylonUsb_MD_VC120_V5_0_TL.dll" 21 | DESTINATION ${CMAKE_INSTALL_BINARY} 22 | CONFIGURATIONS 23 | # here we should have other identation also with single line 24 | Release 25 | RelWithDebInfo 26 | ) 27 | 28 | elseif(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" 29 | OR CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019") 30 | install( 31 | # do we handle arguments correctly? 32 | # are you certain? 33 | EXPORT graphicsmagick-targets 34 | FILE unofficial-graphicsmagick-targets.cmake # standard line-comment 35 | NAMESPACE unofficial::graphicsmagick:: 36 | DESTINATION share/unofficial-graphicsmagick 37 | # we can also check what happens here assuming someone commented out single line 38 | ) 39 | 40 | target_link_options(${PROJECT_NAME} 41 | PRIVATE $<$:/WX:NO> # FIXME: and text 42 | ) 43 | 44 | target_compile_options(${PROJECT_NAME} 45 | PRIVATE 46 | $<$:/wd4251> # warning about DLL interface 47 | $<$,$,$>,$>:/QRfpe-> 48 | $<$:-w> # TODO: suppress all GCC warnings 49 | $<$:-Wno-everything> # TODO: suppress all Clang warnings 50 | ) 51 | endif() 52 | endif() 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_real_implementation_of_feature_in_cmake.approved.txt: -------------------------------------------------------------------------------- 1 | if(ENABLE_MSVC_SOME_FEATURE_PREPARE) 2 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE TRUE) 3 | else() 4 | set_property( 5 | GLOBAL PROPERTY 6 | CONFIGURATION_MSVC_SOME_FEATURE_PREPARE FALSE 7 | ) 8 | endif() 9 | 10 | option(ENABLE_MSVC_SOME_FEATURE "Some Text written here" OFF) 11 | if(ENABLE_MSVC_SOME_FEATURE) 12 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE TRUE) 13 | else() 14 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE FALSE) 15 | endif() 16 | 17 | # Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec nulla vitae mi 18 | # molestie feugiat. Mauris hendrerit lectus in venenatis ultricies. Ut ac arcu vel libero 19 | # elementum cursus. Praesent justo lacus, varius eu tortor quis, maximus elementum justo. 20 | # Nulla accumsan augue urna, elementum condimentum sapien sodales efficitur. Curabitur eu 21 | # volutpat nisl. Phasellus id diam purus. Maecenas sit amet ipsum sapien. 22 | # cmake-check disable 23 | unset(ENABLE_MSVC_SOME_FEATURE_PREPARE CACHE) 24 | unset(ENABLE_MSVC_SOME_FEATURE CACHE) 25 | # cmake-check enable 26 | 27 | function(__create_def_file TARGET EXPORT_EXTERN_C) 28 | message( 29 | STATUS "Enable for ${TARGET} (export extern C: ${EXPORT_EXTERN_C})" 30 | ) 31 | 32 | string(REPLACE "\\" ";" _path "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") 33 | string(REPLACE "/" ";" _path "${_path}") 34 | list(REVERSE _path) 35 | list(GET _path 0 _platform) 36 | 37 | find_package(NameOfATool REQUIRED) 38 | 39 | file(TO_NATIVE_PATH "${NameOfATool_BINARY}" _NameOfATool) 40 | file(TO_NATIVE_PATH "${NameOfATool_DIRECTORY}" _linksecure_dir) 41 | file( 42 | TO_NATIVE_PATH 43 | "${CMAKE_HOME_DIRECTORY}${_platform}/SOME_FEATURE" 44 | _apicompliancefiledir 45 | ) 46 | file( 47 | TO_NATIVE_PATH 48 | "${_apicompliancefiledir}/${TARGET}.def" 49 | _apicompliancefile 50 | ) 51 | 52 | if(${EXPORT_EXTERN_C}) 53 | set(_export_extern_c_option "/ExportCFunctions") 54 | endif() 55 | 56 | add_custom_command( 57 | TARGET ${TARGET} 58 | POST_BUILD 59 | VERBATIM 60 | COMMAND ${CMAKE_COMMAND} 61 | ARGS -E echo "Remove $/${TARGET}.def" 62 | COMMAND ${CMAKE_COMMAND} -E remove -f $/${TARGET}.def 63 | COMMAND ${CMAKE_COMMAND} -E echo "Create $/${TARGET}.def" 64 | COMMAND ${_NameOfATool} $ "$/${TARGET}.def" ${_linksecure_dir} /APICompatibility ${_apicompliancefile} ${_export_extern_c_option} 65 | ) 66 | endfunction() 67 | 68 | function(__target_link_with_def_file TARGET) 69 | message(STATUS "Enable re-linking of ${TARGET}") 70 | 71 | # this custom target is needed only to be able to add a dependency. 72 | # CMake cannot add dependencies to files, only to targets. 73 | add_custom_target(${TARGET}-deffile 74 | DEPENDS $/${TARGET}.def 75 | ) 76 | add_dependencies(${TARGET} ${TARGET}-deffile) 77 | 78 | target_link_options(${TARGET} 79 | PRIVATE 80 | $<$:/DEF:$/${TARGET}.def> 81 | 82 | # ignore: export 'exportname' specified multiple times; using first specification 83 | $<$:/IGNORE:4197> 84 | ) 85 | endfunction() 86 | 87 | function(target_msvc_SOME_FEATURE_support TARGET) 88 | get_property(_msvc_SOME_FEATURE 89 | GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE 90 | ) 91 | get_property(_msvc_SOME_FEATURE_prepare 92 | GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE_PREPARE 93 | ) 94 | 95 | # optional arguments 96 | set(_export_extern_c FALSE) 97 | foreach(arg ${ARGN}) 98 | if("${arg}" STREQUAL "EXPORT_EXTERN_C_FUNCTIONS") 99 | set(_export_extern_c TRUE) 100 | endif() 101 | endforeach() 102 | 103 | if(MSVC AND ${BUILD_SHARED_LIBS} AND _msvc_SOME_FEATURE_prepare) 104 | __create_def_file(${TARGET} ${_export_extern_c}) 105 | endif() 106 | 107 | if(MSVC AND ${BUILD_SHARED_LIBS} AND _msvc_SOME_FEATURE) 108 | __target_link_with_def_file(${TARGET}) 109 | endif() 110 | endfunction() 111 | 112 | -------------------------------------------------------------------------------- /tests/integration/approved_files/TestFileFormatting.test_real_implementation_of_feature_in_cmake_split_keywords_and_values.approved.txt: -------------------------------------------------------------------------------- 1 | if(ENABLE_MSVC_SOME_FEATURE_PREPARE) 2 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE TRUE) 3 | else() 4 | set_property( 5 | GLOBAL PROPERTY 6 | CONFIGURATION_MSVC_SOME_FEATURE_PREPARE 7 | FALSE) 8 | endif() 9 | 10 | option(ENABLE_MSVC_SOME_FEATURE "Some Text written here" OFF) 11 | if(ENABLE_MSVC_SOME_FEATURE) 12 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE TRUE) 13 | else() 14 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE FALSE) 15 | endif() 16 | 17 | # Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec nulla vitae mi 18 | # molestie feugiat. Mauris hendrerit lectus in venenatis ultricies. Ut ac arcu vel libero 19 | # elementum cursus. Praesent justo lacus, varius eu tortor quis, maximus elementum justo. 20 | # Nulla accumsan augue urna, elementum condimentum sapien sodales efficitur. Curabitur eu 21 | # volutpat nisl. Phasellus id diam purus. Maecenas sit amet ipsum sapien. 22 | # cmake-check disable 23 | unset(ENABLE_MSVC_SOME_FEATURE_PREPARE CACHE) 24 | unset(ENABLE_MSVC_SOME_FEATURE CACHE) 25 | # cmake-check enable 26 | 27 | function(__create_def_file TARGET EXPORT_EXTERN_C) 28 | message( 29 | STATUS 30 | "Enable for ${TARGET} (export extern C: ${EXPORT_EXTERN_C})") 31 | 32 | string(REPLACE "\\" ";" _path "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") 33 | string(REPLACE "/" ";" _path "${_path}") 34 | list(REVERSE _path) 35 | list(GET _path 0 _platform) 36 | 37 | find_package(NameOfATool REQUIRED) 38 | 39 | file(TO_NATIVE_PATH "${NameOfATool_BINARY}" _NameOfATool) 40 | file(TO_NATIVE_PATH "${NameOfATool_DIRECTORY}" _linksecure_dir) 41 | file( 42 | TO_NATIVE_PATH 43 | "${CMAKE_HOME_DIRECTORY}${_platform}/SOME_FEATURE" 44 | _apicompliancefiledir) 45 | file( 46 | TO_NATIVE_PATH 47 | "${_apicompliancefiledir}/${TARGET}.def" 48 | _apicompliancefile) 49 | 50 | if(${EXPORT_EXTERN_C}) 51 | set(_export_extern_c_option "/ExportCFunctions") 52 | endif() 53 | 54 | add_custom_command( 55 | TARGET 56 | ${TARGET} 57 | POST_BUILD 58 | VERBATIM 59 | COMMAND 60 | ${CMAKE_COMMAND} 61 | ARGS 62 | -E echo "Remove $/${TARGET}.def" 63 | COMMAND 64 | ${CMAKE_COMMAND} -E remove -f $/${TARGET}.def 65 | COMMAND 66 | ${CMAKE_COMMAND} -E echo "Create $/${TARGET}.def" 67 | COMMAND 68 | ${_NameOfATool} $ "$/${TARGET}.def" ${_linksecure_dir} /APICompatibility ${_apicompliancefile} ${_export_extern_c_option} 69 | ) 70 | endfunction() 71 | 72 | function(__target_link_with_def_file TARGET) 73 | message(STATUS "Enable re-linking of ${TARGET}") 74 | 75 | # this custom target is needed only to be able to add a dependency. 76 | # CMake cannot add dependencies to files, only to targets. 77 | add_custom_target(${TARGET}-deffile 78 | DEPENDS 79 | $/${TARGET}.def 80 | ) 81 | add_dependencies(${TARGET} ${TARGET}-deffile) 82 | 83 | target_link_options(${TARGET} 84 | PRIVATE 85 | $<$:/DEF:$/${TARGET}.def> 86 | 87 | # ignore: export 'exportname' specified multiple times; using first specification 88 | $<$:/IGNORE:4197> 89 | ) 90 | endfunction() 91 | 92 | function(target_msvc_SOME_FEATURE_support TARGET) 93 | get_property(_msvc_SOME_FEATURE 94 | GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE) 95 | get_property(_msvc_SOME_FEATURE_prepare 96 | GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE_PREPARE) 97 | 98 | # optional arguments 99 | set(_export_extern_c FALSE) 100 | foreach(arg ${ARGN}) 101 | if("${arg}" STREQUAL "EXPORT_EXTERN_C_FUNCTIONS") 102 | set(_export_extern_c TRUE) 103 | endif() 104 | endforeach() 105 | 106 | if(MSVC AND ${BUILD_SHARED_LIBS} AND _msvc_SOME_FEATURE_prepare) 107 | __create_def_file(${TARGET} ${_export_extern_c}) 108 | endif() 109 | 110 | if(MSVC AND ${BUILD_SHARED_LIBS} AND _msvc_SOME_FEATURE) 111 | __target_link_with_def_file(${TARGET}) 112 | endif() 113 | endfunction() 114 | 115 | -------------------------------------------------------------------------------- /tests/integration/input_files/arguments.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set([==[ currently a weird bracket argument introduced 10 | some 2839697%%*^$& text ]===] fake close and stuff]==] some 11 | other 12 | [===[www]===] 13 | [======[this 14 | should 15 | be 16 | indented differently 17 | ]======] 18 | "quoted argument with \" escaped quote") 19 | 20 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 21 | 22 | add_subdirectory(sum_of_non_adjacent) 23 | add_subdirectory(record_last_n_logs) 24 | add_subdirectory(max_values_subarrays) 25 | add_subdirectory(string_distance) 26 | -------------------------------------------------------------------------------- /tests/integration/input_files/comments.cmake: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # a very long comment with some text and stuff 5 | # at the beginning of the file - absolutely no need for reading this text - everyone ignores comments either way so please 6 | # do it also in this particular case 7 | if(NOT Value) 8 | # here the comment should be indented 9 | if(GoogleTest_FOUND AND NOT (TARGET GoogleTest::GoogleTest)) 10 | set(SOME_VALUE ON) 11 | 12 | install(TARGETS mylibapp 13 | RUNTIME 14 | # TODO: there is some very important information 15 | # why this silly line of code beneath is really here 16 | DESTINATION bin 17 | COMPONENT applications) 18 | install(FILES "${PYLON_BINARY_DIRS}/PylonUsb_MD_VC120_V5_0_TL.dll" DESTINATION ${CMAKE_INSTALL_BINARY} 19 | CONFIGURATIONS 20 | # here we should have other identation also with single line 21 | Release 22 | RelWithDebInfo) 23 | 24 | 25 | elseif(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" 26 | OR CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019") 27 | install( 28 | # do we handle arguments correctly? 29 | # are you certain? 30 | EXPORT graphicsmagick-targets 31 | FILE unofficial-graphicsmagick-targets.cmake # standard line-comment 32 | NAMESPACE unofficial::graphicsmagick:: 33 | DESTINATION share/unofficial-graphicsmagick 34 | # we can also check what happens here assuming someone commented out single line 35 | ) 36 | 37 | target_link_options(${PROJECT_NAME} 38 | PRIVATE 39 | $<$:/WX:NO> # FIXME: and text 40 | ) 41 | 42 | target_compile_options(${PROJECT_NAME} 43 | PRIVATE 44 | $<$:/wd4251> # warning about DLL interface 45 | $<$,$,$>,$>:/QRfpe-> 46 | $<$:-w> # TODO: suppress all GCC warnings 47 | $<$:-Wno-everything> # TODO: suppress all Clang warnings 48 | ) 49 | endif() 50 | endif() 51 | 52 | -------------------------------------------------------------------------------- /tests/integration/input_files/complicated_conditions.cmake: -------------------------------------------------------------------------------- 1 | 2 | if(NOT Value) 3 | if(GoogleTest_FOUND AND NOT (TARGET GoogleTest::GoogleTest)) 4 | set(SOME_VALUE ON) 5 | elseif(CMAKE_GENERATOR STREQUAL "Visual Studio 15 2017" OR CMAKE_GENERATOR STREQUAL "Visual Studio 16 2019" 6 | ) 7 | set(SOME_VALUE ON) 8 | if(EXISTS ${CMAKE_HOME_DIRECTORY}/ABC/${GTest_DIR}/${GTest_COMPONENT}/source ) 9 | set(SOME_VALUE ON) 10 | endif() 11 | if(BUILD_SHARED_LIBS AND ((${CMAKE_SYSTEM_NAME} STREQUAL Windows) OR (${CMAKE_SYSTEM_NAME} STREQUAL WindowsCE 12 | AND NOT ${CMAKE_SYSTEM_VERSION} VERSION_EQUAL 6.00))) 13 | set(SOME_VALUE ON) 14 | if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU") AND ((${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9") OR (CMAKE_SIZE_OF_VOID_P EQUAL 4) OR (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm"))) # TODO: implict definition of A_PLATFORM 15 | set(SOME_VALUE ON) 16 | 17 | endif() 18 | endif() 19 | 20 | endif() 21 | endif() 22 | -------------------------------------------------------------------------------- /tests/integration/input_files/conditions_with_parentheses.cmake: -------------------------------------------------------------------------------- 1 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" AND 2 | (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9" 3 | OR (CMAKE_SIZE_OF_VOID_P EQUAL 4 OR 4 | (${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm")))) # TODO: implict definition of A_PLATFORM 5 | endif() 6 | 7 | 8 | if((((${CMAKE_CXX_COMPILER_ID}) STREQUAL "GNU" AND 9 | ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9") 10 | OR CMAKE_SIZE_OF_VOID_P EQUAL 4) OR 11 | ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm") # TODO: implict definition of A_PLATFORM 12 | endif() 13 | 14 | if(${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" AND 15 | (${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9" OR CMAKE_SIZE_OF_VOID_P EQUAL 4) OR 16 | ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm") 17 | endif() -------------------------------------------------------------------------------- /tests/integration/input_files/first_example.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | add_subdirectory(sum_of_non_adjacent) 13 | add_subdirectory(record_last_n_logs) 14 | add_subdirectory(max_values_subarrays) 15 | add_subdirectory(string_distance) 16 | -------------------------------------------------------------------------------- /tests/integration/input_files/incorrect_file.cmake: -------------------------------------------------------------------------------- 1 | install(TARGETS gtest gtest_main 2 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 3 | ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 4 | LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}")) 5 | 6 | install(TARGETS mylibapp 7 | RUNTIME 8 | DESTINATION bin 9 | COMPONENT applications) 10 | install(FILES "${PYLON_BINARY_DIRS}/PylonUsb_MD_VC120_V5_0_TL.dll" DESTINATION ${CMAKE_INSTALL_BINARY} CONFIGURATIONS Release RelWithDebInfo) 11 | -------------------------------------------------------------------------------- /tests/integration/input_files/indentations.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | 4 | 5 | function(doubleEach) 6 | foreach(ARG ${ARGN}) # Iterate over each argument 7 | if(${ARG} MATCHES "\w+" "") 8 | math(EXPR N "${ARG} * 2") 9 | else() 10 | message("${N}") # Print N 11 | endif() 12 | endforeach() 13 | endfunction() 14 | 15 | -------------------------------------------------------------------------------- /tests/integration/input_files/install.cmake: -------------------------------------------------------------------------------- 1 | 2 | install(TARGETS gtest gtest_main 3 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 4 | ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 5 | LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") 6 | 7 | 8 | install(TARGETS mylibapp 9 | RUNTIME 10 | DESTINATION bin 11 | COMPONENT applications) 12 | install(FILES "${PYLON_BINARY_DIRS}/PylonUsb_MD_VC120_V5_0_TL.dll" DESTINATION ${CMAKE_INSTALL_BINARY} CONFIGURATIONS Release RelWithDebInfo) 13 | 14 | 15 | install( 16 | EXPORT graphicsmagick-targets 17 | FILE unofficial-graphicsmagick-targets.cmake 18 | NAMESPACE unofficial::graphicsmagick:: 19 | DESTINATION share/unofficial-graphicsmagick 20 | ) 21 | 22 | 23 | install(FILES $.genex 24 | DESTINATION $<1:lib>$<0:/wrong> 25 | ) 26 | 27 | 28 | install( 29 | TARGETS tinyfiledialogs 30 | EXPORT tinyfiledialogsConfig 31 | ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" 32 | LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" 33 | RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" 34 | INCLUDES DESTINATION "${CMAKE_INSTALL_BINDIR}" 35 | FILES_MATCHING 36 | PATTERN ".*" 37 | FILES_MATCHING EXCLUDE abc.cpp 38 | ) 39 | install( 40 | FILES tinyfiledialogs.h 41 | DESTINATION "${CMAKE_INSTALL_PREFIX}/include/tinyfiledialogs" 42 | ) 43 | 44 | export( 45 | TARGETS tinyfiledialogs 46 | NAMESPACE tinyfiledialogs:: 47 | FILE "${CMAKE_CURRENT_BINARY_DIR}/tinyfiledialogsConfig.cmake" 48 | ) 49 | 50 | install( 51 | EXPORT tinyfiledialogsConfig 52 | NAMESPACE tinyfiledialogs:: 53 | DESTINATION "${CMAKE_INSTALL_PREFIX}/share/tinyfiledialogs" 54 | ) 55 | -------------------------------------------------------------------------------- /tests/integration/input_files/line_length_handling.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | foreach(ARG ${ARGN}) # Iterate over each argument 8 | if(${ARG} (MATCHES ("\w+"))) 9 | math(EXPR N "${ARG} * 2") 10 | else() 11 | set(a_very_long_invocation_that should_be_just_out the_border_of__80 chars) 12 | set(a_very_long_invocation_that should_be_just_out the_border_of__80 chars_clearly_so) 13 | set(a very long invocation that is just below the limit of chars supp) 14 | message("${N}") # Print N 15 | this(should 16 | be 17 | wrapped 18 | "adewa") 19 | endif() 20 | endforeach() 21 | 22 | foreach(X aaaa bbbb cccc dddd eeee ffff gggg hhhh jjjj iiii kkkk llll mmmm nnnn oooo pppp rrrr) 23 | message("${X}") 24 | endforeach() 25 | 26 | foreach(X IN LISTS aaaa bbbb cccc dddd eeee ffff gggg hhhh jjjj iiii kkkk llll mmmm nnnn oooo pppp rrrr) 27 | message("${X}") 28 | endforeach() 29 | 30 | foreach(loop_count RANGE ${very_long_start_name} ${even_longer_finish_name}) 31 | message("${X}") 32 | endforeach() 33 | 34 | foreach(first_name second_name IN ZIP_LISTS long_list_of_first_names long_list_of_second_names) 35 | message(STATUS "en=${en}, ba=${ba}") 36 | endforeach() 37 | -------------------------------------------------------------------------------- /tests/integration/input_files/newlines_violations.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | 4 | 5 | 6 | include(CTest) 7 | 8 | 9 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 10 | 11 | find_package(GTest REQUIRED) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | add_subdirectory(string_distance) 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/integration/input_files/set_of_functions.cmake: -------------------------------------------------------------------------------- 1 | if(ENABLE_MSVC_SOME_FEATURE_PREPARE) 2 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE TRUE) 3 | else() 4 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE_PREPARE FALSE) 5 | endif() 6 | 7 | option(ENABLE_MSVC_SOME_FEATURE "Some Text written here" OFF) 8 | if(ENABLE_MSVC_SOME_FEATURE) 9 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE TRUE) 10 | else() 11 | set_property(GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE FALSE) 12 | endif() 13 | 14 | # Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec nec nulla vitae mi 15 | # molestie feugiat. Mauris hendrerit lectus in venenatis ultricies. Ut ac arcu vel libero 16 | # elementum cursus. Praesent justo lacus, varius eu tortor quis, maximus elementum justo. 17 | # Nulla accumsan augue urna, elementum condimentum sapien sodales efficitur. Curabitur eu 18 | # volutpat nisl. Phasellus id diam purus. Maecenas sit amet ipsum sapien. 19 | # cmake-check disable 20 | unset(ENABLE_MSVC_SOME_FEATURE_PREPARE CACHE) 21 | unset(ENABLE_MSVC_SOME_FEATURE CACHE) 22 | # cmake-check enable 23 | 24 | function(__create_def_file TARGET EXPORT_EXTERN_C) 25 | message(STATUS "Enable for ${TARGET} (export extern C: ${EXPORT_EXTERN_C})") 26 | 27 | string(REPLACE "\\" ";" _path "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") 28 | string(REPLACE "/" ";" _path "${_path}") 29 | list(REVERSE _path) 30 | list(GET _path 0 _platform) 31 | 32 | find_package(NameOfATool REQUIRED) 33 | 34 | file(TO_NATIVE_PATH "${NameOfATool_BINARY}" _NameOfATool) 35 | file(TO_NATIVE_PATH "${NameOfATool_DIRECTORY}" _linksecure_dir) 36 | file(TO_NATIVE_PATH "${CMAKE_HOME_DIRECTORY}${_platform}/SOME_FEATURE" _apicompliancefiledir) 37 | file(TO_NATIVE_PATH "${_apicompliancefiledir}/${TARGET}.def" _apicompliancefile) 38 | 39 | if(${EXPORT_EXTERN_C}) 40 | set(_export_extern_c_option "/ExportCFunctions") 41 | endif() 42 | 43 | add_custom_command(TARGET ${TARGET} POST_BUILD 44 | VERBATIM 45 | COMMAND ${CMAKE_COMMAND} ARGS -E echo "Remove $/${TARGET}.def" 46 | COMMAND ${CMAKE_COMMAND} -E remove -f $/${TARGET}.def 47 | COMMAND ${CMAKE_COMMAND} -E echo "Create $/${TARGET}.def" 48 | COMMAND ${_NameOfATool} $ "$/${TARGET}.def" ${_linksecure_dir} /APICompatibility ${_apicompliancefile} ${_export_extern_c_option} 49 | ) 50 | endfunction() 51 | 52 | 53 | function(__target_link_with_def_file TARGET) 54 | message(STATUS "Enable re-linking of ${TARGET}") 55 | 56 | # this custom target is needed only to be able to add a dependency. 57 | # CMake cannot add dependencies to files, only to targets. 58 | add_custom_target(${TARGET}-deffile 59 | DEPENDS $/${TARGET}.def 60 | ) 61 | add_dependencies(${TARGET} ${TARGET}-deffile) 62 | 63 | target_link_options(${TARGET} 64 | PRIVATE 65 | $<$:/DEF:$/${TARGET}.def> 66 | 67 | # ignore: export 'exportname' specified multiple times; using first specification 68 | $<$:/IGNORE:4197> 69 | ) 70 | endfunction() 71 | 72 | 73 | function(target_msvc_SOME_FEATURE_support TARGET) 74 | get_property(_msvc_SOME_FEATURE 75 | GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE) 76 | get_property(_msvc_SOME_FEATURE_prepare 77 | GLOBAL PROPERTY CONFIGURATION_MSVC_SOME_FEATURE_PREPARE) 78 | 79 | # optional arguments 80 | set(_export_extern_c FALSE) 81 | foreach(arg ${ARGN}) 82 | if("${arg}" STREQUAL "EXPORT_EXTERN_C_FUNCTIONS") 83 | set(_export_extern_c TRUE) 84 | endif() 85 | endforeach() 86 | 87 | if(MSVC AND ${BUILD_SHARED_LIBS} AND _msvc_SOME_FEATURE_prepare) 88 | __create_def_file(${TARGET} ${_export_extern_c}) 89 | endif() 90 | 91 | if(MSVC AND ${BUILD_SHARED_LIBS} AND _msvc_SOME_FEATURE) 92 | __target_link_with_def_file(${TARGET}) 93 | endif() 94 | endfunction() 95 | -------------------------------------------------------------------------------- /tests/integration/input_files/spaces_violations.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | include(CTest) 4 | include(${CMAKE_BINARY_DIR}/conan_paths.cmake) 5 | 6 | find_package(GTest REQUIRED) 7 | 8 | # Here we have a line comment with weird stuff like #[===]] $#%!#@$!#@%^^%$&% 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | foreach(ARG ${ARGN}) # Iterate over each argument 13 | if(${ARG} (MATCHES ("\w+"))) 14 | math(EXPR N "${ARG} * 2") 15 | else() 16 | message("${N}") # Print N 17 | endif() 18 | endforeach() 19 | 20 | add_subdirectory( sum_of_non_adjacent ) 21 | add_subdirectory( record_last n_logs ) 22 | add_subdirectory(max_values_subarrays ) 23 | add_subdirectory(string_distance) 24 | add_subdirectory(new_dir) 25 | -------------------------------------------------------------------------------- /tests/integration/input_files/target_setting.cmake: -------------------------------------------------------------------------------- 1 | ################################################ 2 | # BLABLA 3 | ################################################ 4 | 5 | cmake_minimum_required(VERSION 3.13 FATAL_ERROR) 6 | project(A_PROJECT) 7 | 8 | find_package(Library REQUIRED) 9 | 10 | add_library(${PROJECT_NAME}) 11 | add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 12 | 13 | set_target_properties(${PROJECT_NAME} 14 | PROPERTIES 15 | FOLDER Components 16 | ) 17 | 18 | target_sources(${PROJECT_NAME} 19 | PRIVATE 20 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 21 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 22 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 23 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 24 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 25 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 26 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 27 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 28 | ) 29 | 30 | if(${CMAKE_SYSTEM_NAME} STREQUAL Windows OR ${CMAKE_SYSTEM_NAME} STREQUAL Linux) 31 | target_sources(${PROJECT_NAME} 32 | PRIVATE 33 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 34 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 35 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 36 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 37 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 38 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 39 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 40 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 41 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 42 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 43 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 44 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 45 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 46 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 47 | ) 48 | endif() 49 | 50 | set_property( 51 | SOURCE 52 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 53 | PROPERTY COMPILE_FLAGS 54 | $<$:/Y-> # ignore precompiled headers 55 | ) 56 | 57 | target_link_libraries(${PROJECT_NAME} 58 | PUBLIC 59 | boost::boost 60 | PRIVATE 61 | $<$:pthread> 62 | ) 63 | 64 | target_include_directories(${PROJECT_NAME} 65 | PUBLIC 66 | $ 67 | PRIVATE 68 | $ 69 | ) 70 | 71 | include(CMakePackage OPTIONAL) 72 | if(COMMAND target_set) 73 | target_set(${PROJECT_NAME} 74 | GROUP 75 | File.cpp 76 | DIRECTORY 77 | ${CMAKE_CURRENT_SOURCE_DIR}/Source/File.cpp 78 | ) 79 | endif() 80 | 81 | ################################################ 82 | # BLABLA 83 | ################################################ 84 | add_custom_command( 85 | TARGET 86 | ${_TARGET} 87 | POST_BUILD 88 | ) -------------------------------------------------------------------------------- /tests/integration/test_cmake_tidy.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from unittest import mock 8 | 9 | from approvaltests.approvals import verify 10 | from io import StringIO 11 | 12 | from tests.integration.test_integration_base import TestIntegrationBase 13 | from tests.integration.utils import execute_cmake_tidy, normalize 14 | 15 | 16 | class TestCMakeTidy(TestIntegrationBase): 17 | @mock.patch('sys.stderr', new_callable=StringIO) 18 | def test_incorrect_command_should_print_error_with_usage_help(self, stderr): 19 | self.assertFail(execute_cmake_tidy(command='invalid', arguments=[])) 20 | normalized_output = normalize(stderr.getvalue()) 21 | verify(normalized_output, self.reporter) 22 | 23 | @mock.patch('sys.stdout', new_callable=StringIO) 24 | def test_version_argument_should_provide_correct_tool_version(self, stdout): 25 | self.assertSuccess(execute_cmake_tidy(command=None, arguments=['-v'])) 26 | normalized_output = normalize(stdout.getvalue()) 27 | verify(normalized_output, self.reporter) 28 | -------------------------------------------------------------------------------- /tests/integration/test_cmake_tidy_analyze.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from unittest import mock 8 | 9 | from approvaltests.approvals import verify 10 | from io import StringIO 11 | 12 | from tests.integration.test_integration_base import TestIntegrationBase 13 | from tests.integration.utils import execute_cmake_tidy, normalize, mangle_version 14 | 15 | 16 | class TestCMakeTidyAnalyze(TestIntegrationBase): 17 | @mock.patch('sys.stdout', new_callable=StringIO) 18 | def test_analyze_command_help_shown(self, stdout): 19 | self.assertSuccess(execute_cmake_tidy(command='analyze', arguments=['--help'])) 20 | normalized_output = normalize(stdout.getvalue()) 21 | verify(normalized_output, self.reporter) 22 | 23 | @mock.patch('sys.stdout', new_callable=StringIO) 24 | def test_analyze_should_correctly_print_version(self, stdout): 25 | self.assertSuccess(execute_cmake_tidy(None, arguments=['-v', 'analyze'])) 26 | verify(mangle_version(stdout.getvalue()), self.reporter) 27 | -------------------------------------------------------------------------------- /tests/integration/test_cmake_tidy_format.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | from unittest import mock 9 | 10 | from approvaltests.approvals import verify 11 | from io import StringIO 12 | 13 | from tests.integration.test_integration_base import TestIntegrationBase 14 | from tests.integration.utils import execute_cmake_tidy, normalize, get_input_file, mangle_version 15 | 16 | 17 | class TestCMakeTidyFormat(TestIntegrationBase): 18 | @mock.patch('sys.stdout', new_callable=StringIO) 19 | def test_format_command_help_shown(self, stdout): 20 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--help'])) 21 | normalized_output = normalize(stdout.getvalue()) 22 | verify(normalized_output, self.reporter) 23 | 24 | @mock.patch('sys.stderr', new_callable=StringIO) 25 | def test_incorrect_command_should_print_error_with_usage_help(self, stdout): 26 | self.assertFail(execute_cmake_tidy(command='format', arguments=[])) 27 | normalized_output = normalize(stdout.getvalue()) 28 | verify(normalized_output, self.reporter) 29 | 30 | @mock.patch('sys.stdout', new_callable=StringIO) 31 | def test_format_should_dump_config_only_configuration_to_stdout_by_default(self, stdout): 32 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config', 'dummy.txt'])) 33 | normalized_output = normalize(stdout.getvalue()) 34 | verify(normalized_output, self.reporter) 35 | 36 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_settings', 37 | mock.MagicMock(return_value={'keywords': ['CUSTOM_KEYWORD']})) 38 | @mock.patch('sys.stdout', new_callable=StringIO) 39 | def test_format_should_dump_full_config_even_if_file_overrides_only_one(self, stdout): 40 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config', 'file.txt'])) 41 | normalized_output = normalize(stdout.getvalue()) 42 | verify(normalized_output, self.reporter) 43 | 44 | @mock.patch('cmake_tidy.commands.format.output_writer.write_to_file') 45 | @mock.patch('sys.stdout', new_callable=StringIO) 46 | def test_format_inplace_simple_file_with_verbose_option(self, stdout, write): 47 | self.assertSuccess(execute_cmake_tidy(command='format', 48 | arguments=['-i', '--verbose', get_input_file('arguments.cmake')])) 49 | write.assert_called_once() 50 | self.assertIn(get_input_file('arguments.cmake'), stdout.getvalue()) 51 | normalized_output = normalize(write.call_args[0][1]) 52 | verify(normalized_output, self.reporter) 53 | 54 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_settings', 55 | mock.MagicMock(return_value={'tabs_as_spaces': 33})) 56 | @mock.patch('sys.stderr', new_callable=StringIO) 57 | def test_format_should_fail_with_warning_about_incorrect_settings_when_dump_invoked(self, stdout): 58 | self.assertFail(execute_cmake_tidy(command='format', arguments=['--dump-config', 'file.txt'])) 59 | normalized_output = normalize(stdout.getvalue()) 60 | verify(normalized_output, self.reporter) 61 | 62 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_settings', 63 | mock.MagicMock(return_value={'tabs_as_spaces': 33})) 64 | @mock.patch('sys.stderr', new_callable=StringIO) 65 | def test_format_should_fail_with_warning_about_incorrect_settings_when_trying_to_format(self, stdout): 66 | self.assertFail(execute_cmake_tidy(command='format', arguments=[get_input_file('arguments.cmake')])) 67 | normalized_output = normalize(stdout.getvalue()) 68 | verify(normalized_output, self.reporter) 69 | 70 | @mock.patch('sys.stderr', new_callable=StringIO) 71 | @mock.patch('cmake_tidy.commands.format.output_writer.write_to_file') 72 | def test_format_inplace_with_error_should_inform_about_failure_and_keep_initial_file(self, write, stderr): 73 | self.assertFail(execute_cmake_tidy(command='format', arguments=['-i', get_input_file('incorrect_file.cmake')])) 74 | write.assert_not_called() 75 | normalized_output = normalize(stderr.getvalue()) 76 | verify(normalized_output, self.reporter) 77 | 78 | @mock.patch('sys.stderr', new_callable=StringIO) 79 | @mock.patch('cmake_tidy.commands.format.output_writer.write_to_file', 80 | mock.MagicMock(side_effect=PermissionError)) 81 | def test_format_should_return_error_when_file_is_read_only_and_inplace_param_is_used(self, stderr): 82 | self.assertFail(execute_cmake_tidy(command='format', arguments=['-i', get_input_file('arguments.cmake')])) 83 | normalized_output = normalize(stderr.getvalue()) 84 | normalized_output = re.sub(r'File .*arguments', 'File arguments', normalized_output) 85 | verify(normalized_output, self.reporter) 86 | 87 | @mock.patch('sys.stdout', new_callable=StringIO) 88 | def test_format_should_provide_unified_diff_to_stdout(self, stdout): 89 | self.assertSuccess(execute_cmake_tidy(command='format', 90 | arguments=['--diff', get_input_file('arguments.cmake')])) 91 | normalized_output = normalize(stdout.getvalue()) 92 | normalized_output = self.__replace_with_fake_path('arguments.cmake', normalized_output) 93 | verify(normalized_output, self.reporter) 94 | 95 | @mock.patch('sys.stdout', new_callable=StringIO) 96 | def test_format_should_correctly_print_version(self, stdout): 97 | self.assertSuccess(execute_cmake_tidy(None, arguments=['-v', 'format'])) 98 | verify(mangle_version(stdout.getvalue()), self.reporter) 99 | 100 | @mock.patch('cmake_tidy.commands.format.output_writer.write_to_file') 101 | @mock.patch('sys.stdout', new_callable=StringIO) 102 | def test_format_multiple_files_verbose(self, stdout, write): 103 | arguments = ['-i', '--verbose', get_input_file('arguments.cmake'), get_input_file('comments.cmake')] 104 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=arguments)) 105 | self.assertEqual(2, write.call_count) 106 | self.assertIn(get_input_file('arguments.cmake'), stdout.getvalue()) 107 | self.assertIn(get_input_file('comments.cmake'), stdout.getvalue()) 108 | 109 | @staticmethod 110 | def __replace_with_fake_path(filename: str, text: str) -> str: 111 | return re.sub(r' .*' + filename, ' /' + filename, text) 112 | -------------------------------------------------------------------------------- /tests/integration/test_cmake_tidy_format_discover_config_file.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import json 8 | import tempfile 9 | from pathlib import Path 10 | from unittest import mock 11 | 12 | from io import StringIO 13 | 14 | from cmake_tidy.formatting import SettingsReader 15 | from tests.integration.test_integration_base import TestIntegrationBase 16 | from tests.integration.utils import execute_cmake_tidy 17 | 18 | 19 | class TestCMakeTidyFormatDiscoverConfigFile(TestIntegrationBase): 20 | def setUp(self): 21 | super(TestCMakeTidyFormatDiscoverConfigFile, self).setUp() 22 | self.default_settings = SettingsReader.get_default_format_settings() 23 | self.temp_directory = tempfile.TemporaryDirectory() 24 | 25 | def tearDown(self) -> None: 26 | self.temp_directory.cleanup() 27 | 28 | def create_config_file(self, directory: Path, data: str): 29 | config_file = directory 30 | config_file.mkdir(parents=True, exist_ok=False) 31 | config_file = config_file / '.cmake-tidy.json' 32 | with config_file.open('w') as f: 33 | f.write(data) 34 | 35 | @mock.patch('sys.stdout', new_callable=StringIO) 36 | def test_format_should_dump_default_config_when_no_config_available_in_cwd(self, stdout): 37 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config'])) 38 | self.assertDictEqual(self.default_settings, json.loads(stdout.getvalue())) 39 | 40 | @mock.patch('sys.stdout', new_callable=StringIO) 41 | def test_format_should_dump_config_from_current_cwd(self, stdout): 42 | fake_path = Path(self.temp_directory.name) / 'abc' 43 | self.create_config_file(fake_path, '{"line_length": 20}') 44 | 45 | with mock.patch('pathlib.Path.cwd', return_value=fake_path): 46 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config'])) 47 | received_settings = json.loads(stdout.getvalue()) 48 | self.assertFalse(self.default_settings == received_settings) 49 | self.assertEqual(20, received_settings.get('line_length')) 50 | 51 | @mock.patch('sys.stdout', new_callable=StringIO) 52 | def test_format_should_dump_config_from_input_file_location_as_config_is_there(self, stdout): 53 | fake_path = Path(self.temp_directory.name) / 'some_dir' 54 | self.create_config_file(fake_path, '{"line_length": 40}') 55 | input_filename = str((fake_path / 'test.cmake').absolute()) 56 | 57 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config', input_filename])) 58 | received_settings = json.loads(stdout.getvalue()) 59 | self.assertFalse(self.default_settings == received_settings) 60 | self.assertEqual(40, received_settings.get('line_length')) 61 | 62 | @mock.patch('sys.stdout', new_callable=StringIO) 63 | def test_format_should_dump_config_from_one_of_input_file_parents_location(self, stdout): 64 | fake_path = Path(self.temp_directory.name) / 'some_dir' 65 | self.create_config_file(fake_path, '{"line_length": 33}') 66 | input_filename = str((fake_path / 'another' / 'test.cmake').absolute()) 67 | 68 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config', input_filename])) 69 | received_settings = json.loads(stdout.getvalue()) 70 | self.assertFalse(self.default_settings == received_settings) 71 | self.assertEqual(33, received_settings.get('line_length')) 72 | 73 | @mock.patch('sys.stdout', new_callable=StringIO) 74 | def test_format_should_dump_config_from_one_of_input_file_parents_location_priority_close_to_file(self, stdout): 75 | fake_path = Path(self.temp_directory.name) / 'some_dir' 76 | self.create_config_file(fake_path, '{"line_length": 33}') 77 | self.create_config_file(fake_path / 'another', '{"line_length": 32}') 78 | input_filename = str((fake_path / 'another' / 'yet_something' / 'test.cmake').absolute()) 79 | 80 | self.assertSuccess(execute_cmake_tidy(command='format', arguments=['--dump-config', input_filename])) 81 | received_settings = json.loads(stdout.getvalue()) 82 | self.assertFalse(self.default_settings == received_settings) 83 | self.assertEqual(32, received_settings.get('line_length')) 84 | -------------------------------------------------------------------------------- /tests/integration/test_integration_base.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from approvaltests.reporters import GenericDiffReporterFactory 10 | 11 | 12 | class TestIntegrationBase(unittest.TestCase): 13 | def setUp(self): 14 | self.reporter = GenericDiffReporterFactory().get_first_working() 15 | 16 | def assertSuccess(self, exit_code): 17 | self.assertEqual(0, exit_code, 'Application exit code different than 0!') 18 | 19 | def assertFail(self, exit_code): 20 | self.assertNotEqual(0, exit_code, 'Application succeeded unexpectedly!') 21 | -------------------------------------------------------------------------------- /tests/integration/utils.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import re 8 | from pathlib import Path 9 | from typing import Optional 10 | 11 | from cmake_tidy.__main__ import main 12 | 13 | 14 | def execute_cmake_tidy(command: Optional[str], arguments: list) -> int: 15 | try: 16 | if command: 17 | main([command] + arguments) 18 | else: 19 | main(arguments) 20 | except SystemExit as system_exited: 21 | return system_exited.code 22 | 23 | 24 | def normalize(data: str) -> str: 25 | return data.replace('\r\n', '\n') 26 | 27 | 28 | def mangle_version(data: str) -> str: 29 | return re.sub(r'[0-9]+', 'X', data) 30 | 31 | 32 | def get_input_file(filename: str) -> str: 33 | return str(Path(__file__).resolve().parent / 'input_files' / filename) 34 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaciejPatro/cmake-tidy/ddab3d9c6dd1a6c9cfa47bff5a9f120defea9e6a/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/parser_composite_elements.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.lexical_data.elements import Element, ComplexElement, PrimitiveElement 8 | 9 | 10 | def spaces(data: str) -> PrimitiveElement: 11 | return PrimitiveElement('spaces', data) 12 | 13 | 14 | def line_ending(comment, newlines_number): 15 | return ComplexElement('line_ending') \ 16 | .add(PrimitiveElement('line_comment', comment)) \ 17 | .add(PrimitiveElement('newlines', newlines_number)) 18 | 19 | 20 | def parentheses(args: Element) -> Element: 21 | return ComplexElement('parentheses') \ 22 | .add(PrimitiveElement('parenthesis_start', '(')) \ 23 | .add(args) \ 24 | .add(PrimitiveElement('parenthesis_end', ')')) 25 | 26 | 27 | def newlines(number: int) -> Element: 28 | return ComplexElement('line_ending').add(PrimitiveElement('newlines', number)) 29 | 30 | 31 | def bracket_argument(bracket_size: int, data: str) -> Element: 32 | bracket_part = '=' * bracket_size 33 | return ComplexElement('bracket_argument') \ 34 | .add(PrimitiveElement('bracket_start', f'[{bracket_part}[')) \ 35 | .add(PrimitiveElement('bracket_argument_content', data)) \ 36 | .add(PrimitiveElement('bracket_end', f']{bracket_part}]')) 37 | 38 | 39 | def quoted_argument(data='') -> PrimitiveElement: 40 | return PrimitiveElement('quoted_argument', data) 41 | 42 | 43 | def unquoted_argument(data='') -> PrimitiveElement: 44 | return PrimitiveElement('unquoted_argument', data) 45 | 46 | 47 | def command_invocation(func_name: str, args=None): 48 | return ComplexElement('command_invocation') \ 49 | .add(start_cmd(func_name)) \ 50 | .add(args) \ 51 | .add(end_cmd()) 52 | 53 | 54 | def file() -> Element: 55 | return ComplexElement('file') 56 | 57 | 58 | def arguments() -> Element: 59 | return ComplexElement('arguments') 60 | 61 | 62 | def start_cmd(name: str) -> PrimitiveElement: 63 | return PrimitiveElement('start_cmd_invoke', name) 64 | 65 | 66 | def end_cmd() -> PrimitiveElement: 67 | return PrimitiveElement('end_cmd_invoke', ')') 68 | 69 | 70 | def unhandled(data: str) -> PrimitiveElement: 71 | return PrimitiveElement('unhandled', data) 72 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_format_dispatcher.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.formatting.cmake_format_dispatcher import CMakeFormatDispatcher 10 | 11 | 12 | class TestCMakeFormatDispatcher(unittest.TestCase): 13 | def setUp(self) -> None: 14 | self.state = {'last': None} 15 | self.dispatcher = CMakeFormatDispatcher(self.state) 16 | 17 | def assertLastStateEqual(self, value): 18 | self.assertEqual(self.state['last'], value) 19 | 20 | def test_state_should_not_be_changed_when_object_created(self): 21 | self.assertLastStateEqual(None) 22 | 23 | def test_dispatched_method_should_remember_last_usage(self): 24 | self.dispatcher['new'] = lambda: 1 25 | self.assertEqual(1, self.dispatcher['new']()) 26 | self.assertLastStateEqual('new') 27 | 28 | def test_ensure_that_state_changes_at_the_end_of_invocation(self): 29 | self.dispatcher['some'] = lambda: self.state['last'] 30 | self.assertEqual(None, self.dispatcher['some']()) 31 | self.assertLastStateEqual('some') 32 | 33 | def test_dispatcher_should_accept_only_callable_values(self): 34 | with self.assertRaises(TypeError): 35 | self.dispatcher['value'] = 1 36 | 37 | def test_dispatcher_should_raise_key_error_when_no_such_key(self): 38 | with self.assertRaises(KeyError): 39 | self.dispatcher['x'] 40 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.formatting import CMakeFormatter 10 | from cmake_tidy.lexical_data.elements import Element 11 | from tests.unit.parser_composite_elements import spaces, newlines, file, command_invocation 12 | 13 | 14 | class TestCMakeFormatter(unittest.TestCase): 15 | def setUp(self) -> None: 16 | self.settings = {'succeeding_newlines': 1, 17 | 'tab_size': 2, 18 | 'force_command_lowercase': True, 19 | 'wrap_short_invocations_to_single_line': False, 20 | 'closing_parentheses_in_newline_when_split': False, 21 | 'line_length': 80, 22 | 'keep_property_and_value_in_one_line': False, 23 | 'keyword_and_single_value_in_one_line': False, 24 | 'tabs_as_spaces': True} 25 | 26 | def assertFormatting(self, formatted_string, lex_data): 27 | self.assertEqual(formatted_string, CMakeFormatter(self.settings).format(lex_data)) 28 | 29 | def assertFormattingArguments(self, expected_formatting, function_arguments): 30 | self.assertFormatting(expected_formatting, file().add(command_invocation('abc(', function_arguments))) 31 | 32 | def assertConditionFormatting(self, expected: str, args: Element): 33 | root = file().add(command_invocation('if(', args)) 34 | self.assertFormatting(expected, root) 35 | 36 | 37 | class TestCMakeFormatterBasicElements(TestCMakeFormatter): 38 | def test_return_single_newline(self): 39 | self.assertFormatting('\n', file().add(newlines(3))) 40 | 41 | def test_return_3_newlines_although_settings_allow_more(self): 42 | self.settings['succeeding_newlines'] = 5 43 | self.assertFormatting('\n\n\n', file().add(newlines(3))) 44 | 45 | def test_replace_tab_with_space_one_to_two(self): 46 | self.assertFormatting(' ' * 4, spaces('\t\t')) 47 | 48 | def test_replace_tabs_with_multiple_spaces(self): 49 | self.settings['tab_size'] = 4 50 | self.assertFormatting(' ' * 10, spaces(' \t \t')) 51 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter_comments_within_arguments.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from tests.unit.parser_composite_elements import spaces, file, command_invocation, unquoted_argument, \ 8 | arguments, newlines, line_ending 9 | from tests.unit.test_cmake_formatter import TestCMakeFormatter 10 | 11 | 12 | class TestCMakeFormatterCommandArgumentsWithComments(TestCMakeFormatter): 13 | def test_multiple_line_comments_before_value(self): 14 | args = arguments().add(unquoted_argument('abc')) \ 15 | .add(spaces(' ')) \ 16 | .add(unquoted_argument('TARGET')) \ 17 | .add(newlines(1)) \ 18 | .add(line_ending('# first line', 1)) \ 19 | .add(line_ending('# second line', 1)) \ 20 | .add(unquoted_argument('${PROJECT_NAME}')) \ 21 | .add(newlines(1)) 22 | 23 | root = file().add(command_invocation('add_custom_target(', args)) 24 | 25 | expected_formatting = """add_custom_target(abc 26 | TARGET 27 | # first line 28 | # second line 29 | ${PROJECT_NAME} 30 | )""" 31 | self.assertFormatting(expected_formatting, root) 32 | 33 | def test_multiple_line_comments_between_keywords(self): 34 | args = arguments().add(unquoted_argument('abc')) \ 35 | .add(newlines(1)) \ 36 | .add(unquoted_argument('ALL')) \ 37 | .add(newlines(1)) \ 38 | .add(line_ending('# first line', 1)) \ 39 | .add(line_ending('# second line', 1)) \ 40 | .add(unquoted_argument('TARGET')) \ 41 | .add(spaces(' ')) \ 42 | .add(unquoted_argument('${PROJECT_NAME}')) \ 43 | .add(newlines(1)) 44 | 45 | root = file().add(command_invocation('add_custom_target(', args)) 46 | 47 | expected_formatting = """add_custom_target(abc 48 | ALL 49 | # first line 50 | # second line 51 | TARGET 52 | ${PROJECT_NAME} 53 | )""" 54 | self.assertFormatting(expected_formatting, root) 55 | 56 | def test_multiple_line_comments_before_first_keyword(self): 57 | args = arguments().add(unquoted_argument('abc')) \ 58 | .add(newlines(1)) \ 59 | .add(line_ending('# first line', 1)) \ 60 | .add(line_ending('# second line', 1)) \ 61 | .add(unquoted_argument('TARGET')) \ 62 | .add(newlines(1)) 63 | 64 | root = file().add(command_invocation('add_custom_target(', args)) 65 | 66 | expected_formatting = """add_custom_target(abc 67 | # first line 68 | # second line 69 | TARGET 70 | )""" 71 | self.assertFormatting(expected_formatting, root) 72 | 73 | def test_multiple_line_comments_at_the_end_of_invocation(self): 74 | args = arguments().add(unquoted_argument('abc')) \ 75 | .add(newlines(1)) \ 76 | .add(unquoted_argument('TARGET')) \ 77 | .add(newlines(1)) \ 78 | .add(line_ending('# first line', 1)) \ 79 | .add(line_ending('# second line', 1)) 80 | 81 | root = file().add(command_invocation('add_custom_target(', args)) 82 | 83 | expected_formatting = """add_custom_target(abc 84 | TARGET 85 | # first line 86 | # second line 87 | )""" 88 | self.assertFormatting(expected_formatting, root) 89 | 90 | def test_multiple_line_comments_at_the_start_of_invocation(self): 91 | args = arguments().add(newlines(1)) \ 92 | .add(line_ending('# first line', 1)) \ 93 | .add(line_ending('# second line', 1)) \ 94 | .add(unquoted_argument('TARGET')) \ 95 | .add(newlines(1)) 96 | 97 | root = file().add(command_invocation('add_custom_target(', args)) 98 | 99 | expected_formatting = """add_custom_target( 100 | # first line 101 | # second line 102 | TARGET 103 | )""" 104 | self.assertFormatting(expected_formatting, root) 105 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter_conditional_invocation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from cmake_tidy.lexical_data.elements import Element 8 | from tests.unit.parser_composite_elements import arguments, unquoted_argument, spaces, file, command_invocation, \ 9 | quoted_argument, line_ending, newlines 10 | from tests.unit.test_cmake_formatter import TestCMakeFormatter 11 | 12 | 13 | class TestCMakeFormatterConditionalInvocation(TestCMakeFormatter): 14 | def test_already_aligned_invocation(self): 15 | args = arguments() \ 16 | .add(unquoted_argument('abc')).add(spaces(' ')) \ 17 | .add(unquoted_argument('OR')).add(spaces(' ')) \ 18 | .add(unquoted_argument('def')) 19 | 20 | self.assertConditionFormatting('if(abc OR def)', args) 21 | 22 | def test_splitting_only_after_logical_operations(self): 23 | self.settings['condition_splitting_move_and_or_to_newline'] = False 24 | self.settings['line_length'] = 10 25 | 26 | args = arguments() \ 27 | .add(unquoted_argument('CMAKE_C_COMPILER_ID')).add(spaces(' ')) \ 28 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) \ 29 | .add(quoted_argument('GNU')).add(spaces(' ')) \ 30 | .add(unquoted_argument('AND')).add(spaces(' ')) \ 31 | .add(unquoted_argument('CMAKE_CXX_COMPILER_ID')).add(spaces(' ')) \ 32 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) \ 33 | .add(quoted_argument('GNU')) 34 | 35 | expected_formatting = """if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND 36 | CMAKE_CXX_COMPILER_ID STREQUAL "GNU")""" 37 | self.assertConditionFormatting(expected_formatting, args) 38 | 39 | def test_already_split_condition_should_have_correct_indent(self): 40 | args = arguments() \ 41 | .add(unquoted_argument('CMAKE_C_COMPILER_ID')).add(spaces(' ')) \ 42 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) \ 43 | .add(quoted_argument('GNU')).add(spaces(' ')) \ 44 | .add(unquoted_argument('AND')) \ 45 | .add(newlines(1)) \ 46 | .add(unquoted_argument('CMAKE_CXX_COMPILER_ID')).add(spaces(' ')) \ 47 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) \ 48 | .add(quoted_argument('GNU')) 49 | 50 | expected_formatting = """if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND 51 | CMAKE_CXX_COMPILER_ID STREQUAL "GNU")""" 52 | self.assertConditionFormatting(expected_formatting, args) 53 | 54 | def test_splitting_only_after_logical_operations_comments_excluded(self): 55 | self.settings['condition_splitting_move_and_or_to_newline'] = False 56 | self.settings['line_length'] = 10 57 | 58 | args = arguments() \ 59 | .add(unquoted_argument('CMAKE_C_COMPILER_ID')).add(spaces(' ')) \ 60 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) \ 61 | .add(quoted_argument('GNU')).add(spaces(' ')) \ 62 | .add(unquoted_argument('AND')).add(spaces(' ')) \ 63 | .add(line_ending('# a comment', 1)) \ 64 | .add(unquoted_argument('CMAKE_CXX_COMPILER_ID')).add(spaces(' ')) \ 65 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) \ 66 | .add(quoted_argument('GNU')) 67 | 68 | expected_formatting = """if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND # a comment 69 | CMAKE_CXX_COMPILER_ID STREQUAL "GNU")""" 70 | self.assertConditionFormatting(expected_formatting, args) 71 | 72 | def test_splitting_already_split_invocation_after_and(self): 73 | self.settings['condition_splitting_move_and_or_to_newline'] = False 74 | self.settings['line_length'] = 10 75 | 76 | args = arguments() \ 77 | .add(unquoted_argument('VERY_LONG_THING')).add(newlines(1)) \ 78 | .add(unquoted_argument('AND')).add(spaces(' ')) \ 79 | .add(unquoted_argument('CMAKE_CXX_COMPILER_ID')) 80 | 81 | self.assertConditionFormatting('if(VERY_LONG_THING AND\n CMAKE_CXX_COMPILER_ID)', args) 82 | 83 | def test_splitting_before_logical_operator(self): 84 | self.settings['condition_splitting_move_and_or_to_newline'] = True 85 | self.settings['line_length'] = 10 86 | 87 | args = arguments() \ 88 | .add(unquoted_argument('VERY_LONG_THING')).add(spaces(' ')) \ 89 | .add(unquoted_argument('OR')).add(spaces(' ')) \ 90 | .add(unquoted_argument('CMAKE_CXX_COMPILER_ID')) 91 | 92 | self.assertConditionFormatting('if(VERY_LONG_THING\n OR CMAKE_CXX_COMPILER_ID)', args) 93 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter_conditionals_with_parentheses.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from tests.unit.parser_composite_elements import arguments, unquoted_argument, spaces, \ 8 | quoted_argument, newlines, parentheses 9 | from tests.unit.test_cmake_formatter import TestCMakeFormatter 10 | 11 | 12 | class TestCMakeFormatterConditionalWithParentheses(TestCMakeFormatter): 13 | def setUp(self) -> None: 14 | super().setUp() 15 | self.settings['condition_splitting_move_and_or_to_newline'] = True 16 | 17 | def test_split_with_parentheses(self): 18 | condition = parentheses(arguments() 19 | .add(unquoted_argument('${CMAKE_CXX_COMPILER_ID}')).add(spaces(' ')) 20 | .add(unquoted_argument('STREQUAL')).add(spaces(' ')) 21 | .add(quoted_argument('GNU')).add(newlines(5)) 22 | .add(unquoted_argument('AND')).add(newlines(1)) 23 | .add(unquoted_argument('${CMAKE_CXX_COMPILER_VERSION}')).add(spaces(' ')) 24 | .add(unquoted_argument('VERSION_LESS')).add(spaces(' ')) 25 | .add(quoted_argument('9'))) 26 | 27 | expected_formatting = """if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" 28 | AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS "9"))""" 29 | 30 | self.assertConditionFormatting(expected_formatting, arguments().add(condition)) 31 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter_elements_interactions.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from tests.unit.test_cmake_formatter import TestCMakeFormatter 8 | from tests.unit.parser_composite_elements import file, command_invocation, spaces, line_ending, newlines 9 | 10 | 11 | class TestCMakeFormatterElementsInteractions(TestCMakeFormatter): 12 | def test_command_invocation_should_be_by_default_lowercase(self): 13 | invocation = file() \ 14 | .add(command_invocation('abc(')) \ 15 | .add(spaces(' \t')) \ 16 | .add(line_ending('# a comment', 1)) 17 | self.assertFormatting('abc() # a comment\n', invocation) 18 | 19 | def test_if_statement_with_space_while_other_invocations_are_not_affected(self): 20 | self.settings['space_after_loop_condition'] = True 21 | invocation = file() \ 22 | .add(command_invocation('if(')) \ 23 | .add(newlines(1)) \ 24 | .add(command_invocation('abc(')) \ 25 | .add(newlines(1)) \ 26 | .add(command_invocation('endif(')) 27 | 28 | expected_formatting = """if () 29 | abc() 30 | endif()""" 31 | 32 | self.assertFormatting(expected_formatting, invocation) 33 | 34 | def test_uppercase_if_statement_handled_correctly_like_lowercase(self): 35 | self.settings['space_after_loop_condition'] = True 36 | self.settings['force_command_lowercase'] = False 37 | 38 | invocation = file() \ 39 | .add(command_invocation('IF(')) \ 40 | .add(newlines(1)) \ 41 | .add(command_invocation('abc(')) \ 42 | .add(newlines(1)) \ 43 | .add(command_invocation('ELSEIF (')) \ 44 | .add(newlines(1)) \ 45 | .add(command_invocation('def(')) \ 46 | .add(newlines(1)) \ 47 | .add(command_invocation('ENDIF(')) 48 | 49 | expected_formatting = """IF () 50 | abc() 51 | ELSEIF () 52 | def() 53 | ENDIF()""" 54 | 55 | self.assertFormatting(expected_formatting, invocation) 56 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter_invocation_wrapping.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from tests.unit.parser_composite_elements import arguments, newlines, spaces, unquoted_argument, file, \ 8 | command_invocation 9 | from tests.unit.test_cmake_formatter import TestCMakeFormatter 10 | 11 | 12 | class TestCMakeFormatterCommandInvocationsWrapping(TestCMakeFormatter): 13 | def test_invocation_wrapping_for_short_function(self): 14 | self.settings['wrap_short_invocations_to_single_line'] = True 15 | args = arguments() \ 16 | .add(newlines(4)) \ 17 | .add(spaces(' ')) \ 18 | .add(unquoted_argument('argument1')) \ 19 | .add(spaces(' ')) \ 20 | .add(unquoted_argument('argument2')) \ 21 | .add(newlines(4)) 22 | root = file().add(command_invocation('function_call(', args)) 23 | 24 | expected_formatting = """function_call(argument1 argument2)""" 25 | 26 | self.assertFormatting(expected_formatting, root) 27 | 28 | def test_invocation_wrapping_only_when_line_length_is_smaller_than_set_threshold(self): 29 | self.settings['wrap_short_invocations_to_single_line'] = True 30 | self.settings['line_length'] = 15 31 | 32 | args = arguments() \ 33 | .add(newlines(1)) \ 34 | .add(unquoted_argument('abc')) \ 35 | .add(newlines(1)) \ 36 | .add(unquoted_argument('def')) 37 | wrappable_invocation = command_invocation('wr(', args) 38 | not_wrappable_invocation = command_invocation('a_very_long_name_command(', args) 39 | root = file().add(wrappable_invocation) \ 40 | .add(newlines(1)) \ 41 | .add(not_wrappable_invocation) 42 | 43 | expected_formatting = """wr(abc def) 44 | a_very_long_name_command( 45 | abc 46 | def)""" 47 | 48 | self.assertFormatting(expected_formatting, root) 49 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_formatter_invocations.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from tests.unit.parser_composite_elements import newlines, spaces, file, command_invocation, line_ending, arguments, \ 8 | unquoted_argument 9 | from tests.unit.test_cmake_formatter import TestCMakeFormatter 10 | 11 | 12 | class TestCMakeFormatterCommandInvocations(TestCMakeFormatter): 13 | def test_command_invocation_should_be_by_default_lowercase(self): 14 | invocation = file().add(command_invocation('FUNCTION(')) 15 | self.assertFormatting('function()', invocation) 16 | 17 | def test_command_invocation_not_change_to_lowercase_when_decided(self): 18 | self.settings['force_command_lowercase'] = False 19 | invocation = file().add(command_invocation('FUNCTION(')) 20 | self.assertFormatting('FUNCTION()', invocation) 21 | 22 | def test_add_space_between_command_name_and_begin_parentheses_when_decided(self): 23 | self.settings['space_between_command_and_begin_parentheses'] = True 24 | invocation = file().add(command_invocation('if(')) 25 | self.assertFormatting('if ()', invocation) 26 | 27 | def test_function_declaration_should_indent_correctly_within_its_scope(self): 28 | function_with_invocation_in_second_line = file() \ 29 | .add(command_invocation('function(')) \ 30 | .add(newlines(1)) \ 31 | .add(line_ending('# comment', 1)) \ 32 | .add(command_invocation('test(')) \ 33 | .add(newlines(1)) \ 34 | .add(command_invocation('endfunction(')) \ 35 | .add(newlines(1)) \ 36 | .add(command_invocation('test2(')) 37 | expected_formatting = """function() 38 | # comment 39 | test() 40 | endfunction() 41 | test2()""" 42 | self.assertFormatting(expected_formatting, function_with_invocation_in_second_line) 43 | 44 | def test_if_statement_should_indent_properly_also_removing_unneeded_spaces(self): 45 | root = file() \ 46 | .add(command_invocation('if (')) \ 47 | .add(newlines(1)) \ 48 | .add(spaces(' ')) \ 49 | .add(command_invocation('test(')) \ 50 | .add(newlines(1)) \ 51 | .add(spaces(' ')) \ 52 | .add(command_invocation('elseif(')) \ 53 | .add(newlines(1)) \ 54 | .add(spaces(' ')) \ 55 | .add(command_invocation('test(')) \ 56 | .add(newlines(1)) \ 57 | .add(spaces(' ')) \ 58 | .add(command_invocation('endif(')) 59 | 60 | expected_formatting = """if() 61 | test() 62 | elseif() 63 | test() 64 | endif()""" 65 | 66 | self.assertFormatting(expected_formatting, root) 67 | 68 | def test_invocation_with_whitespaces_before_line_end(self): 69 | invocation_lead_by_spaces = file() \ 70 | .add(command_invocation('set(')) \ 71 | .add(spaces(' ')) \ 72 | .add(newlines(1)) 73 | 74 | self.assertFormatting('set()\n', invocation_lead_by_spaces) 75 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_parser.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.parsing.cmake_parser import CMakeParser 10 | from cmake_tidy.lexical_data.elements import PrimitiveElement 11 | from tests.unit.parser_composite_elements import file, line_ending, spaces, unhandled, newlines 12 | 13 | 14 | class TestCMakeParser(unittest.TestCase): 15 | def setUp(self) -> None: 16 | self.parser = CMakeParser() 17 | 18 | def assertReprEqual(self, expected, received): 19 | self.assertEqual(str(expected), str(received)) 20 | 21 | 22 | class TestParseBasicElements(TestCMakeParser): 23 | def test_should_parse_correctly_empty_input(self): 24 | self.assertReprEqual(PrimitiveElement(), self.parser.parse('')) 25 | 26 | def test_should_parse_correctly_newlines(self): 27 | file_with_new_lines = file().add(newlines(2)) 28 | self.assertReprEqual(file_with_new_lines, self.parser.parse('\n\n')) 29 | 30 | def test_should_handle_line_comments(self): 31 | comment = '# comment here' 32 | root = file().add(line_ending(comment, 2)) 33 | 34 | self.assertReprEqual(root, self.parser.parse(comment + '\n\n')) 35 | 36 | def test_should_handle_empty_comment(self): 37 | comment = '#' 38 | root = file().add(line_ending(comment, 2)) 39 | 40 | self.assertReprEqual(root, self.parser.parse(comment + '\n\n')) 41 | 42 | def test_should_parse_line_comments(self): 43 | comment = '# cdaew9u32#$#@%#232cd a2o#@$@!' 44 | root = file() \ 45 | .add(line_ending(comment, 1)) 46 | 47 | self.assertReprEqual(root, self.parser.parse(comment + '\n')) 48 | -------------------------------------------------------------------------------- /tests/unit/test_cmake_parser_command_invocation.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | from tests.unit.test_cmake_parser import TestCMakeParser 8 | from tests.unit.parser_composite_elements import file, command_invocation, arguments, unquoted_argument, \ 9 | bracket_argument, quoted_argument, spaces, newlines, parentheses, line_ending 10 | 11 | 12 | class TestParseCommandInvocation(TestCMakeParser): 13 | def test_without_arguments(self): 14 | start_invocation = 'include(' 15 | root = file().add(command_invocation(start_invocation, [])) 16 | 17 | self.assertReprEqual(root, self.parser.parse(start_invocation + ')')) 18 | 19 | def test_with_empty_quoted_argument(self): 20 | start_invocation = 'include(' 21 | root = file().add(command_invocation(start_invocation, arguments().add(quoted_argument('')))) 22 | 23 | self.assertReprEqual(root, self.parser.parse(start_invocation + '\"\")')) 24 | 25 | def test_with_unquoted_arguments_in_braces(self): 26 | start_invocation = 'include(' 27 | expected_args = arguments().add( 28 | parentheses(arguments().add(unquoted_argument('some'))) 29 | ) 30 | root = file().add(command_invocation(start_invocation, expected_args)) 31 | 32 | self.assertReprEqual(root, self.parser.parse(start_invocation + '(some))')) 33 | 34 | def test_with_bracket_argument(self): 35 | start_invocation = 'function_name(' 36 | bracket_start = '[[' 37 | bracket_end = ']]' 38 | bracket_argument_data = 'this is bracket_dwad832423#$@#$ content]===] still there' 39 | 40 | root = file().add(command_invocation(start_invocation, 41 | arguments().add(bracket_argument(0, bracket_argument_data)))) 42 | 43 | self.assertReprEqual(root, self.parser.parse( 44 | f'{start_invocation}{bracket_start}{bracket_argument_data}{bracket_end})')) 45 | 46 | def test_with_quoted_argument_with_escaped_quote_inside(self): 47 | start_invocation = 'name(' 48 | argument_content = 'simple\n\\\" text' 49 | root = file().add( 50 | command_invocation(start_invocation, 51 | arguments().add(quoted_argument(argument_content))) 52 | ) 53 | 54 | self.assertReprEqual(root, self.parser.parse( 55 | f'{start_invocation}"{argument_content}")')) 56 | 57 | def test_real_add_test_command_example(self): 58 | command = """add_test( 59 | NAME dbg-${TARGET}-fast 60 | CONFIGURATIONS Debug 61 | COMMAND ${Runner_BINARY_DEBUG} $ 62 | ("${DATA_PATH_OPTION}" 63 | [===[--text]===]) 64 | )""" 65 | 66 | expected_arguments_in_parentheses = parentheses(arguments() 67 | .add(quoted_argument('${DATA_PATH_OPTION}')) 68 | .add(newlines(1)) 69 | .add(spaces(' ')) 70 | .add(bracket_argument(3, '--text'))) 71 | 72 | expected_args = arguments() \ 73 | .add(newlines(1)) \ 74 | .add(spaces(' ')) \ 75 | .add(unquoted_argument('NAME')) \ 76 | .add(spaces(' ')) \ 77 | .add(unquoted_argument('dbg-${TARGET}-fast')) \ 78 | .add(newlines(1)) \ 79 | .add(spaces(' ')) \ 80 | .add(unquoted_argument('CONFIGURATIONS')) \ 81 | .add(spaces(' ')) \ 82 | .add(unquoted_argument('Debug')) \ 83 | .add(newlines(1)) \ 84 | .add(spaces(' ')) \ 85 | .add(unquoted_argument('COMMAND')) \ 86 | .add(spaces(' ')) \ 87 | .add(unquoted_argument('${Runner_BINARY_DEBUG}')) \ 88 | .add(spaces(' ')) \ 89 | .add(unquoted_argument('$')) \ 90 | .add(newlines(1)) \ 91 | .add(spaces(' ')) \ 92 | .add(expected_arguments_in_parentheses) \ 93 | .add(newlines(1)) \ 94 | .add(spaces(' ')) 95 | expected_invocation = command_invocation('add_test(', expected_args) 96 | expected_parsed_structure = file().add(expected_invocation) 97 | 98 | self.assertReprEqual(expected_parsed_structure, self.parser.parse(command)) 99 | 100 | def test_command_with_line_comment(self): 101 | command = """add_test( 102 | NAME # a name 103 | CONFIGURATIONS)""" 104 | 105 | expected_args = arguments() \ 106 | .add(newlines(1)) \ 107 | .add(spaces(' ')) \ 108 | .add(unquoted_argument('NAME')) \ 109 | .add(spaces(' ')) \ 110 | .add(line_ending('# a name', 1)) \ 111 | .add(spaces(' ')) \ 112 | .add(unquoted_argument('CONFIGURATIONS')) 113 | 114 | expected_parsed_structure = file().add(command_invocation('add_test(', expected_args)) 115 | 116 | self.assertReprEqual(expected_parsed_structure, self.parser.parse(command)) 117 | 118 | def test_escape_sequence_in_quoted_argument(self): 119 | command = 'string("\\\\")' 120 | 121 | expected_args = arguments().add(quoted_argument('\\\\')) 122 | expected_parsed_structure = file().add(command_invocation('string(', expected_args)) 123 | 124 | self.assertReprEqual(expected_parsed_structure, self.parser.parse(command)) 125 | -------------------------------------------------------------------------------- /tests/unit/test_configuration.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.utils.app_configuration.configuration import Configuration 10 | 11 | 12 | class TestConfigurationPropertiesHandling(unittest.TestCase): 13 | def setUp(self) -> None: 14 | self.config = Configuration({}) 15 | 16 | def test_not_existing_property_should_raise(self): 17 | with self.assertRaises(AttributeError): 18 | self.config.some 19 | 20 | def test_all_properties_should_not_contain_key_with_same_name(self): 21 | self.assertNotIn('all_properties', self.config.all_properties) 22 | 23 | 24 | class TestConfigurationInheritanceBehavior(unittest.TestCase): 25 | class BasedOnConfiguration(Configuration): 26 | @property 27 | def a_property(self): 28 | return 'a_property' 29 | 30 | class InheritedConfiguration(BasedOnConfiguration): 31 | def __init__(self, arguments: dict): 32 | super().__init__(arguments) 33 | 34 | @property 35 | def new_property(self): 36 | return 'new' 37 | 38 | @property 39 | def initialized_property(self): 40 | return self._config.get(self._property_name()) 41 | 42 | def setUp(self) -> None: 43 | self.inherited_config = self.InheritedConfiguration({'initialized_property': 'abc'}) 44 | 45 | def test_should_contain_main_class_property(self): 46 | self.assertEqual('a_property', self.inherited_config.a_property) 47 | 48 | def test_should_have_new_property_setup(self): 49 | self.assertEqual('new', self.inherited_config.new_property) 50 | 51 | def test_should_initialize_correctly_inherited_property(self): 52 | self.assertEqual('abc', self.inherited_config.initialized_property) 53 | 54 | def test_all_properties_should_contain_also_inherited_ones(self): 55 | self.assertIn('new_property', self.inherited_config.all_properties) 56 | -------------------------------------------------------------------------------- /tests/unit/test_elements.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.lexical_data.elements import PrimitiveElement, ComplexElement 10 | 11 | 12 | class TestElements(unittest.TestCase): 13 | def test_empty_primitive_should_print_nothing(self): 14 | self.assertEqual('', str(PrimitiveElement())) 15 | 16 | def test_primitive_should_hold_name_and_list_of_values(self): 17 | values = ['abc', 5, 'd'] 18 | name = 'property' 19 | 20 | primitive = PrimitiveElement(name, values) 21 | 22 | self.assertEqual(name, primitive.name) 23 | self.assertListEqual(values, primitive.values) 24 | self.assertEqual(f'{name}: {values}', str(primitive)) 25 | 26 | def test_complex_element_should_print_nothing_when_empty(self): 27 | self.assertEqual('', str(ComplexElement())) 28 | 29 | def test_complex_element_should_handle_multiple_primitives(self): 30 | element = ComplexElement('complex') \ 31 | .add(PrimitiveElement('abc', 123)) \ 32 | .add(PrimitiveElement('def', 456)) 33 | 34 | self.assertIn('complex.abc: 123', str(element)) 35 | self.assertIn('complex.def: 456', str(element)) 36 | 37 | def test_complex_element_should_ignore_none_provided_as_element(self): 38 | element = ComplexElement('complex') \ 39 | .add(PrimitiveElement('abc', 123)) \ 40 | .add(None) \ 41 | .add(PrimitiveElement('def', 456)) 42 | 43 | self.assertEqual('complex.abc: 123\ncomplex.def: 456', str(element)) 44 | 45 | def test_complex_elements_can_be_both_populated_with_primitives_and_complex_elements(self): 46 | root = ComplexElement('root') 47 | root.add(PrimitiveElement('abc', 123)) 48 | root.add(ComplexElement('another') 49 | .add(PrimitiveElement('def', 456)) 50 | .add(PrimitiveElement('ghi', 789))) 51 | root.add(ComplexElement('empty')) 52 | 53 | self.assertEqual('root.abc: 123\nroot.another.def: 456\n another.ghi: 789', str(root)) 54 | 55 | def test_visitor_printer_going_through_tree(self): 56 | visitor = self.Visitor() 57 | 58 | root = ComplexElement('file') \ 59 | .add(ComplexElement('element').add(PrimitiveElement('a', 1))) \ 60 | .add(PrimitiveElement('b', 2)) 61 | root.accept(visitor) 62 | 63 | self.assertListEqual(['a', 'element', 'b', 'file'], visitor.calls) 64 | 65 | class Visitor: 66 | def __init__(self): 67 | self.calls = [] 68 | 69 | def visit(self, name: str, value=None): 70 | self.calls.append(name) 71 | -------------------------------------------------------------------------------- /tests/unit/test_formatting_settings_reader.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | from pathlib import Path 6 | from unittest import mock, TestCase 7 | 8 | from cmake_tidy.formatting.settings_reader import InvalidSchemaError, SettingsReader, SchemaValidationError 9 | 10 | 11 | class TestSettingReader(TestCase): 12 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_schema_file', 13 | mock.MagicMock(return_value={'description', 'type'})) 14 | def test_settings_reader_construction_should_raise_when_schema_is_invalid(self): 15 | with self.assertRaises(InvalidSchemaError): 16 | SettingsReader() 17 | 18 | def test_settings_reader_current_schema_should_be_correct(self): 19 | try: 20 | SettingsReader() 21 | except InvalidSchemaError: 22 | self.fail() 23 | 24 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_settings', 25 | mock.MagicMock(return_value={'random': 1})) 26 | def test_invalid_settings_data_provided(self): 27 | self.assertSchemaValidationFails() 28 | 29 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_settings', 30 | mock.MagicMock(return_value={'line_length': True})) 31 | def test_valid_setting_name_with_wrong_value_type(self): 32 | self.assertSchemaValidationFails() 33 | 34 | @mock.patch('cmake_tidy.formatting.settings_reader.SettingsReader._read_settings') 35 | def test_default_settings_should_be_valid(self, read_settings): 36 | read_settings.return_value = SettingsReader.get_default_format_settings() 37 | try: 38 | SettingsReader().try_loading_format_settings(Path('invalid')) 39 | except SchemaValidationError: 40 | self.fail() 41 | 42 | def assertSchemaValidationFails(self): 43 | with self.assertRaises(SchemaValidationError): 44 | SettingsReader().try_loading_format_settings(Path('invalid')) 45 | -------------------------------------------------------------------------------- /tests/unit/test_keyword_verifier.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | from unittest import mock 9 | from unittest.mock import MagicMock 10 | 11 | from cmake_tidy.formatting.utils.tokens import Tokens 12 | from cmake_tidy.lexical_data import KeywordVerifier 13 | 14 | 15 | class TestKeywordVerifier(unittest.TestCase): 16 | def setUp(self) -> None: 17 | self.settings = {'keywords': ['some'], 'unquoted_uppercase_as_keyword': False} 18 | self.verify = KeywordVerifier(self.settings) 19 | 20 | @mock.patch('builtins.open') 21 | def test_ensure_properties_are_read_only_once(self, mock_open: MagicMock): 22 | self.verify = KeywordVerifier(self.settings) 23 | self.assertFalse(mock_open.called) 24 | 25 | def test_should_accept_keyword_when_on_the_list(self): 26 | self.assertTrue(self.verify.is_keyword('some')) 27 | self.assertTrue(self.verify.is_keyword(Tokens.reindent(1) + 'some')) 28 | self.assertFalse(self.verify.is_keyword('some2')) 29 | self.assertFalse(self.verify.is_keyword('${some}')) 30 | 31 | def test_unquoted_arguments_with_uppercase_letters_only_are_keywords(self): 32 | self.settings['unquoted_uppercase_as_keyword'] = True 33 | self.verify = KeywordVerifier(self.settings) 34 | 35 | self.assertTrue(self.verify.is_keyword('OTHER')) 36 | self.assertTrue(self.verify.is_keyword(Tokens.reindent(1) + 'OTHER')) 37 | self.assertTrue(self.verify.is_keyword('WITH_SEPARATION')) 38 | 39 | self.assertFalse(self.verify.is_keyword('"$OTHER"')) 40 | self.assertFalse(self.verify.is_keyword('SOMeARG')) 41 | self.assertFalse(self.verify.is_keyword('a_ARGUMENT')) 42 | self.assertFalse(self.verify.is_keyword('NOT_')) 43 | self.assertFalse(self.verify.is_keyword('_SOME')) 44 | 45 | def test_whether_token_is_first_class_keyword(self): 46 | self.assertTrue(self.verify.is_first_class_keyword('PROPERTY')) 47 | self.assertTrue(self.verify.is_first_class_keyword('PROPERTIES')) 48 | self.assertTrue(self.verify.is_first_class_keyword(Tokens.reindent(1) + 'PROPERTY')) 49 | self.assertFalse(self.verify.is_first_class_keyword('PROPERTY2')) 50 | self.assertFalse(self.verify.is_first_class_keyword('proPERTY')) 51 | 52 | def test_available_properties_version(self): 53 | self.assertEqual('3.18.0', self.verify.get_cmake_properties_version()) 54 | 55 | def test_cmake_properties_matching_exactly(self): 56 | self.assertTrue(self.verify.is_property('LINK_DIRECTORIES')) 57 | self.assertTrue(self.verify.is_property('INSTALL_REMOVE_ENVIRONMENT_RPATH')) 58 | self.assertFalse(self.verify.is_property('1INSTALL_REMOVE_ENVIRONMENT_RPATH')) 59 | 60 | def test_cmake_properties_starting_with(self): 61 | self.assertTrue(self.verify.is_property('IMPORTED_NO_SONAME')) 62 | self.assertTrue(self.verify.is_property('IMPORTED_NO_SONAME_123')) 63 | self.assertFalse(self.verify.is_property('IMPORTED_NO_SONAME123')) 64 | self.assertFalse(self.verify.is_property('123_IMPORTED_NO_SONAME_123')) 65 | self.assertFalse(self.verify.is_property('IMPORTED_NO_SONAM')) 66 | 67 | def test_cmake_properties_ending_with(self): 68 | self.assertTrue(self.verify.is_property('_OUTPUT_NAME')) 69 | self.assertTrue(self.verify.is_property('VALUE_OUTPUT_NAME')) 70 | self.assertFalse(self.verify.is_property('_OUTPUT_NAME_VALUE')) 71 | 72 | def test_cmake_properties_with_reindent_token(self): 73 | self.assertTrue(self.verify.is_property(Tokens.reindent(1) + 'LINK_DIRECTORIES')) 74 | 75 | def test_double_keywords(self): 76 | self.assertTrue(self.verify.is_double_keyword(Tokens.reindent(1) + 'RUNTIME', 'DESTINATION')) 77 | self.assertTrue(self.verify.is_double_keyword('ARCHIVE', Tokens.reindent(1) + 'DESTINATION')) 78 | self.assertTrue(self.verify.is_double_keyword('LIBRARY', 'DESTINATION')) 79 | self.assertFalse(self.verify.is_double_keyword('OUTPUT', 'DESTINATION')) 80 | self.assertFalse(self.verify.is_double_keyword('LIBRARY', 'OUTPUT')) 81 | 82 | def test_recognition_of_conditional_invocation(self): 83 | self.assertTrue(KeywordVerifier.is_conditional_invocation('If(')) 84 | self.assertTrue(KeywordVerifier.is_conditional_invocation('while(')) 85 | self.assertTrue(KeywordVerifier.is_conditional_invocation('while (')) 86 | self.assertFalse(KeywordVerifier.is_conditional_invocation('foreach (')) 87 | self.assertFalse(KeywordVerifier.is_conditional_invocation('if2(')) 88 | self.assertFalse(KeywordVerifier.is_conditional_invocation('if*')) 89 | 90 | def test_is_command_keyword(self): 91 | self.assertTrue(KeywordVerifier.is_command_keyword('COMMAND')) 92 | self.assertTrue(KeywordVerifier.is_command_keyword(Tokens.reindent(1) + 'COMMAND')) 93 | self.assertTrue(KeywordVerifier.is_command_keyword(Tokens.reindent(1) + 'ARGS')) 94 | self.assertFalse(KeywordVerifier.is_command_keyword('CMD')) 95 | self.assertFalse(KeywordVerifier.is_command_keyword(Tokens.reindent(2) + 'COMMAND')) 96 | -------------------------------------------------------------------------------- /tests/unit/test_line_length_calculator.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | from unittest import mock 9 | from unittest.mock import MagicMock 10 | 11 | from cmake_tidy.formatting.utils.line_length_calculator import LineLengthCalculator 12 | from cmake_tidy.formatting.utils.tokens import Tokens 13 | from cmake_tidy.lexical_data import KeywordVerifier 14 | 15 | 16 | class TestLineLengthCalculator(unittest.TestCase): 17 | def setUp(self) -> None: 18 | self.calculator = LineLengthCalculator({'tab_size': 4}) 19 | 20 | def test_calculate_length_of_simple_strings(self): 21 | self.assertEqual(0, self.calculator.calculate('')) 22 | self.assertEqual(88, self.calculator.calculate(' ' * 88)) 23 | self.assertEqual(16, self.calculator.calculate('some(invocation)')) 24 | 25 | def test_calculate_size_of_tabs(self): 26 | self.assertEqual(4, LineLengthCalculator({'tab_size': 4}).calculate('\t')) 27 | self.assertEqual(16, LineLengthCalculator({'tab_size': 8}).calculate('\t\t')) 28 | 29 | def test_ignore_reindent_tokens(self): 30 | self.assertEqual(0, self.calculator.calculate(Tokens.reindent(3))) 31 | self.assertEqual(14, self.calculator.calculate(f'\tsome{Tokens.reindent(99)}\tso')) 32 | 33 | def test_ignore_remove_spaces_tokens(self): 34 | self.assertEqual(0, self.calculator.calculate(Tokens.remove_spaces())) 35 | self.assertEqual(4, self.calculator.calculate(f'piwo{Tokens.remove_spaces()}')) 36 | -------------------------------------------------------------------------------- /tests/unit/test_proxy_visitor.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.utils.proxy_visitor import ProxyVisitor 10 | 11 | 12 | class TestProxyVisitor(unittest.TestCase): 13 | @staticmethod 14 | def __private_function_with_int_argument(number: int) -> str: 15 | return str(number) 16 | 17 | class SimpleFunctor: 18 | def __init__(self): 19 | self.state = False 20 | 21 | def __call__(self, x: str) -> None: 22 | self.state = True 23 | 24 | def setUp(self): 25 | self.function_dict = {'private': self.__private_function_with_int_argument} 26 | 27 | def test_private_function_invoked_in_proxy(self): 28 | proxy = ProxyVisitor(self.function_dict) 29 | self.assertEqual('5', proxy.visit('private', 5)) 30 | 31 | def test_lambda_invocation_in_proxy(self): 32 | self.function_dict['lambda'] = lambda a: a[0] + a[1] 33 | proxy = ProxyVisitor(self.function_dict) 34 | self.assertEqual(6, proxy.visit('lambda', [2, 4])) 35 | 36 | def test_functor_preserving_state(self): 37 | functor = self.SimpleFunctor() 38 | self.function_dict['functor'] = functor 39 | proxy = ProxyVisitor(self.function_dict) 40 | proxy.visit('functor') 41 | self.assertTrue(functor.state) 42 | -------------------------------------------------------------------------------- /tests/unit/test_tokens.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright Maciej Patro (maciej.patro@gmail.com) 3 | # MIT License 4 | ############################################################################### 5 | 6 | 7 | import unittest 8 | 9 | from cmake_tidy.formatting.utils.tokens import Tokens 10 | 11 | 12 | class TestTokens(unittest.TestCase): 13 | def test_reindent_token_generation_and_matching(self): 14 | self.assertRegex(Tokens.reindent(1), Tokens.get_reindent_regex()) 15 | self.assertRegex(Tokens.reindent(9), Tokens.get_reindent_regex()) 16 | self.assertRegex(Tokens.reindent(), Tokens.get_reindent_regex()) 17 | 18 | def test_matching_line_comment_tokens(self): 19 | self.assertTrue(Tokens.is_line_comment('# comment')) 20 | self.assertTrue(Tokens.is_line_comment('#comment')) 21 | self.assertTrue(Tokens.is_line_comment(Tokens.reindent(1) + '#comment')) 22 | self.assertTrue(Tokens.is_line_comment(Tokens.remove_spaces() + Tokens.reindent(1) + '#comment')) 23 | self.assertTrue(Tokens.is_line_comment('\t # something')) 24 | self.assertTrue(Tokens.is_line_comment('\t #')) 25 | self.assertFalse(Tokens.is_line_comment('_# not a comment')) 26 | self.assertFalse(Tokens.is_line_comment('comment')) 27 | 28 | def test_matching_spacing_tokens(self): 29 | self.assertFalse(Tokens.is_line_comment('')) 30 | self.assertTrue(Tokens.is_spacing_token(' ')) 31 | self.assertTrue(Tokens.is_spacing_token('\t ')) 32 | self.assertTrue(Tokens.is_spacing_token('\t \n')) 33 | self.assertTrue(Tokens.is_spacing_token(f'\t {Tokens.remove_spaces()}\n')) 34 | self.assertFalse(Tokens.is_spacing_token('\t d')) 35 | self.assertFalse(Tokens.is_spacing_token(f'\t {Tokens.reindent(1)}')) 36 | --------------------------------------------------------------------------------