diff --git a/cmake_format/BUILD b/cmake_format/BUILD index 6ef457d..7bd3806 100644 --- a/cmake_format/BUILD +++ b/cmake_format/BUILD @@ -3,49 +3,48 @@ package(default_visibility=["//visibility:public"]) py_library( name="cmake_format", srcs=[ - "doc/gendoc_sources.py", - "doc/conf.py", - "screw_users_test.py", + "__init__.py", "__main__.py", - "parser_tests.py", + "annotate.py", + "commands.py", "common.py", - "markup_tests.py", - "format_tests.py", - "layout_tests.py", - "invocation_tests.py", - "__init__.py", - "formatter.py", "configuration.py", - "command_tests/file_tests.py", - "command_tests/add_library_tests.py", - "command_tests/install_tests.py", - "command_tests/add_executable_tests.py", - "command_tests/__init__.py", - "command_tests/export_tests.py", - "command_tests/conditional_tests.py", - "command_tests/set_tests.py", - "command_tests/add_custom_command_tests.py", + "doc/conf.py", + "doc/gendoc_sources.py", + "formatter.py", + "invocation_tests.py", + "layout_tests.py", + "lexer.py", + "lexer_tests.py", + "markup.py", + "markup_tests.py", + "parse_funs/add_executable.py", + "parse_funs/add_library.py", + "parse_funs/add_xxx.py", "parse_funs/external_project.py", + "parse_funs/fetch_content.py", "parse_funs/file.py", - "parse_funs/add_library.py", "parse_funs/__init__.py", - "parse_funs/add_executable.py", - "parse_funs/add_xxx.py", "parse_funs/install.py", - "parse_funs/fetch_content.py", - "tests.py", - "commands.py", + "parser.py", + "parser_tests.py", "pypi/setup.py", "render.py", - "parser.py", - "lexer_tests.py", + "screw_users_test.py", "test/cmake-format.py", - "lexer.py", - "annotate.py", - "markup.py"], + "test/cmake-format-split-1.py", + "test/cmake-format-split-2.py", + "tests.py"], data=["templates/layout.html.tpl", "templates/style.css"]) +py_library( + name="testdata", + data=glob(["test/*"]), +) + +# -- Python 2 -- + py_binary( name="cmake-format", srcs=["__main__.py"], @@ -53,19 +52,10 @@ py_binary( main="__main__.py" ) -py_test( - name="format_tests", - srcs=["format_tests.py"], - deps=[":cmake_format"], - data=glob(["test/*"]), - python_version="PY2", - ) - py_test( name="invocation_tests", srcs=["invocation_tests.py"], - deps=[":cmake_format"], - data=glob(["test/*"]), + deps=[":cmake_format", ":testdata"], python_version="PY2", ) @@ -97,22 +87,13 @@ py_test( python_version="PY2", ) - -py_test( - name="format_tests_py3", - srcs=["format_tests.py"], - main="format_tests.py", - deps=[":cmake_format"], - data=glob(["test/*"]), - python_version="PY3", - ) +# -- Python 3 -- py_test( name="invocation_tests_py3", srcs=["invocation_tests.py"], main="invocation_tests.py", - deps=[":cmake_format"], - data=glob(["test/*"]), + deps=[":cmake_format", ":testdata"], python_version="PY3", ) diff --git a/cmake_format/CMakeLists.txt b/cmake_format/CMakeLists.txt index d2e452b..cd5afd9 100644 --- a/cmake_format/CMakeLists.txt +++ b/cmake_format/CMakeLists.txt @@ -1,84 +1,72 @@ -format_and_lint(cmake_format - # cmake-format: sort - __init__.py - __main__.py - annotate.py - commands.py - command_tests/add_custom_command_tests.py - command_tests/add_executable_tests.py - command_tests/add_library_tests.py - command_tests/conditional_tests.py - command_tests/export_tests.py - command_tests/file_tests.py - command_tests/__init__.py - command_tests/install_tests.py - command_tests/set_tests.py - common.py - configuration.py - doc/conf.py - doc/gendoc_sources.py - formatter.py - format_tests.py - invocation_tests.py - layout_tests.py - lexer.py - lexer_tests.py - markup.py - markup_tests.py - parse_funs/add_executable.py - parse_funs/add_library.py - parse_funs/add_xxx.py - parse_funs/external_project.py - parse_funs/fetch_content.py - parse_funs/file.py - parse_funs/__init__.py - parse_funs/install.py - parser.py - parser_tests.py - pypi/setup.py - render.py - screw_users_test.py - test/cmake-format.py - tests.py) +format_and_lint( + cmake_format + # cmake-format: sort + __init__.py + __main__.py + annotate.py + commands.py + command_tests/add_custom_command_tests.py + command_tests/add_executable_tests.py + command_tests/add_library_tests.py + command_tests/conditional_tests.py + command_tests/export_tests.py + command_tests/file_tests.py + command_tests/__init__.py + command_tests/install_tests.py + command_tests/__main__.py + command_tests/misc_tests.py + command_tests/set_tests.py + common.py + configuration.py + doc/conf.py + doc/gendoc_sources.py + formatter.py + invocation_tests.py + layout_tests.py + lexer.py + lexer_tests.py + markup.py + markup_tests.py + parse_funs/add_executable.py + parse_funs/add_library.py + parse_funs/add_xxx.py + parse_funs/external_project.py + parse_funs/fetch_content.py + parse_funs/file.py + parse_funs/__init__.py + parse_funs/install.py + parser.py + parser_tests.py + pypi/setup.py + render.py + screw_users_test.py + test/cmake-format.py + test/cmake-format-split-1.py + test/cmake-format-split-2.py + tests.py) -add_test(NAME cmake_format-format_tests - COMMAND python -m cmake_format.format_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-invocation_tests - COMMAND python -m cmake_format.invocation_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-layout_tests - COMMAND python -m cmake_format.layout_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-lexer_tests - COMMAND python -m cmake_format.lexer_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-markup_tests - COMMAND python -m cmake_format.markup_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-parser_tests - COMMAND python -m cmake_format.parser_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +set( + _testnames + invocation_tests + layout_tests + lexer_tests + markup_tests + parser_tests) + +foreach(_testname ${_testnames}) + add_test( + NAME cmake_format-${_testname} + COMMAND python -Bm cmake_format.${_testname} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +endforeach() if(NOT IS_TRAVIS_CI) - add_test(NAME cmake_format-format_tests_py3 - COMMAND python3 -m cmake_format.format_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-invocation_tests_py3 - COMMAND python3 -m cmake_format.invocation_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-layout_tests_py3 - COMMAND python3 -m cmake_format.layout_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-lexer_tests_py3 - COMMAND python3 -m cmake_format.lexer_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-markup_tests_py3 - COMMAND python3 -m cmake_format.markup_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-parser_tests_py3 - COMMAND python3 -m cmake_format.parser_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + foreach(_testname ${_testnames}) + add_test( + NAME cmake_format-${_testname}_py3 + COMMAND python3 -Bm cmake_format.${_testname} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + endforeach() endif() add_subdirectory(doc) diff --git a/cmake_format/__init__.py b/cmake_format/__init__.py index f3ec111..5bed871 100644 --- a/cmake_format/__init__.py +++ b/cmake_format/__init__.py @@ -3,4 +3,4 @@ """ from __future__ import unicode_literals -VERSION = '0.5.5' +VERSION = '0.6.0' diff --git a/cmake_format/__main__.py b/cmake_format/__main__.py index 943ed6d..f37dc22 100644 --- a/cmake_format/__main__.py +++ b/cmake_format/__main__.py @@ -421,7 +421,7 @@ def setup_argparser(arg_parser): 'Default is stdout.') arg_parser.add_argument( - '-c', '--config-file', '--config-files', nargs='+', + '-c', '--config-file', '--config-files', '--config', nargs='+', help='path to configuration file(s)') arg_parser.add_argument('infilepaths', nargs='*') add_config_options(arg_parser) diff --git a/cmake_format/command_tests/BUILD b/cmake_format/command_tests/BUILD new file mode 100644 index 0000000..3293020 --- /dev/null +++ b/cmake_format/command_tests/BUILD @@ -0,0 +1,163 @@ +package(default_visibility=["//visibility:public"]) + +py_library( + name="command_tests", + srcs=[ + "__init__.py", + "add_custom_command_tests.py", + "add_executable_tests.py", + "add_library_tests.py", + "conditional_tests.py", + "export_tests.py", + "file_tests.py", + "install_tests.py", + "misc_tests.py", + "set_tests.py"], + data=[ + "conditional_tests.cmake", + "misc_tests.cmake"]) + +# -- Python 2 -- + +py_test( + name="add_custom_command_tests", + srcs=["add_custom_command_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="add_executable_tests", + srcs=["add_executable_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="add_library_tests", + srcs=["add_library_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="conditional_tests", + srcs=["conditional_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="export_tests", + srcs=["export_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="file_tests", + srcs=["file_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="install_tests", + srcs=["install_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +py_test( + name="misc_tests", + srcs=["misc_tests.py"], + deps=[ + "//cmake_format:cmake_format", + "//cmake_format:testdata", + ":command_tests"], + python_version="PY2", + ) + +py_test( + name="set_tests", + srcs=["set_tests.py"], + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY2", + ) + +# -- Python 3 -- + +py_test( + name="add_custom_command_tests_py3", + srcs=["add_custom_command_tests.py"], + main="add_custom_command_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="add_executable_tests_py3", + srcs=["add_executable_tests.py"], + main="add_executable_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="add_library_tests_py3", + srcs=["add_library_tests.py"], + main="add_library_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="conditional_tests_py3", + srcs=["conditional_tests.py"], + main="conditional_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="export_tests_py3", + srcs=["export_tests.py"], + main="export_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="file_tests_py3", + srcs=["file_tests.py"], + main="file_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="install_tests_py3", + srcs=["install_tests.py"], + main="install_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) + +py_test( + name="misc_tests_py3", + srcs=["misc_tests.py"], + main="misc_tests.py", + deps=[ + "//cmake_format:cmake_format", + "//cmake_format:testdata", + ":command_tests"], + python_version="PY3", + ) + +py_test( + name="set_tests_py3", + srcs=["set_tests.py"], + main="set_tests.py", + deps=["//cmake_format:cmake_format", ":command_tests"], + python_version="PY3", + ) \ No newline at end of file diff --git a/cmake_format/command_tests/CMakeLists.txt b/cmake_format/command_tests/CMakeLists.txt index 1c2ceb2..ea1e6b0 100644 --- a/cmake_format/command_tests/CMakeLists.txt +++ b/cmake_format/command_tests/CMakeLists.txt @@ -1,53 +1,30 @@ set(MODPREFIX cmake_format.command_tests) -add_test(NAME cmake_format-add_custom_command_tests - COMMAND python -m ${MODPREFIX}.add_custom_command_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-add_executable_tests - COMMAND python -m ${MODPREFIX}.add_executable_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-add_library_tests - COMMAND python -m ${MODPREFIX}.add_library_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-conditional_tests - COMMAND python -m ${MODPREFIX}.conditional_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-export_tests - COMMAND python -m ${MODPREFIX}.export_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-file_tests - COMMAND python -m ${MODPREFIX}.file_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-install_tests - COMMAND python -m ${MODPREFIX}.install_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -add_test(NAME cmake_format-set_tests - COMMAND python -m ${MODPREFIX}.set_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +set( + _testnames + add_custom_command_tests + add_executable_tests + add_library_tests + conditional_tests + export_tests + file_tests + install_tests + misc_tests + set_tests +) + +foreach(_testname ${_testnames}) + add_test( + NAME cmake_format-${_testname} + COMMAND python -Bm ${MODPREFIX}.${_testname} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +endforeach() if(NOT IS_TRAVIS_CI) - add_test(NAME cmake_format-add_custom_command_tests_py3 - COMMAND python3 -m ${MODPREFIX}.add_custom_command_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-add_executable_tests_py3 - COMMAND python3 -m ${MODPREFIX}.add_executable_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-add_library_tests_py3 - COMMAND python3 -m ${MODPREFIX}.add_library_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-conditional_tests_py3 - COMMAND python3 -m ${MODPREFIX}.conditional_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-export_tests_py3 - COMMAND python3 -m ${MODPREFIX}.export_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-file_tests_py3 - COMMAND python3 -m ${MODPREFIX}.file_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-install_tests_py3 - COMMAND python3 -m ${MODPREFIX}.install_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) - add_test(NAME cmake_format-set_tests_py3 - COMMAND python3 -m ${MODPREFIX}.set_tests - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + foreach(_testname ${_testnames}) + add_test( + NAME cmake_format-${_testname}_py3 + COMMAND python -Bm ${MODPREFIX}.${_testname} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + endforeach() endif() diff --git a/cmake_format/command_tests/__init__.py b/cmake_format/command_tests/__init__.py index ca665b0..87c2dc0 100644 --- a/cmake_format/command_tests/__init__.py +++ b/cmake_format/command_tests/__init__.py @@ -2,8 +2,12 @@ # pylint: disable=R1708 from __future__ import unicode_literals +import contextlib import difflib +import functools +import inspect import io +import os import sys import unittest @@ -15,6 +19,86 @@ from cmake_format import parse_funs from cmake_format.parser import NodeType +# NOTE(josh): backport from functools.py in python 3.6 so that we can use it in +# python 2.7 +if sys.version_info < (3, 5, 0): + # pylint: disable=all + class partialmethod(object): + """Method descriptor with partial application of the given arguments + and keywords. + + Supports wrapping existing descriptors and handles non-descriptor + callables as instance methods. + """ + + def __init__(self, func, *args, **keywords): + if not callable(func) and not hasattr(func, "__get__"): + raise TypeError("{!r} is not callable or a descriptor" + .format(func)) + + # func could be a descriptor like classmethod which isn't callable, + # so we can't inherit from partial (it verifies func is callable) + if isinstance(func, partialmethod): + # flattening is mandatory in order to place cls/self before all + # other arguments + # it's also more efficient since only one function will be called + self.func = func.func + self.args = func.args + args + self.keywords = func.keywords.copy() + self.keywords.update(keywords) + else: + self.func = func + self.args = args + self.keywords = keywords + + def __repr__(self): + args = ", ".join(map(repr, self.args)) + keywords = ", ".join("{}={!r}".format(k, v) + for k, v in self.keywords.items()) + format_string = "{module}.{cls}({func}, {args}, {keywords})" + return format_string.format(module=self.__class__.__module__, + cls=self.__class__.__qualname__, + func=self.func, + args=args, + keywords=keywords) + + def _make_unbound_method(self): + def _method(*args, **keywords): + call_keywords = self.keywords.copy() + call_keywords.update(keywords) + cls_or_self = args[0] + rest = args[:] + call_args = (cls_or_self,) + self.args + tuple(rest) + return self.func(*call_args, **call_keywords) + _method.__isabstractmethod__ = self.__isabstractmethod__ + _method._partialmethod = self + return _method + + def __get__(self, obj, cls): + get = getattr(self.func, "__get__", None) + result = None + if get is not None: + new_func = get(obj, cls) + if new_func is not self.func: + # Assume __get__ returning something new indicates the + # creation of an appropriate callable + result = functools.partial(new_func, *self.args, **self.keywords) + try: + result.__self__ = new_func.__self__ + except AttributeError: + pass + if result is None: + # If the underlying descriptor didn't do anything, treat this + # like an instance method + result = self._make_unbound_method().__get__(obj, cls) + return result + + @property + def __isabstractmethod__(self): + return getattr(self.func, "__isabstractmethod__", False) +else: + from functools import partialmethod + def strip_indent(content, indent=6): """ @@ -150,7 +234,7 @@ def assert_layout_tree(test, nodes, tups, tree=None, history=None): assert_layout_tree(test, node.children, expect_children, tree, subhistory) -def assert_layout(test, input_str, expect_tree, strip_len=6): +def assert_layout(test, input_str, expect_tree, strip_len=0): """ Run the formatter on the input string and assert that the result matches the output string @@ -163,11 +247,13 @@ def assert_layout(test, input_str, expect_tree, strip_len=6): assert_layout_tree(test, [box_tree], expect_tree) -def assert_format(test, input_str, output_str, strip_len=0): +def assert_format(test, input_str, output_str=None, strip_len=0): """ Run the formatter on the input string and assert that the result matches the output string """ + if output_str is None: + output_str = input_str input_str = strip_indent(input_str, strip_len) output_str = strip_indent(output_str, strip_len) @@ -217,6 +303,37 @@ class TestBase(unittest.TestCase): Given a bunch of example usages of a particular command, ensure that they lex, parse, layout, and format the same as expected. """ + kNumSidecarTests = 0 + + @classmethod + def load_sidecar_tests(cls): + cmake_sidecar = inspect.getfile(cls)[:-3] + ".cmake" + if not os.path.exists(cmake_sidecar): + return + with io.open(cmake_sidecar, "r", encoding="utf-8") as infile: + lines = infile.read().split("\n") + + test_name = None + line_buffer = [] + num_sidecar_tests = 0 + + for lineno, line in enumerate(lines): + if line.startswith("# cmftest-begin: "): + test_name = line[17:] + line_buffer = [] + elif line.endswith("# cmftest-end"): + if test_name is None: + raise ValueError( + "Malformed sidecar {}:{}".format(cmake_sidecar, lineno)) + test_content = "\n".join(line_buffer) + "\n" + closure = partialmethod(assert_format, test_content) + setattr(cls, test_name, closure) + num_sidecar_tests += 1 + test_name = None + line_buffer = [] + else: + line_buffer.append(line) + setattr(cls, "kNumSidecarTests", num_sidecar_tests) def __init__(self, *args, **kwargs): super(TestBase, self).__init__(*args, **kwargs) @@ -250,34 +367,28 @@ def setUp(self): self.parse_db.update( parse_funs.get_legacy_parse(self.config.fn_spec).kwargs) - for name, value in vars(self).items(): - if callable(value) and name.startswith("test_"): - setattr(self, name, WrapTestWithRunFun(self, value)) + @contextlib.contextmanager + def subTest(self, msg=None, **params): + # pylint: disable=no-member + if sys.version_info < (3, 4, 0): + yield None + else: + yield super(TestBase, self).subTest(msg=msg, **params) def assertExpectations(self): # Empty source_str is shorthand for "assertInvariant" if self.source_str is None: self.source_str = self.expect_format - if sys.version_info < (3, 0, 0): - if self.expect_lex is not None: + if self.expect_lex is not None: + with self.subTest(phase="lex"): # pylint: disable=no-member assert_lex(self, self.source_str, self.expect_lex) - if self.expect_parse is not None: + if self.expect_parse is not None: + with self.subTest(phase="parse"): # pylint: disable=no-member assert_parse(self, self.source_str, self.expect_parse) - if self.expect_layout is not None: + if self.expect_layout is not None: + with self.subTest(phase="layout"): # pylint: disable=no-member assert_layout(self, self.source_str, self.expect_layout) - if self.expect_format is not None: + if self.expect_format is not None: + with self.subTest(phase="format"): # pylint: disable=no-member assert_format(self, self.source_str, self.expect_format) - else: - if self.expect_lex is not None: - with self.subTest(phase="lex"): # pylint: disable=no-member - assert_lex(self, self.source_str, self.expect_lex) - if self.expect_parse is not None: - with self.subTest(phase="parse"): # pylint: disable=no-member - assert_parse(self, self.source_str, self.expect_parse) - if self.expect_layout is not None: - with self.subTest(phase="layout"): # pylint: disable=no-member - assert_layout(self, self.source_str, self.expect_layout) - if self.expect_format is not None: - with self.subTest(phase="format"): # pylint: disable=no-member - assert_format(self, self.source_str, self.expect_format) diff --git a/cmake_format/command_tests/__main__.py b/cmake_format/command_tests/__main__.py new file mode 100644 index 0000000..6347291 --- /dev/null +++ b/cmake_format/command_tests/__main__.py @@ -0,0 +1,29 @@ +import unittest + +# pylint: disable=unused-import +from cmake_format.command_tests.add_custom_command_tests \ + import TestAddCustomCommand +from cmake_format.command_tests.add_executable_tests \ + import TestAddExecutableCommand +from cmake_format.command_tests.add_library_tests \ + import TestAddLibraryCommand +from cmake_format.command_tests.conditional_tests \ + import TestConditionalCommands +from cmake_format.command_tests.export_tests \ + import TestExportCommand +from cmake_format.command_tests.file_tests \ + import TestFileCommands +from cmake_format.command_tests.install_tests \ + import TestInstallCommands +from cmake_format.command_tests.misc_tests \ + import TestMiscFormatting +from cmake_format.command_tests.set_tests \ + import TestSetCommand + + +def main(): + unittest.main() + + +if __name__ == "__main__": + main() diff --git a/cmake_format/command_tests/add_custom_command_tests.py b/cmake_format/command_tests/add_custom_command_tests.py index f11bfe3..d40582f 100644 --- a/cmake_format/command_tests/add_custom_command_tests.py +++ b/cmake_format/command_tests/add_custom_command_tests.py @@ -28,19 +28,23 @@ def test_single_argument(self): ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ @@ -63,12 +67,13 @@ def test_single_argument(self): ] self.expect_format = """\ -add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/foobar_doc.stamp - COMMAND sphinx-build -M html ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_BINARY_DIR} - COMMAND touch ${CMAKE_CURRENT_BINARY_DIR}/foobar_doc.stamp - DEPENDS ${foobar_docs} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/foobar_doc.stamp + COMMAND sphinx-build -M html ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + COMMAND touch ${CMAKE_CURRENT_BINARY_DIR}/foobar_doc.stamp + DEPENDS ${foobar_docs} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) """ diff --git a/cmake_format/command_tests/add_executable_tests.py b/cmake_format/command_tests/add_executable_tests.py index 4b10da7..0a1f57c 100644 --- a/cmake_format/command_tests/add_executable_tests.py +++ b/cmake_format/command_tests/add_executable_tests.py @@ -74,45 +74,30 @@ def test_all_arguments(self): ] self.expect_format = """\ -add_executable(foobar WIN32 EXCLUDE_FROM_ALL - sourcefile_01.cc - sourcefile_02.cc - sourcefile_03.cc - sourcefile_04.cc - sourcefile_05.cc - sourcefile_06.cc - sourcefile_07.cc) +add_executable( + foobar WIN32 EXCLUDE_FROM_ALL + sourcefile_01.cc + sourcefile_02.cc + sourcefile_03.cc + sourcefile_04.cc + sourcefile_05.cc + sourcefile_06.cc + sourcefile_07.cc) """ def test_sort_arguments(self): self.config.autosort = True - self.source_str = """\ -add_executable(foobar WIN32 sourcefile_04.cc - sourcefile_03.cc sourcefile_01.cc sourcefile_02.cc) -""" - self.expect_format = """\ -add_executable(foobar WIN32 - sourcefile_01.cc - sourcefile_02.cc - sourcefile_03.cc - sourcefile_04.cc) +add_executable(foobar WIN32 sourcefile_01.cc sourcefile_02.cc sourcefile_03.cc + sourcefile_04.cc) """ def test_disable_autosort_with_tag(self): self.config.autosort = True - self.source_str = """\ -add_executable(foobar WIN32 # cmake-format: unsort - sourcefile_04.cc sourcefile_03.cc sourcefile_01.cc sourcefile_02.cc) -""" - self.expect_format = """\ -add_executable(foobar WIN32 - # cmake-format: unsort - sourcefile_04.cc - sourcefile_03.cc - sourcefile_01.cc - sourcefile_02.cc) +add_executable( + foobar WIN32 # cmake-format: unsort + sourcefile_04.cc sourcefile_03.cc sourcefile_01.cc sourcefile_02.cc) """ def test_imported_form(self): diff --git a/cmake_format/command_tests/add_library_tests.py b/cmake_format/command_tests/add_library_tests.py index c10f813..7c41ee7 100644 --- a/cmake_format/command_tests/add_library_tests.py +++ b/cmake_format/command_tests/add_library_tests.py @@ -74,14 +74,15 @@ def test_all_arguments(self): ] self.expect_format = """\ -add_library(foobar STATIC EXCLUDE_FROM_ALL - sourcefile_01.cc - sourcefile_02.cc - sourcefile_03.cc - sourcefile_04.cc - sourcefile_05.cc - sourcefile_06.cc - sourcefile_07.cc) +add_library( + foobar STATIC EXCLUDE_FROM_ALL + sourcefile_01.cc + sourcefile_02.cc + sourcefile_03.cc + sourcefile_04.cc + sourcefile_05.cc + sourcefile_06.cc + sourcefile_07.cc) """ def test_parse_with_concluding_comments(self): @@ -121,11 +122,8 @@ def test_sort_arguments(self): """ self.expect_format = """\ -add_library(foobar STATIC - sourcefile_01.cc - sourcefile_02.cc - sourcefile_03.cc - sourcefile_04.cc) +add_library(foobar STATIC sourcefile_01.cc sourcefile_02.cc sourcefile_03.cc + sourcefile_04.cc) """ def test_disable_autosort_with_tag(self): @@ -136,12 +134,8 @@ def test_disable_autosort_with_tag(self): """ self.expect_format = """\ -add_library(foobar STATIC - # cmake-format: unsort - sourcefile_04.cc - sourcefile_03.cc - sourcefile_01.cc - sourcefile_02.cc) +add_library(foobar STATIC # cmake-format: unsort + sourcefile_04.cc sourcefile_03.cc sourcefile_01.cc sourcefile_02.cc) """ def test_imported_form(self): diff --git a/cmake_format/command_tests/conditional_tests.cmake b/cmake_format/command_tests/conditional_tests.cmake new file mode 100644 index 0000000..acceb1a --- /dev/null +++ b/cmake_format/command_tests/conditional_tests.cmake @@ -0,0 +1,45 @@ +# cmftest-begin: test_complicated_boolean +set(matchme + "_DATA_\\|_CMAKE_\\|INTRA_PRED\\|_COMPILED\\|_HOSTING\\|_PERF_\\|CODER_") +if(("${var}" MATCHES "_TEST_" AND NOT "${var}" MATCHES "${matchme}") + OR (CONFIG_AV1_ENCODER + AND CONFIG_ENCODE_PERF_TESTS + AND "${var}" MATCHES "_ENCODE_PERF_TEST_") + OR (CONFIG_AV1_DECODER + AND CONFIG_DECODE_PERF_TESTS + AND "${var}" MATCHES "_DECODE_PERF_TEST_") + OR (CONFIG_AV1_ENCODER AND "${var}" MATCHES "_TEST_ENCODER_") + OR (CONFIG_AV1_DECODER AND "${var}" MATCHES "_TEST_DECODER_")) + list(APPEND aom_test_source_vars ${var}) +endif() +# cmftest-end + +# cmftest-begin: test_less_complicated_boolean +set(matchme + "_DATA_\\|_CMAKE_\\|INTRA_PRED\\|_COMPILED\\|_HOSTING\\|_PERF_\\|CODER_") +if(("${var}" MATCHES "_TEST_" AND NOT "${var}" MATCHES "${matchme}") + OR (CONFIG_AV1_ENCODER + AND CONFIG_ENCODE_PERF_TESTS + AND "${var}" MATCHES "_ENCODE_PERF_TEST_")) + list(APPEND aom_test_source_vars ${var}) +endif() +# cmftest-end + +# cmftest-begin: test_nested_parens +if((NOT HELLO) OR (NOT EXISTS ${WORLD})) + message(WARNING "something is wrong") + set(foobar FALSE) +endif() +# cmftest-end + +# cmftest-begin: test_negated_single_nested_parens +if(NOT ("" STREQUALS "")) + # pass +endif() +# cmftest-end + +# cmftest-begin: test_conditional_in_if_and_endif +if(SOMETHING AND (NOT SOMETHING_ELSE STREQUAL "")) + # pass +endif(SOMETHING AND (NOT SOMETHING_ELSE STREQUAL "")) +# cmftest-end diff --git a/cmake_format/command_tests/conditional_tests.py b/cmake_format/command_tests/conditional_tests.py index 674e9f5..482e7bc 100644 --- a/cmake_format/command_tests/conditional_tests.py +++ b/cmake_format/command_tests/conditional_tests.py @@ -9,46 +9,16 @@ class TestConditionalCommands(TestBase): """ Test various examples of commands that take conditional statements """ + kNumSidecarTests = 0 - def test_complicated_boolean(self): - self.config.max_subargs_per_line = 10 - self.expect_format = """\ -set(matchme "_DATA_\\|_CMAKE_\\|INTRA_PRED\\|_COMPILED\\|_HOSTING\\|_PERF_\\|CODER_") -if(("${var}" MATCHES "_TEST_" AND NOT "${var}" MATCHES "${matchme}") - OR (CONFIG_AV1_ENCODER - AND CONFIG_ENCODE_PERF_TESTS - AND "${var}" MATCHES "_ENCODE_PERF_TEST_") - OR (CONFIG_AV1_DECODER - AND CONFIG_DECODE_PERF_TESTS - AND "${var}" MATCHES "_DECODE_PERF_TEST_") - OR (CONFIG_AV1_ENCODER AND "${var}" MATCHES "_TEST_ENCODER_") - OR (CONFIG_AV1_DECODER AND "${var}" MATCHES "_TEST_DECODER_")) - list(APPEND aom_test_source_vars ${var}) -endif() -""" + def test_numsidecar(self): + """ + Sanity check to makesure all sidecar tests are run. + """ + self.assertEqual(5, self.kNumSidecarTests) - def test_nested_parens(self): - self.expect_format = """\ -if((NOT HELLO) OR (NOT EXISTS ${WORLD})) - message(WARNING "something is wrong") - set(foobar FALSE) -endif() -""" - - def test_negated_single_nested_parens(self): - self.expect_format = """\ -if(NOT ("" STREQUALS "")) - # pass -endif() -""" - - def test_conditional_in_if_and_endif(self): - self.expect_format = """\ -if(SOMETHING AND (NOT SOMETHING_ELSE STREQUAL "")) - # pass -endif(SOMETHING AND (NOT SOMETHING_ELSE STREQUAL "")) -""" +TestConditionalCommands.load_sidecar_tests() if __name__ == '__main__': unittest.main() diff --git a/cmake_format/command_tests/file_tests.py b/cmake_format/command_tests/file_tests.py index 530738c..8dd71f6 100644 --- a/cmake_format/command_tests/file_tests.py +++ b/cmake_format/command_tests/file_tests.py @@ -46,28 +46,33 @@ def test_file_append(self): """ def test_file_generate_output(self): + # TODO(josh): "file content line three" should probably be on the next line self.expect_format = """\ -file(GENERATE - OUTPUT foobar.baz - CONTENT "file content line one" # - "file content line two" - "file content line three" - CONDITION (FOO AND BAR) OR BAZ) +file( + GENERATE + OUTPUT foobar.baz + CONTENT "file content line one" # + "file content line two" "file content line three" + CONDITION (FOO AND BAR) OR BAZ) """ def test_file_glob(self): self.expect_format = """\ -file(GLOB globout RELATIVE foo/bar/baz "*.py" "*.txt") +file( + GLOB globout + RELATIVE foo/bar/baz + "*.py" "*.txt") """ def test_file_copy(self): self.expect_format = r""" -file(COPY foo bar baz - DESTINATION foobar - FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE - FILES_MATCHING - PATTERN "*.h" - REGEX ".*\\.cc") +file( + COPY foo bar baz + DESTINATION foobar + FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE + FILES_MATCHING + PATTERN "*.h" + REGEX ".*\\.cc") """[1:] def test_file_write_a(self): diff --git a/cmake_format/command_tests/install_tests.py b/cmake_format/command_tests/install_tests.py index 016a487..26c26f4 100644 --- a/cmake_format/command_tests/install_tests.py +++ b/cmake_format/command_tests/install_tests.py @@ -97,11 +97,12 @@ def test_install_targets(self): ] self.expect_format = """\ -install(TARGETS ${PROJECT_NAME} - EXPORT ${CMAKE_PROJECT_NAME}Targets - ARCHIVE DESTINATION lib COMPONENT install-app - LIBRARY DESTINATION lib COMPONENT install-app - RUNTIME DESTINATION bin COMPONENT install-app) +install( + TARGETS ${PROJECT_NAME} + EXPORT ${CMAKE_PROJECT_NAME}Targets + ARCHIVE DESTINATION lib COMPONENT install-app + LIBRARY DESTINATION lib COMPONENT install-app + RUNTIME DESTINATION bin COMPONENT install-app) """ def test_kwarg_match_consumes(self): @@ -109,8 +110,8 @@ def test_kwarg_match_consumes(self): install(TARGETS myprog RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime) """ self.expect_format = """\ -install(TARGETS myprog - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime) +install(TARGETS myprog RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT runtime) """ diff --git a/cmake_format/command_tests/misc_tests.cmake b/cmake_format/command_tests/misc_tests.cmake new file mode 100644 index 0000000..440612a --- /dev/null +++ b/cmake_format/command_tests/misc_tests.cmake @@ -0,0 +1,18 @@ +# cmftest-begin: test_nonargument_terminal_comments +add_library( + foo + # This comment is not attached to an argument + bar.cc foo.cc) + +find_package( + foobar REQUIRED + COMPONENTS some_component # some_other_component + # This is a very long comment, and actually the second comment in + # this row. +) +# cmftest-end + +# cmftest-begin: test_arg_just_fits_two +message( + FATAL_ERROR "81 character line ----------------------------------------") +# cmftest-end diff --git a/cmake_format/command_tests/misc_tests.py b/cmake_format/command_tests/misc_tests.py new file mode 100644 index 0000000..99a5cc2 --- /dev/null +++ b/cmake_format/command_tests/misc_tests.py @@ -0,0 +1,1245 @@ +# -*- coding: utf-8 -*- +# pylint: disable=bad-continuation +# pylint: disable=too-many-lines +from __future__ import unicode_literals + +import io +import os +import unittest + +from cmake_format import configuration +from cmake_format.command_tests import assert_format, TestBase + + +class TestMiscFormatting(TestBase): + """ + Ensure that various inputs format the way we want them to + """ + + def test_numsidecar(self): + """ + Sanity check to makesure all sidecar tests are run. + """ + self.assertEqual(2, self.kNumSidecarTests) + + def test_collapse_additional_newlines(self): + self.source_str = """\ +# The following multiple newlines should be collapsed into a single newline + + + + +cmake_minimum_required(VERSION 2.8.11) +project(cmake_format_test) +""" + + self.expect_format = """\ +# The following multiple newlines should be collapsed into a single newline + +cmake_minimum_required(VERSION 2.8.11) +project(cmake_format_test) +""" + + def test_multiline_reflow(self): + self.source_str = """\ +# This multiline-comment should be reflowed +# into a single comment +# on one line +""" + self.expect_format = """\ +# This multiline-comment should be reflowed into a single comment on one line +""" + + def test_comment_before_command(self): + self.expect_format = """\ +# This comment should remain right before the command call. Furthermore, the +# command call should be formatted to a single line. +add_subdirectories(foo bar baz foo2 bar2 baz2) +""" + + def test_long_args_command_split(self): + self.source_str = """\ +# This very long command should be split to multiple lines +set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) +""" + self.expect_format = """\ +# This very long command should be split to multiple lines +set(HEADERS very_long_header_name_a.h very_long_header_name_b.h + very_long_header_name_c.h) +""" + + def test_lots_of_args_command_split(self): + self.source_str = """\ +# This command should be split into one line per entry because it has a long +# argument list. +set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc) +""" + self.expect_format = """\ +# This command should be split into one line per entry because it has a long +# argument list. +set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc + source_g.cc) +""" + + def test_string_preserved_during_split(self): + self.source_str = """\ +# The string in this command should not be split +set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") +""" + self.expect_format = """\ +# The string in this command should not be split +set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS + "-std=c++11 -Wall -Wextra") +""" + + def test_long_arg_on_newline(self): + self.source_str = """\ +# This command has a very long argument and can't be aligned with the command +# end, so it should be moved to a new line with block indent + 1. +some_long_command_name("Some very long argument that really needs to be on the next line.") +""" + self.expect_format = """\ +# This command has a very long argument and can't be aligned with the command +# end, so it should be moved to a new line with block indent + 1. +some_long_command_name( + "Some very long argument that really needs to be on the next line.") +""" + + def test_long_kwargarg_on_newline(self): + self.source_str = """\ +# This situation is similar but the argument to a KWARG needs to be on a +# newline instead. +set(CMAKE_CXX_FLAGS "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") +""" + self.expect_format = """\ +# This situation is similar but the argument to a KWARG needs to be on a newline +# instead. +set(CMAKE_CXX_FLAGS + "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") +""" + + def test_argcomment_preserved_and_reflowed(self): + self.expect_format = """\ +set(HEADERS + header_a.h header_b.h # This comment should be preserved, moreover it should + # be split across two lines. + header_c.h header_d.h) +""" + + def test_argcomments_force_reflow(self): + self.config.line_width = 140 + self.source_str = """\ +cmake_parse_arguments(ARG + "SILENT" # optional keywords + "" # one value keywords + "" # multi value keywords + ${ARGN}) +""" + self.expect_format = """\ +cmake_parse_arguments( + ARG + "SILENT" # optional keywords + "" # one value keywords + "" # multi value keywords + ${ARGN}) +""" + + def test_format_off(self): + self.source_str = """\ +# This part of the comment should +# be formatted +# but... +# cmake-format: off +# This bunny should remain untouched: +# .   _ ∩ +#   レヘヽ| | +#     (・x・) +#    c( uu} +# cmake-format: on +# while this part should +# be formatted again +""" + self.expect_format = """\ +# This part of the comment should be formatted but... +# cmake-format: off +# This bunny should remain untouched: +# .   _ ∩ +#   レヘヽ| | +#     (・x・) +#    c( uu} +# cmake-format: on +# while this part should be formatted again +""" + + def test_paragraphs_preserved(self): + self.source_str = """\ +# This is a paragraph +# +# This is a second paragraph +# +# This is a third paragraph +""" + self.expect_format = """\ +# This is a paragraph +# +# This is a second paragraph +# +# This is a third paragraph +""" + + def test_todo_preserved(self): + self.source_str = """\ +# This is a comment +# that should be joined but +# TODO(josh): This todo should not be joined with the previous line. +# NOTE(josh): Also this should not be joined with the todo. +""" + self.expect_format = """\ +# This is a comment that should be joined but +# TODO(josh): This todo should not be joined with the previous line. +# NOTE(josh): Also this should not be joined with the todo. +""" + + def test_complex_nested_stuff(self): + self.config.autosort = False + self.expect_format = """\ +if(foo) + if(sbar) + # This comment is in-scope. + add_library( + foo_bar_baz + foo.cc bar.cc # this is a comment for arg2 this is more comment for arg2, + # it should be joined with the first. + baz.cc) # This comment is part of add_library + + other_command( + some_long_argument some_long_argument) # this comment is very long and + # gets split across some lines + + other_command( + some_long_argument some_long_argument some_long_argument) # this comment + # is even longer + # and wouldn't + # make sense to + # pack at the + # end of the + # command so it + # gets it's own + # lines + endif() +endif() +""" + + def test_custom_command(self): + self.expect_format = """\ +# This very long command should be broken up along keyword arguments +foo(nonkwarg_a nonkwarg_b + HEADERS a.h b.h c.h d.h e.h f.h + SOURCES a.cc b.cc d.cc + DEPENDS foo + bar baz) +""" + + def test_always_wrap(self): + self.source_str = """\ +foo(nonkwarg_a HEADERS a.h SOURCES a.cc DEPENDS foo) +""" + + with self.subTest(always_wrap=False): + # assert_format(self, self.source_str) + pass + + self.config.always_wrap = ['foo'] + with self.subTest(always_wrap=True): + assert_format(self, self.source_str, """\ +foo(nonkwarg_a + HEADERS a.h + SOURCES a.cc + DEPENDS foo) +""") + + def test_multiline_string(self): + self.expect_format = """\ +foo(some_arg some_arg " + This string is on multiple lines +") +""" + + def test_some_string_stuff(self): + self.source_str = """\ +# This command uses a string with escaped quote chars +foo(some_arg some_arg "This is a \\"string\\" within a string") + +# This command uses an empty string +foo(some_arg some_arg "") + +# This command uses a multiline string +foo(some_arg some_arg " + This string is on multiple lines +") +""" + self.expect_format = """\ +# This command uses a string with escaped quote chars +foo(some_arg some_arg "This is a \\"string\\" within a string") + +# This command uses an empty string +foo(some_arg some_arg "") + +# This command uses a multiline string +foo(some_arg some_arg " + This string is on multiple lines +") +""" + + def test_format_off_code(self): + self.source_str = """\ +# No, I really want this to look ugly +# cmake-format: off +add_library(a b.cc + c.cc d.cc + e.cc) +# cmake-format: on +""" + self.expect_format = """\ +# No, I really want this to look ugly +# cmake-format: off +add_library(a b.cc + c.cc d.cc + e.cc) +# cmake-format: on +""" + + def test_multiline_statement_comment_idempotent(self): + self.source_str = """\ +set(HELLO hello world!) # TODO(josh): fix this bad code with some change that + # takes mutiple lines to explain +""" + self.expect_format = """\ +set(HELLO hello world!) # TODO(josh): fix this bad code with some change that + # takes mutiple lines to explain +""" + + def test_function_def(self): + self.source_str = """\ +function(forbarbaz arg1) + do_something(arg1 ${ARGN}) +endfunction() +""" + self.expect_format = """\ +function(forbarbaz arg1) + do_something(arg1 ${ARGN}) +endfunction() +""" + + def test_macro_def(self): + self.source_str = """\ +macro(forbarbaz arg1) + do_something(arg1 ${ARGN}) +endmacro() +""" + self.expect_format = """\ +macro(forbarbaz arg1) + do_something(arg1 ${ARGN}) +endmacro() +""" + + def test_foreach(self): + self.config.max_subargs_per_line = 6 + self.source_str = """\ +foreach(forbarbaz arg1 arg2 arg3) + message(hello ${foobarbaz}) +endforeach() +""" + self.expect_format = """\ +foreach(forbarbaz arg1 arg2 arg3) + message(hello ${foobarbaz}) +endforeach() +""" + + def test_while(self): + self.config.max_subargs_per_line = 6 + self.source_str = """\ + +while(forbarbaz arg1 arg2 arg3) + message(hello ${foobarbaz}) +endwhile() +""" + self.expect_format = """\ +while(forbarbaz arg1 arg2 arg3) + message(hello ${foobarbaz}) +endwhile() +""" + + def test_ctrl_space(self): + self.config.separate_ctrl_name_with_space = True + self.source_str = """\ +if(foo) + myfun(foo bar baz) +endif() +""" + self.expect_format = """\ +if (foo) + myfun(foo bar baz) +endif () +""" + + def test_fn_space(self): + self.config.separate_fn_name_with_space = True + self.source_str = """\ +myfun(foo bar baz) +""" + self.expect_format = """\ +myfun (foo bar baz) +""" + + def test_preserve_separator(self): + self.source_str = """\ +# -------------------- +# This is some +# text that I expect +# to reflow +# -------------------- +""" + self.expect_format = """\ +# -------------------- +# This is some text that I expect to reflow +# -------------------- +""" + + self.source_str = """\ +# !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* +# This is some +# text that I expect +# to reflow +# !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* +""" + self.expect_format = """\ +# !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* +# This is some text that I expect to reflow +# !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* +""" + + self.source_str = """\ +# ----Not Supported---- +# This is some +# text that I expect +# to reflow +# ----Not Supported---- +""" + self.expect_format = """\ +# ----Not Supported---- +# This is some text that I expect to reflow +# ----Not Supported---- +""" + + def test_bullets(self): + self.source_str = """\ +# This is a bulleted list: +# +# * item 1 +# * item 2 +# this line gets merged with item 2 +# * item 3 is really long and needs to be wrapped to a second line because it wont all fit on one line without wrapping. +# +# But the list has ended and this line is free. And +# * this is not a bulleted list +# * and it will be +# * merged +""" + self.expect_format = """\ +# This is a bulleted list: +# +# * item 1 +# * item 2 this line gets merged with item 2 +# * item 3 is really long and needs to be wrapped to a second line because it +# wont all fit on one line without wrapping. +# +# But the list has ended and this line is free. And * this is not a bulleted +# list * and it will be * merged +""" + + def test_enum_lists(self): + self.source_str = """\ +# This is a bulleted list: +# +# 1. item +# 2. item +# 3. item +# +# 4. item +# 5. item +# 6. item +# +# 1. item +# 3. item +# 5. item +# 6. item +# 6. item is really long and needs to be wrapped to a second line because it wont all fit on one line without wrapping. +# 7. item +# 9. item +# 9. item +# 9. item +# 9. item +# 9. item +# +""" + self.expect_format = """\ +# This is a bulleted list: +# +# 1. item +# 2. item +# 3. item +# +# 1. item +# 2. item +# 3. item +# +# 1. item +# 2. item +# 3. item +# 4. item +# 5. item is really long and needs to be wrapped to a second line because it wont +# all fit on one line without wrapping. +# 6. item +# 7. item +# 8. item +# 9. item +# 10. item +# 11. item +# +""" + + def test_nested_bullets(self): + self.source_str = """\ +# This is a bulleted list: +# +# * item 1 +# * item 2 +# +# * item 3 +# * item 4 +# +# * item 5 +# * item 6 +# +# * item 7 +# * item 8 +""" + self.expect_format = """\ +# This is a bulleted list: +# +# * item 1 +# * item 2 +# +# * item 3 +# * item 4 +# +# * item 5 +# * item 6 +# +# * item 7 +# * item 8 +""" + + def test_comment_fence(self): + self.source_str = """\ +# ~~~~~~ +# This is some +# verbatim text +# that should not be +# formatted +# ``````` +""" + self.expect_format = """\ +# ~~~ +# This is some +# verbatim text +# that should not be +# formatted +# ~~~ +""" + + def test_bracket_comments(self): + + self.source_str = """\ +# [[This is a bracket comment. +It is preserved verbatim, but trailing whitespace is removed. +So things like --this-- Are fine:]] +""" + self.expect_format = """\ +# [[This is a bracket comment. +It is preserved verbatim, but trailing whitespace is removed. +So things like --this-- Are fine:]] +""" + + self.source_str = """\ +if(foo) + # [==[This is a bracket comment at some nested level + # it is preserved verbatim, but trailing + # whitespace is removed.]==] +endif() +""" + self.expect_format = """\ +if(foo) + # [==[This is a bracket comment at some nested level + # it is preserved verbatim, but trailing + # whitespace is removed.]==] +endif() +""" + + # Make sure bracket comments are kept inline in their function call + self.source_str = """\ +message("First Argument" #[[Bracket Comment]] "Second Argument") +""" + self.expect_format = """\ +message("First Argument" #[[Bracket Comment]] "Second Argument") +""" + + def test_comment_after_command(self): + self.source_str = """\ +foo_command() # comment +""" + self.expect_format = """\ +foo_command() # comment +""" + + self.source_str = """\ +foo_command() # this is a long comment that exceeds the desired page width and will be wrapped to a newline +""" + self.expect_format = """\ +foo_command() # this is a long comment that exceeds the desired page width and + # will be wrapped to a newline +""" + + def test_arg_just_fits(self): + """ + Ensure that if an argument *just* fits that it isn't superfluously wrapped +""" + + self.source_str = """\ +message(FATAL_ERROR "81 character line ----------------------------------------") +""" + self.expect_format = """\ +message( + FATAL_ERROR "81 character line ----------------------------------------") +""" + with self.subTest(): + assert_format(self, self.source_str, self.expect_format) + + self.source_str = """\ +message(FATAL_ERROR + "100 character line ----------------------------------------------------------" +) # Closing parenthesis is indented one space! +""" + + self.expect_format = """\ +message( + FATAL_ERROR + "100 character line ----------------------------------------------------------" +) # Closing parenthesis is indented one space! +""" + with self.subTest(): + assert_format(self, self.source_str, self.expect_format) + + self.source_str = """\ +message( + "100 character line ----------------------------------------------------------------------" +) # Closing parenthesis is indented one space! +""" + + self.expect_format = """\ +message( + "100 character line ----------------------------------------------------------------------" +) # Closing parenthesis is indented one space! +""" + + with self.subTest(): + assert_format(self, self.source_str, self.expect_format) + self.source_str = self.expect_format = None + + def test_dangle_parens(self): + self.config.dangle_parens = True + self.config.max_subargs_per_line = 6 + + with self.subTest(): + assert_format(self, """\ +foo_command() +foo_command(arg1) +foo_command(arg1) # comment +""", """\ +foo_command() +foo_command(arg1) +foo_command(arg1) # comment +""") + + with self.subTest(): + assert_format(self, """\ +some_long_command_name(longargname longargname longargname longargname longargname) +""", """\ +some_long_command_name( + longargname longargname longargname longargname longargname +) +""") + + with self.subTest(): + assert_format(self, """\ +if(foo) + some_long_command_name(longargname longargname longargname longargname longargname) +endif() +""", """\ +if(foo) + some_long_command_name( + longargname longargname longargname longargname longargname + ) +endif() +""") + + with self.subTest(): + assert_format(self, """\ +some_long_command_name(longargname longargname longargname longargname longargname longargname longargname longargname) +""", """\ +some_long_command_name( + longargname + longargname + longargname + longargname + longargname + longargname + longargname + longargname +) +""") + + with self.subTest(): + assert_format(self, """\ +target_include_directories(target INTERFACE $) +""", """\ +target_include_directories( + target INTERFACE $ +) +""") + + def test_windows_line_endings_input(self): + self.source_str = ( + "#[[*********************************************\r\n" + "* Information line 1\r\n" + "* Information line 2\r\n" + "************************************************]]\r\n") + + self.expect_format = """\ +#[[********************************************* +* Information line 1 +* Information line 2 +************************************************]] +""" + + def test_windows_line_endings_output(self): + config_dict = self.config.as_dict() + config_dict['line_ending'] = 'windows' + self.config = configuration.Configuration(**config_dict) + + self.source_str = """\ +#[[********************************************* +* Information line 1 +* Information line 2 +************************************************]]""" + + self.expect_format = ( + "#[[*********************************************\r\n" + "* Information line 1\r\n" + "* Information line 2\r\n" + "************************************************]]\r\n") + + def test_auto_line_endings(self): + config_dict = self.config.as_dict() + config_dict['line_ending'] = 'auto' + self.config = configuration.Configuration(**config_dict) + + self.source_str = ( + "#[[*********************************************\r\n" + "* Information line 1\r\n" + "* Information line 2\r\n" + "************************************************]]\r\n") + + self.expect_format = ( + "#[[*********************************************\r\n" + "* Information line 1\r\n" + "* Information line 2\r\n" + "************************************************]]\r\n") + + def test_keyword_case(self): + config_dict = self.config.as_dict() + config_dict['keyword_case'] = 'upper' + self.config = configuration.Configuration(**config_dict) + + with self.subTest(): + assert_format(self, """\ +foo(bar baz) +""", """\ +foo(BAR BAZ) +""") + + config_dict = self.config.as_dict() + config_dict['keyword_case'] = 'lower' + self.config = configuration.Configuration(**config_dict) + + with self.subTest(): + assert_format(self, """\ +foo(bar baz) +""", """\ +foo(bar baz) +""") + + config_dict = self.config.as_dict() + config_dict['command_case'] = 'unchanged' + self.config = configuration.Configuration(**config_dict) + with self.subTest(): + assert_format(self, """\ +foo(BaR bAz) +""", """\ +foo(bar baz) +""") + + def test_command_case(self): + with self.subTest(): + assert_format(self, """\ +FOO(bar baz) +""", """\ +foo(bar baz) +""") + + config_dict = self.config.as_dict() + config_dict['command_case'] = 'upper' + self.config = configuration.Configuration(**config_dict) + with self.subTest(): + assert_format(self, """\ +foo(bar baz) +""", """\ +FOO(bar baz) +""") + + config_dict = self.config.as_dict() + config_dict['command_case'] = 'unchanged' + self.config = configuration.Configuration(**config_dict) + with self.subTest(): + assert_format(self, """\ +FoO(bar baz) +""", """\ +FoO(bar baz) +""") + + def test_comment_in_statement(self): + self.expect_format = """\ +add_library(foo # This comment is not attached to an argument + bar.cc foo.cc) +""" + + def test_comment_at_end_of_statement(self): + with self.subTest(): + assert_format(self, """\ +add_library(foo bar.cc foo.cc # This comment is not attached to an argument +) +""") + + with self.subTest(): + assert_format(self, """\ +target_link_libraries( + libraryname PUBLIC ${COMMON_LIBRARIES} # add more library dependencies here +) +""") + + with self.subTest(): + assert_format(self, """\ +find_package( + foobar REQUIRED + COMPONENTS some_component # some_other_component + # This is a very long comment, and actually the second comment in + # this row. +) +""", """\ +find_package( + foobar REQUIRED + COMPONENTS some_component # some_other_component + # This is a very long comment, and actually the second comment in + # this row. +) +""") + + def test_comment_in_kwarg(self): + self.source_str = """\ +install(TARGETS foob + ARCHIVE DESTINATION foobar + # this is a line comment, not a comment on foobar + COMPONENT baz) +""" + self.expect_format = """\ +install( + TARGETS foob + ARCHIVE DESTINATION foobar # this is a line comment, not a comment on foobar + COMPONENT baz) +""" + + def test_algoorder_preference(self): + self.config.max_subargs_per_line = 10 + self.source_str = """\ +some_long_command_name(longargument longargument longargument longargument + longargument longargument) +""" + self.expect_format = """\ +some_long_command_name(longargument longargument longargument longargument + longargument longargument) +""" + + def test_elseif(self): + self.expect_format = """\ +if(MSVC) + +elseif( + (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + OR CMAKE_COMPILER_IS_GNUCC + OR CMAKE_COMPILER_IS_GNUCXX) + +endif() +""" + + def test_elseif_else_control_space(self): + self.config.separate_ctrl_name_with_space = True + self.source_str = """\ +if(foo) +elseif(bar) +else() +endif() +""" + self.expect_format = """\ +if (foo) + +elseif (bar) + +else () + +endif () +""" + + def test_disable_markup(self): + self.config.enable_markup = False + self.source_str = """\ +# don't reflow +# or parse markup +# for these lines +""" + self.expect_format = """\ +# don't reflow +# or parse markup +# for these lines +""" + + def test_literal_first_comment(self): + self.source_str = """\ +# This comment +# is reflowed + +# This comment +# is reflowed +""" + self.expect_format = """\ +# This comment is reflowed + +# This comment is reflowed +""" + + self.config.first_comment_is_literal = True + self.source_str = """\ +# This comment +# is not reflowed + +# This comment +# is reflowed +""" + self.expect_format = """\ +# This comment +# is not reflowed + +# This comment is reflowed +""" + + def test_shebang_preserved(self): + self.source_str = """\ +#!/usr/bin/cmake -P +""" + self.expect_format = """\ +#!/usr/bin/cmake -P +""" + + def test_preserve_copyright(self): + self.source_str = """\ +# Copyright 2018: Josh Bialkowski +# This text should not be reflowed +# because it's a copyright +""" + self.expect_format = """\ +# Copyright 2018: Josh Bialkowski This text should not be reflowed because it's +# a copyright +""" + + self.config.literal_comment_pattern = " Copyright.*" + self.source_str = """\ +# Copyright 2018: Josh Bialkowski +# This text should not be reflowed +# because it's a copyright +""" + self.expect_format = """\ +# Copyright 2018: Josh Bialkowski +# This text should not be reflowed +# because it's a copyright +""" + + def test_kwarg_match_consumes(self): + self.source_str = """\ +add_test(NAME myTestName COMMAND testCommand --run_test=@quick) +""" + self.expect_format = """\ +add_test(NAME myTestName COMMAND testCommand --run_test=@quick) +""" + + def test_byte_order_mark(self): + self.source_str = """\ +\ufeffcmake_minimum_required(VERSION 2.8.11) +project(cmake_format_test) +""" + self.expect_format = """\ +cmake_minimum_required(VERSION 2.8.11) +project(cmake_format_test) +""" + + self.config.emit_byteorder_mark = True + self.source_str = """\ +cmake_minimum_required(VERSION 2.8.11) +project(cmake_format_test) +""" + self.expect_format = """\ +\ufeffcmake_minimum_required(VERSION 2.8.11) +project(cmake_format_test) +""" + + def test_percommand_override(self): + self.source_str = """\ +FoO(bar baz) +""" + self.expect_format = """\ +foo(bar baz) +""" + + self.config.per_command["foo"] = { + "command_case": "unchanged" + } + self.source_str = """\ +FoO(bar baz) +""" + self.expect_format = """\ +FoO(bar baz) +""" + + def test_quoted_assignment_literal(self): + self.source_str = """\ +target_compile_definitions(foo PUBLIC BAR="Quoted String" BAZ_______________________Z) +""" + self.expect_format = """\ +target_compile_definitions(foo PUBLIC BAR="Quoted String" + BAZ_______________________Z) +""" + + def test_keyword_comment(self): + self.source_str = """\ +find_package(package REQUIRED + COMPONENTS # -------------------------------------- + # @TODO: This has to be filled manually + # -------------------------------------- + this_is_a_really_long_word_foo) +""" + + self.expect_format = """\ +find_package( + package REQUIRED + COMPONENTS # -------------------------------------- + # @TODO: This has to be filled manually + # -------------------------------------- + this_is_a_really_long_word_foo) +""" + + def test_example_file(self): + thisdir = os.path.dirname(__file__) + infile_path = os.path.join(thisdir, '..', 'test', 'test_in.cmake') + outfile_path = os.path.join(thisdir, '..', 'test', 'test_out.cmake') + + with io.open(infile_path, 'r', encoding='utf8') as infile: + infile_text = infile.read() + with io.open(outfile_path, 'r', encoding='utf8') as outfile: + outfile_text = outfile.read() + + self.source_str = infile_text + self.expect_format = outfile_text + + def test_one_char_short_hpack_rparen_case(self): + # This was a particularly rare edge case. The situation is that the + # the arguments are one character shy of fitting in the configured line + # width, the statement column is the same as the indent column, and + # all the arguments are positional. The problem was that the hpack if + # possible logic did not account for the final paren. A fix is in place. + self.config.line_width = 132 + self.config.tab_size = 4 + self.source_str = """ +set(cubepp_HDRS + ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/AppDelegate.h + ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/DemoViewController.h) +""" + self.expect_format = """\ +set(cubepp_HDRS ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/AppDelegate.h + ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/DemoViewController.h) +""" + + def test_layout_passes(self): + self.config.layout_passes = { + "StatementNode": [(0, True)], + "ArgGroupNode": [(0, True)], + "PargGroupNode": [(0, True)], + } + self.expect_format = """\ +add_library( + foobar + STATIC + sourcefile_01.cc + sourcefile_02.cc) +""" + + def test_rulers_preserved_without_markup(self): + self.config.enable_markup = False + self.source_str = """ +######################################################################### +# Custom targets +######################################################################### +""" + self.expect_format = """\ +######################################################################### +# Custom targets +######################################################################### +""" + + def test_canonical_spelling(self): + self.expect_format = """\ +ExternalProject_Add( + foobar + URL https://foobar.baz/latest.tar.gz + TLS_VERIFY TRUE + CONFIGURE_COMMAND configure + BUILD_COMMAND make + INSTALL_COMMAND make install) +""" + + def test_comment_hashrulers(self): + self.config.line_width = 74 + with self.subTest(): + assert_format(self, """ +################## +# This comment has a long block before it. +############# +""", """\ +# ######################################################################## +# This comment has a long block before it. +# ######################################################################## +""") + + with self.subTest(): + assert_format(self, """ +############################################### +# This is a section in the CMakeLists.txt file. +############ +# This stuff below here +# should get re-flowed like +# normal comments. Across multiple +# lines and +# beyond. +""", """\ +# ######################################################################## +# This is a section in the CMakeLists.txt file. +# ######################################################################## +# This stuff below here should get re-flowed like normal comments. Across +# multiple lines and beyond. +""") + + # verify the original behavior is as described (they truncate to one #) + self.config.hashruler_min_length = 1000 + with self.subTest(): + assert_format(self, """ +########################################################################## +# This comment has a long block before it. +########################################################################## +""", """\ +# +# This comment has a long block before it. +# +""") + + with self.subTest(): + assert_format(self, """ +########################################################################## +# This is a section in the CMakeLists.txt file. +########################################################################## +# This stuff below here +# should get re-flowed like +# normal comments. Across multiple +# lines and +# beyond. +""", """\ +# +# This is a section in the CMakeLists.txt file. +# +# This stuff below here should get re-flowed like normal comments. Across +# multiple lines and beyond. +""") + + # make sure changing hashruler_min_length works correctly + # self.config.enable_markup = False + for min_width in {3, 5, 7, 9}: + self.config.hashruler_min_length = min_width + + # NOTE(josh): these tests use short rulers that wont be picked up by + # the default pattern + self.config.ruler_pattern = (r'#{%d}#*' % (min_width - 1)) + + just_shy = '#' * (min_width - 1) + just_right = '#' * min_width + longer = '#' * (min_width + 2) + full_line = '#' * (self.config.line_width - 2) + + assert_format(self, """ +# A comment: min_width={min_width}, just_shy +{just_shy} +""".format(min_width=min_width, just_shy=just_shy), + """\ +# A comment: min_width={min_width}, just_shy +# +""".format(min_width=min_width)) + + assert_format(self, """ +# A comment: min_width={min_width}, just_right +{just_right} +""".format(min_width=min_width, just_right=just_right), + """\ +# A comment: min_width={min_width}, just_right +# {full_line} +""".format(min_width=min_width, full_line=full_line)) + + assert_format(self, """ +# A comment: min_width={min_width}, longer +{longer} +""".format(min_width=min_width, longer=longer), + """\ +# A comment: min_width={min_width}, longer +# {full_line} +""".format(min_width=min_width, full_line=full_line)) + + +TestMiscFormatting.load_sidecar_tests() + +if __name__ == '__main__': + unittest.main() diff --git a/cmake_format/command_tests/set_tests.py b/cmake_format/command_tests/set_tests.py index c66bdf6..c73178f 100644 --- a/cmake_format/command_tests/set_tests.py +++ b/cmake_format/command_tests/set_tests.py @@ -59,38 +59,25 @@ def test_tag_respected_if_autosort_enabled(self): def test_bracket_comment_is_not_trailing_comment_of_kwarg(self): self.config.autosort = True - self.config.max_subargs_per_line = 3 - self.source_str = """\ -set(SOURCES #[[cmf:sortable]] foo.cc bar.cc baz.cc) -""" self.expect_format = """\ -set(SOURCES - #[[cmf:sortable]] - bar.cc baz.cc foo.cc) +set(SOURCES #[[cmf:sortable]] # + bar.cc baz.cc foo.cc) """ def test_bracket_comment_short_tag(self): self.config.autosort = True - self.source_str = self.expect_format = """\ -set(SOURCES - #[[cmf:sort]] - bar.cc baz.cc foo.cc) + self.expect_format = """\ +set(SOURCES #[[cmf:sort]] bar.cc baz.cc foo.cc) """ def test_line_comment_long_tag(self): self.config.autosort = True - self.source_str = self.expect_format = """\ -set(sources - # cmake-format: sortable - bar.cc baz.cc foo.cc) + self.expect_format = """\ +set(sources # cmake-format: sortable + bar.cc baz.cc foo.cc) """ def test_long_args_command_split(self): - self.source_str = """\ -# This very long command should be split to multiple lines -set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) -""" - self.expect_format = """\ # This very long command should be split to multiple lines set(HEADERS very_long_header_name_a.h very_long_header_name_b.h @@ -98,12 +85,7 @@ def test_long_args_command_split(self): """ def test_lots_of_args_command_split(self): - self.source_str = """\ -# This command should be split into one line per entry because it has a long -# argument list. -set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc) -""" - + # TODO(josh): repair this self.expect_format = """\ # This command should be split into one line per entry because it has a long # argument list. @@ -113,7 +95,8 @@ def test_lots_of_args_command_split(self): source_d.cc source_e.cc source_f.cc - source_g.cc) + source_g.cc + source_h.cc) """ diff --git a/cmake_format/configuration.py b/cmake_format/configuration.py index 5f14a84..e733d63 100644 --- a/cmake_format/configuration.py +++ b/cmake_format/configuration.py @@ -160,11 +160,14 @@ def as_odict(self, with_comments=False): return out def __init__(self, line_width=80, tab_size=2, - max_subargs_per_line=3, + max_subgroups_hwrap=2, + max_pargs_hwrap=6, separate_ctrl_name_with_space=False, separate_fn_name_with_space=False, dangle_parens=False, - max_prefix_chars=2, + dangle_align=None, + min_prefix_chars=4, + max_prefix_chars=10, max_lines_hwrap=2, bullet_char=None, enum_char=None, @@ -173,8 +176,6 @@ def __init__(self, line_width=80, tab_size=2, keyword_case=None, additional_commands=None, always_wrap=None, - # TODO(josh): remove algorithm_order - algorithm_order=None, enable_sort=True, autosort=False, enable_markup=True, @@ -188,21 +189,21 @@ def __init__(self, line_width=80, tab_size=2, input_encoding=None, output_encoding=None, per_command=None, + layout_passes=None, **_): # pylint: disable=W0613 # pylint: disable=too-many-locals self.line_width = line_width self.tab_size = tab_size - # TODO(josh): make this conditioned on certain commands / kwargs - # because things like execute_process(COMMAND...) are less readable - # formatted as a single list. In fact... special case COMMAND to break on - # flags the way we do kwargs. - self.max_subargs_per_line = max_subargs_per_line + self.max_subgroups_hwrap = max_subgroups_hwrap + self.max_pargs_hwrap = max_pargs_hwrap self.separate_ctrl_name_with_space = separate_ctrl_name_with_space self.separate_fn_name_with_space = separate_fn_name_with_space self.dangle_parens = dangle_parens + self.dangle_align = get_default(dangle_align, "prefix") + self.min_prefix_chars = min_prefix_chars self.max_prefix_chars = max_prefix_chars self.max_lines_hwrap = max_lines_hwrap @@ -233,7 +234,6 @@ def __init__(self, line_width=80, tab_size=2, }) self.always_wrap = get_default(always_wrap, []) - self.algorithm_order = get_default(algorithm_order, [0, 1, 2, 3, 4]) self.enable_sort = enable_sort self.autosort = autosort self.enable_markup = enable_markup @@ -245,6 +245,8 @@ def __init__(self, line_width=80, tab_size=2, self.hashruler_min_length = hashruler_min_length self.canonicalize_hashrulers = canonicalize_hashrulers + self.layout_passes = get_default(layout_passes, {}) + self.input_encoding = get_default(input_encoding, "utf-8") self.output_encoding = get_default(output_encoding, "utf-8") @@ -307,6 +309,7 @@ def linewidth(self): VARCHOICES = { + "dangle_align": ["prefix", "prefix-indent", "child", "off"], 'line_ending': ['windows', 'unix', 'auto'], 'command_case': ['lower', 'upper', 'canonical', 'unchanged'], 'keyword_case': ['lower', 'upper', 'unchanged'], @@ -315,8 +318,12 @@ def linewidth(self): VARDOCS = { "line_width": "How wide to allow formatted cmake files", "tab_size": "How many spaces to tab for indent", - "max_subargs_per_line": ( - "If arglists are longer than this, break them always"), + "max_subgroups_hwrap": ( + "If an argument group contains more than this many sub-groups " + "(parg or kwarg groups), then force it to a vertical layout. "), + "max_pargs_hwrap": ( + "If a positinal argument group contains more than this many arguments, " + "then force it to a vertical layout. "), "separate_ctrl_name_with_space": ( "If true, separate flow control names from their parentheses with a" " space"), @@ -324,7 +331,12 @@ def linewidth(self): "If true, separate function names from parentheses with a space"), "dangle_parens": ( "If a statement is wrapped to more than one line, than dangle the" - " closing parenthesis on it's own line"), + " closing parenthesis on it's own line."), + "dangle_align": ( + "If the trailing parenthesis must be 'dangled' on it's on line, then" + " align it to this reference: `prefix`: the start of the statement, " + " `prefix-indent`: the start of the statement, plus one indentation " + " level, `child`: align to the column of the arguments"), "max_prefix_chars": ( "If the statement spelling length (including space and parenthesis" " is larger than the tab width by more than this amoung, then" @@ -341,9 +353,6 @@ def linewidth(self): "Format command names consistently as 'lower' or 'upper' case"), "keyword_case": "Format keywords consistently as 'lower' or 'upper' case", "always_wrap": "A list of command names which should always be wrapped", - "algorithm_order": ( - "Specify the order of wrapping algorithms during successive reflow " - "attempts"), "enable_sort": ( "If true, the argument lists which are known to be sortable will be " "sorted lexicographicall"), @@ -384,5 +393,9 @@ def linewidth(self): " anything else"), "per_command": ( "A dictionary containing any per-command configuration overrides." - " Currently only `command_case` is supported.") + " Currently only `command_case` is supported."), + "layout_passes": ( + "A dictionary mapping layout nodes to a list of wrap decisions. See" + " the documentation for more information." + ) } diff --git a/cmake_format/doc/README.rst b/cmake_format/doc/README.rst index 05f4578..7378e87 100644 --- a/cmake_format/doc/README.rst +++ b/cmake_format/doc/README.rst @@ -69,7 +69,7 @@ Usage -i, --in-place -o OUTFILE_PATH, --outfile-path OUTFILE_PATH Where to write the formatted file. Default is stdout. - -c CONFIG_FILE [CONFIG_FILE ...], --config-file CONFIG_FILE [CONFIG_FILE ...], --config-files CONFIG_FILE [CONFIG_FILE ...] + -c CONFIG_FILE [CONFIG_FILE ...], --config-file CONFIG_FILE [CONFIG_FILE ...], --config-files CONFIG_FILE [CONFIG_FILE ...], --config CONFIG_FILE [CONFIG_FILE ...] path to configuration file(s) Formatter Configuration: @@ -78,8 +78,13 @@ Usage --line-width LINE_WIDTH How wide to allow formatted cmake files --tab-size TAB_SIZE How many spaces to tab for indent - --max-subargs-per-line MAX_SUBARGS_PER_LINE - If arglists are longer than this, break them always + --max-subgroups-hwrap MAX_SUBGROUPS_HWRAP + If an argument group contains more than this many sub- + groups (parg or kwarg groups), then force it to a + vertical layout. + --max-pargs-hwrap MAX_PARGS_HWRAP + If a positinal argument group contains more than this + many arguments, then force it to a vertical layout. --separate-ctrl-name-with-space [SEPARATE_CTRL_NAME_WITH_SPACE] If true, separate flow control names from their parentheses with a space @@ -88,7 +93,14 @@ Usage a space --dangle-parens [DANGLE_PARENS] If a statement is wrapped to more than one line, than - dangle the closing parenthesis on it's own line + dangle the closing parenthesis on it's own line. + --dangle-align {prefix,prefix-indent,child,off} + If the trailing parenthesis must be 'dangled' on it's + on line, then align it to this reference: `prefix`: + the start of the statement, `prefix-indent`: the start + of the statement, plus one indentation level, `child`: + align to the column of the arguments + --min-prefix-chars MIN_PREFIX_CHARS --max-prefix-chars MAX_PREFIX_CHARS If the statement spelling length (including space and parenthesis is larger than the tab width by more than @@ -106,9 +118,6 @@ Usage case --always-wrap [ALWAYS_WRAP [ALWAYS_WRAP ...]] A list of command names which should always be wrapped - --algorithm-order [ALGORITHM_ORDER [ALGORITHM_ORDER ...]] - Specify the order of wrapping algorithms during - successive reflow attempts --enable-sort [ENABLE_SORT] If true, the argument lists which are known to be sortable will be sorted lexicographicall @@ -189,8 +198,13 @@ pleasant way. # How many spaces to tab for indent tab_size = 2 - # If arglists are longer than this, break them always - max_subargs_per_line = 3 + # If an argument group contains more than this many sub-groups (parg or kwarg + # groups), then force it to a vertical layout. + max_subgroups_hwrap = 2 + + # If a positinal argument group contains more than this many arguments, then + # force it to a vertical layout. + max_pargs_hwrap = 6 # If true, separate flow control names from their parentheses with a space separate_ctrl_name_with_space = False @@ -199,13 +213,21 @@ pleasant way. separate_fn_name_with_space = False # If a statement is wrapped to more than one line, than dangle the closing - # parenthesis on it's own line + # parenthesis on it's own line. dangle_parens = False + # If the trailing parenthesis must be 'dangled' on it's on line, then align it + # to this reference: `prefix`: the start of the statement, `prefix-indent`: the + # start of the statement, plus one indentation level, `child`: align to the + # column of the arguments + dangle_align = 'prefix' + + min_prefix_chars = 4 + # If the statement spelling length (including space and parenthesis is larger # than the tab width by more than this amoung, then force reject un-nested # layouts. - max_prefix_chars = 2 + max_prefix_chars = 10 # If a candidate layout is wrapped horizontally but it exceeds this many lines, # then reject the layout. @@ -232,9 +254,6 @@ pleasant way. # A list of command names which should always be wrapped always_wrap = [] - # Specify the order of wrapping algorithms during successive reflow attempts - algorithm_order = [0, 1, 2, 3, 4] - # If true, the argument lists which are known to be sortable will be sorted # lexicographicall enable_sort = True @@ -252,6 +271,10 @@ pleasant way. # only `command_case` is supported. per_command = {} + # A dictionary mapping layout nodes to a list of wrap decisions. See the + # documentation for more information. + layout_passes = {} + # -------------------------- # Comment Formatting Options @@ -474,9 +497,9 @@ and you can globally disable sorting by making setting this configuration to Custom Commands --------------- -Due to the fact that cmake is a macro language, `cmake-format` is, by necessity, -a *semantic* source code formatter. In general it tries to make smart -formatting decisions based on the meaning of arguments in an otherwise +Due to the fact that cmake is a macro language, `cmake-format` is, by +necessity, a *semantic* source code formatter. In general it tries to make +smart formatting decisions based on the meaning of arguments in an otherwise unstructured list of arguments in a cmake statement. `cmake-format` can intelligently format your custom commands, but you will need to tell it how to interpret your arguments. @@ -513,8 +536,8 @@ fields: * ``kwargs``: a dictionary mapping keywords to sub-specifications. A sub-specification may be a complete dictionary of ``pargs``, ``flags``, and ``kwargs`` (nested, all the way down). Or, if the keyword argument accepts - only positionals, then it can be simply the ``pargs`` specification (as in the - example above). + only positionals, then it can be simply the ``pargs`` specification (as in + the example above). For the example specification above, the custom command would look somehing like this: @@ -592,12 +615,11 @@ Will turn this: add_subdirectories(foo bar baz foo2 bar2 baz2) - # This very long command should be split to multiple lines + # This very long command should be wrapped set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) - # This command should be split into one line per entry because it has a long - # argument list. - set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc) + # This command should be split into one line per entry because it has a long argument list. + set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc source_h.cc) # The string in this command should not be split set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") @@ -644,7 +666,7 @@ Will turn this: if(sbar) # This comment is in-scope. add_library(foo_bar_baz foo.cc bar.cc # this is a comment for arg2 - # this is more comment for arg2, it should be joined with the first. + # this is more comment for arg2, it should be joined with the first. baz.cc) # This comment is part of add_library other_command(some_long_argument some_long_argument) # this comment is very long and gets split across some lines @@ -692,14 +714,9 @@ into this: # This comment should remain right before the command call. Furthermore, the # command call should be formatted to a single line. - add_subdirectories(foo - bar - baz - foo2 - bar2 - baz2) - - # This very long command should be split to multiple lines + add_subdirectories(foo bar baz foo2 bar2 baz2) + + # This very long command should be wrapped set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) @@ -711,11 +728,12 @@ into this: source_d.cc source_e.cc source_f.cc - source_g.cc) + source_g.cc + source_h.cc) # The string in this command should not be split - set_target_properties(foo bar baz - PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") + set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS + "-std=c++11 -Wall -Wextra") # This command has a very long argument and can't be aligned with the command # end, so it should be moved to a new line with block indent + 1. @@ -728,11 +746,9 @@ into this: "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") set(HEADERS - header_a.h - header_b.h # This comment should be preserved, moreover it should be split - # across two lines. - header_c.h - header_d.h) + header_a.h header_b.h # This comment should be preserved, moreover it should + # be split across two lines. + header_c.h header_d.h) # This part of the comment should be formatted but... # cmake-format: off @@ -757,30 +773,32 @@ into this: if(foo) if(sbar) # This comment is in-scope. - add_library(foo_bar_baz - foo.cc - bar.cc # this is a comment for arg2 this is more comment for - # arg2, it should be joined with the first. - baz.cc) # This comment is part of add_library - - other_command(some_long_argument some_long_argument) # this comment is very - # long and gets split - # across some lines - - other_command(some_long_argument some_long_argument some_long_argument) - # this comment is even longer and wouldn't make sense to pack at the end of - # the command so it gets it's own lines + add_library( + foo_bar_baz + foo.cc bar.cc # this is a comment for arg2 this is more comment for arg2, + # it should be joined with the first. + baz.cc) # This comment is part of add_library + + other_command( + some_long_argument some_long_argument) # this comment is very long and + # gets split across some lines + + other_command( + some_long_argument some_long_argument some_long_argument) # this comment + # is even longer + # and wouldn't + # make sense to + # pack at the + # end of the + # command so it + # gets it's own + # lines endif() endif() # This very long command should be broken up along keyword arguments foo(nonkwarg_a nonkwarg_b - HEADERS a.h - b.h - c.h - d.h - e.h - f.h + HEADERS a.h b.h c.h d.h e.h f.h SOURCES a.cc b.cc d.cc DEPENDS foo bar baz) diff --git a/cmake_format/doc/case_studies.rst b/cmake_format/doc/case_studies.rst index f4ae785..78023e8 100644 --- a/cmake_format/doc/case_studies.rst +++ b/cmake_format/doc/case_studies.rst @@ -20,7 +20,8 @@ Also doesn't look too bad when wrapped horizontally:: foo bar baz foo2 bar2 baz2 foo3 bar3 baz3 foo4 bar4 baz4 foo5 bar5 baz5 foo6 bar6 baz6 foo7 bar7 baz7 foo8 bar8 baz8 foo9 bar9 baz9) -Though probably matches expectations better if it is wrapped vertically:: +Though probably matches expectations better if it is wrapped vertically, +even if it does look like shit:: add_subdirectories( foo @@ -63,6 +64,34 @@ and looks better wrapped vertically, horizontally nested:: very_long_header_name_b.h very_long_header_name_c.h) +also looks pretty good packed after the first argument:: + + set(HEADERS very_long_header_name_a.h + very_long_header_name_b.h + very_long_header_name_c.h) + +or possibly nested:: + + set(HEADERS + very_long_header_name_a.h + very_long_header_name_b.h + very_long_header_name_c.h) + + set( + HEADERS + very_long_header_name_a.h + very_long_header_name_b.h + very_long_header_name_c.h) + + but this starts to look a little inconsistent when other arguments are + used:: + + set( + HEADERS PARENT_SCOPE + very_long_header_name_a.h + very_long_header_name_b.h + very_long_header_name_c.h) + Lots of medium-length args, looks good vertical, horizontally nested:: set(SOURCES @@ -295,6 +324,24 @@ for this purpose. This could be a sandard "microtag" format including the ability to set the list sortable. For example: ``#v,s`` would be "vertical, sortable" +Another interesting case is if we have an argument comment on a keyword +argument, or a prefix group. For example:: + + set(foobarbaz # comment about foobarbaz + value_one value_two value_three value_four value_five value_six + value_seven value_eight) + +Should that be formatted as above, or as:: + + set(foobarbaz # comment about foobarbaz + value_one value_two value_three value_four value_five + value_six value_seven value_eight) + +If we're already formatting set as:: + + set(foobarbaz value_one value_two value_three value_four value_five + value_six value_seven value_eight) + ------- Nesting ------- @@ -409,3 +456,124 @@ I don't think there's any reason to add structure for the internal operators like ``MATCHES``. In particular children of a boolean operator can be simple positional argument groups (horizontally-wrapped). We can tag the internal operator as a keyword but we don't need to create a KWARGGROUP for it. + +------------------------------ +Internally Wrapped Positionals +------------------------------ + +The third kwarg (AND) in this statement looks bad because it is Internally +wrapped. The second option looks better: + +.. code:: cmake + + set(matchme "_DATA_\|_CMAKE_\|INTRA_PRED\|_COMPILED\|_HOSTING\|_PERF_\|CODER_") + if(("${var}" MATCHES "_TEST_" AND NOT "${var}" MATCHES "${matchme}") + OR (CONFIG_AV1_ENCODER AND CONFIG_ENCODE_PERF_TESTS AND "${var}" MATCHES + "_ENCODE_PERF_TEST_" + )) + list(APPEND aom_test_source_vars ${var}) + endif() + + set(matchme "_DATA_\|_CMAKE_\|INTRA_PRED\|_COMPILED\|_HOSTING\|_PERF_\|CODER_") + if(("${var}" MATCHES "_TEST_" AND NOT "${var}" MATCHES "${matchme}") + OR (CONFIG_AV1_ENCODER + AND CONFIG_ENCODE_PERF_TESTS + AND "${var}" MATCHES "_ENCODE_PERF_TEST_")) + list(APPEND aom_test_source_vars ${var}) + endif() + +However, this short :code:`set()` statement looks better if we don't push the +internally wrapped argument to the next line: + +.. code:: cmake + + set(sources # cmake-format: sortable + bar.cc baz.cc foo.cc) + +Perhaps the difference is that in the latter case it's going to consume two +lines anyway... whereas in the former case it would only consume one +line. + +-------------------- +Columnized arguments +-------------------- + +Some very long statements with a large number of keywords might look nice +and organized if we columize the child argument groups. For example: + +.. code:: cmake + + ExternalProject_Add( + FOO + PREFIX ${FOO_PREFIX} + TMP_DIR ${TMP_DIR} + STAMP_DIR ${FOO_PREFIX}/stamp + # Download + DOWNLOAD_DIR ${DOWNLOAD_DIR} + DOWNLOAD_NAME ${FOO_ARCHIVE_FILE_NAME} + URL ${STORAGE_URL}/${FOO_ARCHIVE_FILE_NAME} + URL_MD5 ${FOO_MD5} + # Patch + PATCH_COMMAND ${PATCH_COMMAND} ${PROJECT_SOURCE_DIR}/patch.diff + # Configure + SOURCE_DIR ${SRC_DIR} + CMAKE_ARGS ${CMAKE_OPTS} + # Build + BUILD_IN_SOURCE 1 + BUILD_BYPRODUCTS ${CUR_COMPONENT_ARTIFACTS} + # Logging + LOG_CONFIGURE 1 + LOG_BUILD 1 + LOG_INSTALL 1 + ) + +Note what :code:`clang-format` does for these cases. If two consecutive +keywords are more than :code:`n` characters different in length, then break +columns, which might come out something like this: + +.. code:: cmake + + ExternalProject_Add( + FOO + PREFIX ${FOO_PREFIX} + TMP_DIR ${TMP_DIR} + STAMP_DIR ${FOO_PREFIX}/stamp + # Download + DOWNLOAD_DIR ${DOWNLOAD_DIR} + DOWNLOAD_NAME ${FOO_ARCHIVE_FILE_NAME} + URL ${STORAGE_URL}/${FOO_ARCHIVE_FILE_NAME} + URL_MD5 ${FOO_MD5} + # Patch + PATCH_COMMAND ${PATCH_COMMAND} ${PROJECT_SOURCE_DIR}/patch.diff + # Configure + SOURCE_DIR ${SRC_DIR} + CMAKE_ARGS ${CMAKE_OPTS} + # Build + BUILD_IN_SOURCE 1 + BUILD_BYPRODUCTS ${CUR_COMPONENT_ARTIFACTS} + # Logging + LOG_CONFIGURE 1 + LOG_BUILD 1 + LOG_INSTALL 1 + ) + +As an experimental feature, we could require a tag :code:`# cmf: columnize` +to enable this formatting. + +------------------------- +Algorithm Ideas and Notes +------------------------- + +Layout Passes +============= + +Up through version 0.5.2 each node would lay itself out using pass numbers +``[0, ]``. This worked pretty well, but actually I would like +the nesting to be a little more depth dependant. For example I would like +depth 0 (statement) to nest rather early, while I would like higher depths +(i.e. KWARGS) to nest later, but go vertical earlier. + +One alternative is to have a global ``passno`` and apply different rules at +each pass until things fit, but the probem with this option is that two +subtrees might require fastly different passes. We don't want to +vertically wrap one all kwargs just because one needs to. diff --git a/cmake_format/doc/changelog.rst b/cmake_format/doc/changelog.rst index 5ac950b..40f97e8 100644 --- a/cmake_format/doc/changelog.rst +++ b/cmake_format/doc/changelog.rst @@ -2,6 +2,27 @@ Changelog ========= +----------- +v0.6 series +----------- + +v0.6.0 +------ + +Significant refactor of the formatting logic. + +* Move ``format_tests`` into ``command_tests.misc_tests`` +* Prototype sidecar tests for easier readability/maintainability +* ArgGroupNodes gain representation in the layout tree +* Get rid of ``WrapAlgo`` +* Eliminate vertical/nest as separate decisions. Nesting is just the wrap + decision for StatementNode and KwargNode wheras vertical is the wrap + decision for PargGroupnode and ArgGroupNode. +* Replace ``algorithm_order`` with ``_layout_passes`` +* Get rid of ``default_accept_layout`` and move logic into a member function +* Move configuration and ``node_path`` into new ``StackContext`` +* Stricter valid-child-set for most layout nodes + ----------- v0.5 series ----------- @@ -22,12 +43,12 @@ v0.5.5 * Closes `#129`_: cmakeFormat.args in settings.json yields Incorrect type * Closes `#131`_: cmakeFormat.args is an array of items of type string -.. __#121: https://github.com/cheshirekow/cmake_format/issues/121 -.. __#123: https://github.com/cheshirekow/cmake_format/issues/123 -.. __#125: https://github.com/cheshirekow/cmake_format/issues/125 -.. __#128: https://github.com/cheshirekow/cmake_format/issues/128 -.. __#129: https://github.com/cheshirekow/cmake_format/issues/129 -.. __#131: https://github.com/cheshirekow/cmake_format/issues/131 +.. _#121: https://github.com/cheshirekow/cmake_format/issues/121 +.. _#123: https://github.com/cheshirekow/cmake_format/issues/123 +.. _#125: https://github.com/cheshirekow/cmake_format/issues/125 +.. _#128: https://github.com/cheshirekow/cmake_format/issues/128 +.. _#129: https://github.com/cheshirekow/cmake_format/issues/129 +.. _#131: https://github.com/cheshirekow/cmake_format/issues/131 v0.5.4 @@ -45,11 +66,11 @@ v0.5.4 * Closes `#119`_: Fix missing newline argument * Closes `#120`_: auto-line ending option not working correctly under Windows -.. __#114: https://github.com/cheshirekow/cmake_format/issues/114 -.. __#117: https://github.com/cheshirekow/cmake_format/issues/117 -.. __#118: https://github.com/cheshirekow/cmake_format/issues/118 -.. __#119: https://github.com/cheshirekow/cmake_format/issues/119 -.. __#120: https://github.com/cheshirekow/cmake_format/issues/120 +.. _#114: https://github.com/cheshirekow/cmake_format/issues/114 +.. _#117: https://github.com/cheshirekow/cmake_format/issues/117 +.. _#118: https://github.com/cheshirekow/cmake_format/issues/118 +.. _#119: https://github.com/cheshirekow/cmake_format/issues/119 +.. _#120: https://github.com/cheshirekow/cmake_format/issues/120 v0.5.3 ------ diff --git a/cmake_format/doc/configopts.rst b/cmake_format/doc/configopts.rst new file mode 100644 index 0000000..7af814c --- /dev/null +++ b/cmake_format/doc/configopts.rst @@ -0,0 +1,25 @@ +===================== +Configuration Options +===================== + +------------- +layout_passes +------------- + +See the :ref:`Formatting Algorithm ` section for more +information on how `cmake-format` uses multiple passes to converge on the +final layout of the listfile source code. This option can be used to override +the default behavior. The format of this option is a dictionary, where the keys +are the names of the different layout node classes: + +* StatementNode +* ArgGroupNode +* KWargGroupNode +* PargGroupNode +* ParenGroupNode + +The dictionary values are a list of pairs (2-tuples) in the form of +:code:`(passno, wrap-decision)`. Where :code:`passno` is the pass number at +which the wrap-decision becomes active, and :code:`wrap-decision` is a boolean +:code:`(true/false)`. For each layout pass, the decision of whether or not the +node should wrap (either nested, or vertical) is looked-up from this map. diff --git a/cmake_format/doc/example.rst b/cmake_format/doc/example.rst index 3af6e50..1060585 100644 --- a/cmake_format/doc/example.rst +++ b/cmake_format/doc/example.rst @@ -26,12 +26,11 @@ Will turn this: add_subdirectories(foo bar baz foo2 bar2 baz2) - # This very long command should be split to multiple lines + # This very long command should be wrapped set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) - # This command should be split into one line per entry because it has a long - # argument list. - set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc) + # This command should be split into one line per entry because it has a long argument list. + set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc source_h.cc) # The string in this command should not be split set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") @@ -78,7 +77,7 @@ Will turn this: if(sbar) # This comment is in-scope. add_library(foo_bar_baz foo.cc bar.cc # this is a comment for arg2 - # this is more comment for arg2, it should be joined with the first. + # this is more comment for arg2, it should be joined with the first. baz.cc) # This comment is part of add_library other_command(some_long_argument some_long_argument) # this comment is very long and gets split across some lines @@ -126,14 +125,9 @@ into this: # This comment should remain right before the command call. Furthermore, the # command call should be formatted to a single line. - add_subdirectories(foo - bar - baz - foo2 - bar2 - baz2) - - # This very long command should be split to multiple lines + add_subdirectories(foo bar baz foo2 bar2 baz2) + + # This very long command should be wrapped set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) @@ -145,11 +139,12 @@ into this: source_d.cc source_e.cc source_f.cc - source_g.cc) + source_g.cc + source_h.cc) # The string in this command should not be split - set_target_properties(foo bar baz - PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") + set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS + "-std=c++11 -Wall -Wextra") # This command has a very long argument and can't be aligned with the command # end, so it should be moved to a new line with block indent + 1. @@ -162,11 +157,9 @@ into this: "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") set(HEADERS - header_a.h - header_b.h # This comment should be preserved, moreover it should be split - # across two lines. - header_c.h - header_d.h) + header_a.h header_b.h # This comment should be preserved, moreover it should + # be split across two lines. + header_c.h header_d.h) # This part of the comment should be formatted but... # cmake-format: off @@ -191,30 +184,32 @@ into this: if(foo) if(sbar) # This comment is in-scope. - add_library(foo_bar_baz - foo.cc - bar.cc # this is a comment for arg2 this is more comment for - # arg2, it should be joined with the first. - baz.cc) # This comment is part of add_library - - other_command(some_long_argument some_long_argument) # this comment is very - # long and gets split - # across some lines - - other_command(some_long_argument some_long_argument some_long_argument) - # this comment is even longer and wouldn't make sense to pack at the end of - # the command so it gets it's own lines + add_library( + foo_bar_baz + foo.cc bar.cc # this is a comment for arg2 this is more comment for arg2, + # it should be joined with the first. + baz.cc) # This comment is part of add_library + + other_command( + some_long_argument some_long_argument) # this comment is very long and + # gets split across some lines + + other_command( + some_long_argument some_long_argument some_long_argument) # this comment + # is even longer + # and wouldn't + # make sense to + # pack at the + # end of the + # command so it + # gets it's own + # lines endif() endif() # This very long command should be broken up along keyword arguments foo(nonkwarg_a nonkwarg_b - HEADERS a.h - b.h - c.h - d.h - e.h - f.h + HEADERS a.h b.h c.h d.h e.h f.h SOURCES a.cc b.cc d.cc DEPENDS foo bar baz) diff --git a/cmake_format/doc/features.rst b/cmake_format/doc/features.rst index 00e0a93..04b1f3f 100644 --- a/cmake_format/doc/features.rst +++ b/cmake_format/doc/features.rst @@ -158,9 +158,9 @@ and you can globally disable sorting by making setting this configuration to Custom Commands --------------- -Due to the fact that cmake is a macro language, `cmake-format` is, by necessity, -a *semantic* source code formatter. In general it tries to make smart -formatting decisions based on the meaning of arguments in an otherwise +Due to the fact that cmake is a macro language, `cmake-format` is, by +necessity, a *semantic* source code formatter. In general it tries to make +smart formatting decisions based on the meaning of arguments in an otherwise unstructured list of arguments in a cmake statement. `cmake-format` can intelligently format your custom commands, but you will need to tell it how to interpret your arguments. @@ -197,8 +197,8 @@ fields: * ``kwargs``: a dictionary mapping keywords to sub-specifications. A sub-specification may be a complete dictionary of ``pargs``, ``flags``, and ``kwargs`` (nested, all the way down). Or, if the keyword argument accepts - only positionals, then it can be simply the ``pargs`` specification (as in the - example above). + only positionals, then it can be simply the ``pargs`` specification (as in + the example above). For the example specification above, the custom command would look somehing like this: diff --git a/cmake_format/doc/format_algorithm.rst b/cmake_format/doc/format_algorithm.rst index 8d22917..15a52f0 100644 --- a/cmake_format/doc/format_algorithm.rst +++ b/cmake_format/doc/format_algorithm.rst @@ -1,18 +1,23 @@ +.. _formatting-algorithm: + ==================== Formatting Algorithm ==================== -The formatter works by attempting to select an -appropriate ``position`` and ``wrap`` (collectively referred to as a -"layout") for each node in the layout tree. Positions are represented by -``(row, col)`` pairs and the wrap dictates how childen of that node -are positioned. +The formatter works by attempting to select an appropriate ``position`` and +``wrap`` (collectively referred to as a "layout") for each node in the layout +tree. Positions are represented by ``(row, col)`` pairs and the wrap dictates +how childen of that node are positioned. -------- Wrapping -------- -``cmake-format`` implements two styles of wrapping: +``cmake-format`` implements three styles of wrapping. +The default wrapping for all nodes is horizontal wrapping. If horizontal +wrapping fails to emit an admissible layout, then a node will advance to +either vertical wrapping or nested wrapping (which one depends on the type of +node). Horizontal Wrapping =================== @@ -82,7 +87,7 @@ Vertical wrapping assigns each child to the next row:: ████ Again, note that this happens at the depth of the layout tree. In particular -children may be wrapped horizontally internally:: +children may be wrapped horizontally within the subtrees:: | ▒▒▒▒▒▒ ███ ██████ |<- col-limit | ▒▒▒ ██████ ██ | @@ -91,37 +96,11 @@ children may be wrapped horizontally internally:: | ████ ██████████ | | ▒▒ ███ ████ | -------- -Nesting -------- - -In addition to wrapping, ``cmake-format`` also must decide how to nest children -of a layout node. - -Horizontal Nesting -================== - -Horizontal nesting places children in a column immediately following the -terminal cursor of the parent. For example:: - | |<- col-limit - | ▒▒▒ ██ ███ ██ █████ | - | ████████████████ | - | █████████ ████ | - -In a more deeply nested layout tree, we might see the following:: - - | |<- col-limit - | ▓▓▓ ▒▒▒ ██ ███ ██ █████ | - | ████████████████ | - | █████████ ████ | - | ▒▒▒ ████ ███ █ | - | ▒▒▒▒▒▒ ████ ███ █ | - -Vertical nesting -================ +Nesting +======= -Vertical nesting places children in a column which is one tabwidth to the +Nesting places children in a column which is one ``tab_width`` to the right of the parent node's position, and one line below. For example:: | |<- col-limit @@ -154,6 +133,9 @@ may be nested differently. For example:: | ▒▒▒ ████ ███ █ | | ▒▒▒▒▒▒ ████ ███ █ | +Note that the only nodes that can nest are ``STATEMENT`` and ``KWARGGROUP`` +nodes. These nodes necessarily only have one child, an ``ARGGROUP`` node. +Therefore there really isn't a notion of "wrapping" for these nodes. -------------------- Formatting algorithm @@ -166,115 +148,64 @@ first line after the output cursor of it's predecessor, and at a column ``config.tab_size`` to the right of it's parent. ``STATEMENTS`` however, are laid out over several passes until the -text for that subtree lies is accepted. Each pass is governed by a -specification mapping node depth to a layout algorithm (i.e. a -``(nesting,wrapping)`` pair, as well as a condition of acceptance. - - -Ideas -===== - -* If the name of the command (plus parenthesis) is less than or equal to - tab-width then vertical nesting is off the table. -* If the name of the command (plus parenthesis) is "very long" then horizontal - nesting is off the table. -* If a PARGGROUP is a "list" then we should probably have some options for - deciding whether or not it is wrapped horizontally or vertically: - - * vertical wrap if number of arguments > some threshold - * wrap always - * wrap always for specific statments/kwargs/paths - -* Multiple KWARGGROUPS should always be nested vertically (optionally) -* Can implement that guys idea about column-aligning kwarg arguments, - perhaps if kwargs are within some threshold of the same size -* If a statement interior ends with a comment we must dangle the parenthesis -* configuration to decide whether or not to dangle a parenthesis always -* Algorithm order for PARGGROUP (currently):: - - (nest:H, wrap:H), valid if numlines == 1 - (nest:H, wrap:V), valid if columns are not exceeded - (nest:V, wrap:V) - -* I think that I might prefer:: - - (nest:H, wrap:H), valid if numlines <= [config-option=1] - (nest:V, wrap:H), valid if numlines <= [config-option=1] - (nest:V, wrap:V) - -* Algorithm order for ARGGROUP with at least [n=2] KWARGGROUPS: - - (nest:H, wrap:H), valid if numlines < [config-option=0] (i.e. never) - (nest:V, wrap:V), +text for that subtree is accepted. Each pass is governed by a +specification mapping pass number to a wrap decision (i.e. a +boolean indicating whether or not to wrap vertical or nest children) + +Layout Passes +============= + +The current algorithm works in a kind of top-down refinement. When a node is +laid out by calling it's ``reflow()`` method, it is informed of its parent's +current pass number (``passno``). It then iterates through its own ``passno`` +from zero up to it's parent's ``passno`` and terminates at the first admissible +layout. Note that within the layout of the node itself, it's current +``passno`` can only affect its ``wrap`` decision. However, because each of its +children will advance through their own passes, the overall layout of a subtree +between two different passes may change, even if the node at the subtree root +didn't change it's ``wrap`` decision between those passes. + +This approach seems to work well even for +:ref:`deeply nested ` or +:ref:`complex ` statements. -* So should we use some kind of overridable function to get -* If we do limit HWRAP to at most 2 lines, are there any cases where this - would do something we don't want? Generally we don't have lots of - unstructured positional arguments that aren't lists. Also, if we had - such a thing, could we not tag it that way? +Newline decision +================ +When a node is in horizontal layout mode (``wrap=False``), there are a couple +of reasons why the algorithm might choose to insert a newline between two +of it's children. + +1. If a token would overflow the column limit, insert a newline (e.g. the + usual notion of wrapping) +2. If the token is the last token before a closing parenthesis, and the + token plus the parenthesis would overflow the column limit, then insert a + newline. +3. If a token is preceeded by a line comment, then the token cannot be placed + on the same line as the comment (or it will become part of the comment) so + a newline is inserted between them. +4. If a token is a line comment which is not associated with an argument (e.g. + it is a "free" comment at the current scope) then it will not be placed + on the same line as a preceeding argument token. If it was, then subsequent + parses would associate this comment with that argument. In such a case, a + newline is inserted between the preceeding argument and the line comment. +5. If the node is an interior node, and one of it's children is internally + wrapped (i.e. consumes more than two lines) then it will not be placed + on the same line as another node. In such a case a newlines is inserted. +6. If the node is an interior node and a child fails to find an admissible + layout at the current cursor, a newline is inserted and a new layout attempt + is made for the child. + +Admissible layouts +================== -Positional Arguments -==================== +There are a couple of reasons why a layout may be deemed inadmissible: -Candidate algorithm: - -First, wrap horizontally. If they all fit on [n=2] lines, and if the -statement spelling does not exceed [k=2] characters above the tab-width, choose -that layout. Note that the previous rule can be enforced by this rule with -[n=1], so perhaps we can exclude a special case for the previous attempt:: - - foobarbaz_hello(argument_one argument_two argument_three argument_four - argument_five argument_six) - -If not, nest vertically, and if they fit on [n=2] lines, choose that layout:: - - foobarbaz_hello( - argument_one argument_two argument_three argument_four argument_five - argument_six) - -Note that, if the statement spelling (plus optional space and paren) are less -than the tab width, then vertical nesting is disabled. In which case we proceed -directly to the next attempt: vertical wrap:: - - foobarbaz_hello( - argument_one - argument_two - argument_three - argument_four - argument_five - argument_six) - -In order to match expectation and current behavior, I think by default we can -use values of (2, 2) but I think that personally I will want values of (1,1). -We can describe this sequence by the following escallation of attempts. If -each attempt fails we move onto the nest. - -1. initially horizontal nesting, horizontal wrapping -2. switch to vertical nesting, if allowed -3. switch to vertical wrapping if allowed - -This differs a little from the current algorithm and I'm not sure what the best -way to unify them is, perhaps it's to continue -specifying an "algorithm order" kind of thing. We could change it to -something like: - -0. ``(vertical-nest, vertical-wrap)`` -1. ``(False, False)`` -2. ``(False, True)`` -3. ``(True, False)`` -4. ``(True, True)`` - -And then pair each with a configurable approval function. For instance I would -always reject #2. The problem here is that for short statement names like -``set()`` or for large tab-widths, vertical nesting is not allowed. Perhaps -that is just something we need to specify in the approval function though. For -what I want personally, The approval function for #2 would be to reject always -unless the statement spelling too small to vertically wrap. - -Note also, though, that my desired algorithm order is not consistent at depth. -I generally want to avoid #2 for statement groups, but I generally want to -avoid #3 for keyword groups. And how about deeply nested kwarg groups? +1. If the bounding box of a node overflows the column limit +2. If a node is horizontally wrapped at the current ``passno`` but consumes + more than ``max_lines_hwrap`` lines +3. If the node is horizontally wrapped at the current ``passno`` but the node + path is marked as ``always_wrap`` Comments ======== @@ -322,49 +253,3 @@ minimum width of the comment block is given by:: Which would preclude it from being crammed into the right-most slot. -Special-case Positional Arguments -================================= - -There are some situations in which we might want to apply a different threshold -set than the default. ``COMMAND`` kwargs in particular we probably never want -to wrap vertically. - -Interior groups -=============== - -Keyword subtrees (``KWARGGROUP``) themselves are laid out the same as -statements, but interior nodes (``ARGGROUP``) in the parse/layout tree follow -a slightly different set of rules. The reasoning for this separate rule set is -that the syntatic boundary between children in an ``ARGGROUP`` are also -significant semantic boundaries and so breaking a line on these boundaries is -cheaper than breaking a line within a positional argument group. - -Candidate Algorithm: - -Wrap horizonally, if they all fit on [n=1] lines and if there are at most [n=2] -non-empty groups. For example:: - - foobarbaz_hello(argument_one argument_two KEYWORD_ONE kwarg_one) - -Otherwise, nest vertically, if they all fit on [n=1] lines and if there are -at most [n=2] non-empty groups:: - - foobarbaz_hello( - argument_one argument_two argument_three KEYWORD_ONE kwarg_one kwarg_two) - -Otherwise, wrap vertically:: - - foobarbaz_hello( - argument_one argument_two argument_three - KEYWORD_ONE kwarg_one kwarg_two - KEYWORD_TWO kwarg_three kwarg_four) - ------------- -Search Order ------------- - -The current algorithm does a kind of top-down implementation. Each node is -allowed to try layouts in "algorithm order" from start up to their parent's -current algorithm. This seems to work well even for -:ref:`deeply nested ` or -:ref:`complex ` statements. diff --git a/cmake_format/doc/index.rst b/cmake_format/doc/index.rst index 2e0ec57..d0622af 100644 --- a/cmake_format/doc/index.rst +++ b/cmake_format/doc/index.rst @@ -23,6 +23,7 @@ like crap. format_algorithm case_studies render_html + configopts release_notes changelog diff --git a/cmake_format/doc/parse_tree.rst b/cmake_format/doc/parse_tree.rst index dca58a0..9fbac8b 100644 --- a/cmake_format/doc/parse_tree.rst +++ b/cmake_format/doc/parse_tree.rst @@ -228,9 +228,10 @@ You can inspect the parse tree of a listfile by ``cmake-format`` with │ │ ├─ KEYWORD: 2:23 │ │ │ └─ Token(type=WORD, content='VERSION', line=2, col=23) │ │ ├─ Token(type=WHITESPACE, content=' ', line=2, col=30) - │ │ └─ PARGGROUP: 2:31 - │ │ └─ ARGUMENT: 2:31 - │ │ └─ Token(type=UNQUOTED_LITERAL, content='3.5', line=2, col=31) + │ │ └─ ARGGROUP: 2:31 + │ │ └─ PARGGROUP: 2:31 + │ │ └─ ARGUMENT: 2:31 + │ │ └─ Token(type=UNQUOTED_LITERAL, content='3.5', line=2, col=31) │ └─ RPAREN: 2:34 │ └─ Token(type=RIGHT_PAREN, content=')', line=2, col=34) ├─ WHITESPACE: 2:35 @@ -255,9 +256,10 @@ You can inspect the parse tree of a listfile by ``cmake-format`` with │ │ ├─ LPAREN: 4:2 │ │ │ └─ Token(type=LEFT_PAREN, content='(', line=4, col=2) │ │ ├─ ARGGROUP: 4:3 - │ │ │ ├─ ARGUMENT: 4:3 - │ │ │ │ └─ Token(type=WORD, content='FOO', line=4, col=3) - │ │ │ ├─ Token(type=WHITESPACE, content=' ', line=4, col=6) + │ │ │ ├─ PARGGROUP: 4:3 + │ │ │ │ ├─ ARGUMENT: 4:3 + │ │ │ │ │ └─ Token(type=WORD, content='FOO', line=4, col=3) + │ │ │ │ └─ Token(type=WHITESPACE, content=' ', line=4, col=6) │ │ │ └─ KWARGGROUP: 4:7 │ │ │ ├─ KEYWORD: 4:7 │ │ │ │ └─ Token(type=WORD, content='AND', line=4, col=7) @@ -267,16 +269,18 @@ You can inspect the parse tree of a listfile by ``cmake-format`` with │ │ │ ├─ LPAREN: 4:11 │ │ │ │ └─ Token(type=LEFT_PAREN, content='(', line=4, col=11) │ │ │ ├─ ARGGROUP: 4:12 - │ │ │ │ ├─ ARGUMENT: 4:12 - │ │ │ │ │ └─ Token(type=WORD, content='BAR', line=4, col=12) - │ │ │ │ ├─ Token(type=WHITESPACE, content=' ', line=4, col=15) + │ │ │ │ ├─ PARGGROUP: 4:12 + │ │ │ │ │ ├─ ARGUMENT: 4:12 + │ │ │ │ │ │ └─ Token(type=WORD, content='BAR', line=4, col=12) + │ │ │ │ │ └─ Token(type=WHITESPACE, content=' ', line=4, col=15) │ │ │ │ └─ KWARGGROUP: 4:16 │ │ │ │ ├─ KEYWORD: 4:16 │ │ │ │ │ └─ Token(type=WORD, content='OR', line=4, col=16) │ │ │ │ ├─ Token(type=WHITESPACE, content=' ', line=4, col=18) │ │ │ │ └─ ARGGROUP: 4:19 - │ │ │ │ └─ ARGUMENT: 4:19 - │ │ │ │ └─ Token(type=WORD, content='BAZ', line=4, col=19) + │ │ │ │ └─ PARGGROUP: 4:19 + │ │ │ │ └─ ARGUMENT: 4:19 + │ │ │ │ └─ Token(type=WORD, content='BAZ', line=4, col=19) │ │ │ └─ RPAREN: 4:22 │ │ │ └─ Token(type=RIGHT_PAREN, content=')', line=4, col=22) │ │ └─ RPAREN: 4:23 @@ -337,52 +341,61 @@ You can inspect the layout tree of a listfile by ``cmake-format`` with .. code:: text - └─ BODY,HPACK(0) p(0,0) ce:35 - ├─ STATEMENT,HPACK(0) p(0,0) ce:35 - │ ├─ FUNNAME,HPACK(0) p(0,0) ce:22 - │ ├─ LPAREN,HPACK(0) p(0,22) ce:23 - │ ├─ KWARGGROUP,HPACK(0) p(0,23) ce:34 - │ │ ├─ KEYWORD,HPACK(0) p(0,23) ce:30 - │ │ └─ PARGGROUP,HPACK(0) p(0,31) ce:34 - │ │ └─ ARGUMENT,HPACK(0) p(0,31) ce:34 - │ └─ RPAREN,HPACK(0) p(0,34) ce:35 - ├─ STATEMENT,HPACK(0) p(1,0) ce:13 - │ ├─ FUNNAME,HPACK(0) p(1,0) ce:7 - │ ├─ LPAREN,HPACK(0) p(1,7) ce:8 - │ ├─ PARGGROUP,HPACK(0) p(1,8) ce:12 - │ │ └─ ARGUMENT,HPACK(0) p(1,8) ce:12 - │ └─ RPAREN,HPACK(0) p(1,12) ce:13 - └─ FLOW_CONTROL,HPACK(0) p(2,0) ce:29 - ├─ STATEMENT,HPACK(0) p(2,0) ce:24 - │ ├─ FUNNAME,HPACK(0) p(2,0) ce:2 - │ ├─ LPAREN,HPACK(0) p(2,2) ce:3 - │ ├─ ARGUMENT,HPACK(0) p(2,3) ce:6 - │ ├─ KWARGGROUP,HPACK(0) p(2,7) ce:23 - │ │ ├─ KEYWORD,HPACK(0) p(2,7) ce:10 - │ │ └─ ARGGROUP,HPACK(0) p(2,11) ce:23 - │ │ └─ PARENGROUP,HPACK(0) p(2,11) ce:23 - │ │ ├─ LPAREN,HPACK(0) p(2,11) ce:12 - │ │ ├─ ARGGROUP,HPACK(0) p(2,12) ce:22 - │ │ │ ├─ ARGUMENT,HPACK(0) p(2,12) ce:15 - │ │ │ └─ KWARGGROUP,HPACK(0) p(2,16) ce:22 - │ │ │ ├─ KEYWORD,HPACK(0) p(2,16) ce:18 - │ │ │ └─ ARGGROUP,HPACK(0) p(2,19) ce:22 - │ │ │ └─ ARGUMENT,HPACK(0) p(2,19) ce:22 - │ │ └─ RPAREN,HPACK(0) p(2,22) ce:23 - │ └─ RPAREN,HPACK(0) p(2,23) ce:24 - ├─ BODY,HPACK(0) p(3,2) ce:29 - │ └─ STATEMENT,HPACK(0) p(3,2) ce:29 - │ ├─ FUNNAME,HPACK(0) p(3,2) ce:13 - │ ├─ LPAREN,HPACK(0) p(3,13) ce:14 - │ ├─ PARGGROUP,HPACK(0) p(3,14) ce:19 - │ │ └─ ARGUMENT,HPACK(0) p(3,14) ce:19 - │ ├─ PARGGROUP,HPACK(0) p(3,20) ce:28 - │ │ └─ ARGUMENT,HPACK(0) p(3,20) ce:28 - │ └─ RPAREN,HPACK(0) p(3,28) ce:29 - └─ STATEMENT,HPACK(0) p(4,0) ce:7 - ├─ FUNNAME,HPACK(0) p(4,0) ce:5 - ├─ LPAREN,HPACK(0) p(4,5) ce:6 - └─ RPAREN,HPACK(0) p(4,6) ce:7 + └─ BODY,(passno=0,wrap=F) pos:(0,0) colextent:35 + ├─ STATEMENT,(passno=0,wrap=F) pos:(0,0) colextent:35 + │ ├─ FUNNAME,(passno=0,wrap=F) pos:(0,0) colextent:22 + │ ├─ LPAREN,(passno=0,wrap=F) pos:(0,22) colextent:23 + │ ├─ ARGGROUP,(passno=0,wrap=F) pos:(0,23) colextent:34 + │ │ └─ KWARGGROUP,(passno=0,wrap=F) pos:(0,23) colextent:34 + │ │ ├─ KEYWORD,(passno=0,wrap=F) pos:(0,23) colextent:30 + │ │ └─ ARGGROUP,(passno=0,wrap=F) pos:(0,31) colextent:34 + │ │ └─ PARGGROUP,(passno=0,wrap=F) pos:(0,31) colextent:34 + │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(0,31) colextent:34 + │ └─ RPAREN,(passno=0,wrap=F) pos:(0,34) colextent:35 + ├─ STATEMENT,(passno=0,wrap=F) pos:(1,0) colextent:13 + │ ├─ FUNNAME,(passno=0,wrap=F) pos:(1,0) colextent:7 + │ ├─ LPAREN,(passno=0,wrap=F) pos:(1,7) colextent:8 + │ ├─ ARGGROUP,(passno=0,wrap=F) pos:(1,8) colextent:12 + │ │ └─ PARGGROUP,(passno=0,wrap=F) pos:(1,8) colextent:12 + │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(1,8) colextent:12 + │ └─ RPAREN,(passno=0,wrap=F) pos:(1,12) colextent:13 + └─ FLOW_CONTROL,(passno=0,wrap=F) pos:(2,0) colextent:29 + ├─ STATEMENT,(passno=0,wrap=F) pos:(2,0) colextent:24 + │ ├─ FUNNAME,(passno=0,wrap=F) pos:(2,0) colextent:2 + │ ├─ LPAREN,(passno=0,wrap=F) pos:(2,2) colextent:3 + │ ├─ ARGGROUP,(passno=0,wrap=F) pos:(2,3) colextent:23 + │ │ ├─ PARGGROUP,(passno=0,wrap=F) pos:(2,3) colextent:6 + │ │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(2,3) colextent:6 + │ │ └─ KWARGGROUP,(passno=0,wrap=F) pos:(2,7) colextent:23 + │ │ ├─ KEYWORD,(passno=0,wrap=F) pos:(2,7) colextent:10 + │ │ └─ ARGGROUP,(passno=0,wrap=F) pos:(2,11) colextent:23 + │ │ └─ PARENGROUP,(passno=0,wrap=F) pos:(2,11) colextent:23 + │ │ ├─ LPAREN,(passno=0,wrap=F) pos:(2,11) colextent:12 + │ │ ├─ ARGGROUP,(passno=0,wrap=F) pos:(2,12) colextent:22 + │ │ │ ├─ PARGGROUP,(passno=0,wrap=F) pos:(2,12) colextent:15 + │ │ │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(2,12) colextent:15 + │ │ │ └─ KWARGGROUP,(passno=0,wrap=F) pos:(2,16) colextent:22 + │ │ │ ├─ KEYWORD,(passno=0,wrap=F) pos:(2,16) colextent:18 + │ │ │ └─ ARGGROUP,(passno=0,wrap=F) pos:(2,19) colextent:22 + │ │ │ └─ PARGGROUP,(passno=0,wrap=F) pos:(2,19) colextent:22 + │ │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(2,19) colextent:22 + │ │ └─ RPAREN,(passno=0,wrap=F) pos:(2,22) colextent:23 + │ └─ RPAREN,(passno=0,wrap=F) pos:(2,23) colextent:24 + ├─ BODY,(passno=0,wrap=F) pos:(3,2) colextent:29 + │ └─ STATEMENT,(passno=0,wrap=F) pos:(3,2) colextent:29 + │ ├─ FUNNAME,(passno=0,wrap=F) pos:(3,2) colextent:13 + │ ├─ LPAREN,(passno=0,wrap=F) pos:(3,13) colextent:14 + │ ├─ ARGGROUP,(passno=0,wrap=F) pos:(3,14) colextent:28 + │ │ ├─ PARGGROUP,(passno=0,wrap=F) pos:(3,14) colextent:19 + │ │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(3,14) colextent:19 + │ │ └─ PARGGROUP,(passno=0,wrap=F) pos:(3,20) colextent:28 + │ │ └─ ARGUMENT,(passno=0,wrap=F) pos:(3,20) colextent:28 + │ └─ RPAREN,(passno=0,wrap=F) pos:(3,28) colextent:29 + └─ STATEMENT,(passno=0,wrap=F) pos:(4,0) colextent:7 + ├─ FUNNAME,(passno=0,wrap=F) pos:(4,0) colextent:5 + ├─ LPAREN,(passno=0,wrap=F) pos:(4,5) colextent:6 + ├─ ARGGROUP,(passno=0,wrap=F) pos:(4,6) colextent:6 + └─ RPAREN,(passno=0,wrap=F) pos:(4,6) colextent:7 .. dynamic: dump-example-layout-end diff --git a/cmake_format/doc/release_notes.rst b/cmake_format/doc/release_notes.rst index a4185dc..8c534df 100644 --- a/cmake_format/doc/release_notes.rst +++ b/cmake_format/doc/release_notes.rst @@ -5,6 +5,55 @@ Release Notes Details of changes can be found in the changelog, but this file will contain some high level notes and highlights from each release. +v0.6 series +=========== + +------ +v0.6.0 +------ + +This release includes a significant refactor of the formatting logic. Details +of the new algorithm are described in the documentation__. As a result of the +algorithm changes, some config options have changed too. The following +config options are removed: + +* ``max_subargs_per_line`` (see ``max_pargs_hwrap``) +* ``nest_threshold`` (see ``min_prefix_chars``) +* ``algorithm_order`` (see ``layout_passes``) + +.. __: https://cmake-format.readthedocs.io/en/latest/format_algorithm.html + +And the following config options have been added: + +* ``max_subgroups_hwrap`` +* ``max_pargs_hwrap`` +* ``dangle_align`` +* ``min_prefix_chars`` +* ``max_prefix_chars`` +* ``max_lines_hwrap`` +* ``layout_passes`` +* ``enable_sort`` + +Also as a result of the algorithm changes, the default layout has changed. By +default, ``cmake-format`` will now prefer to nest long lists rather than +aligning them to the opening parenthesis of a statement. Also, due to the new +configuration options, the output of ``cmake-format`` is likely to be different +with your current configs. + +Additionally, ``cmake-format`` will now tend to prefer a normal "horizontal" +wrap for relatively long lists of positional arguments (e.g. source files in +``add_library``) whereas it would previously prefer a vertical layout (one-entry +per line). This is a consequence of an ambiguity between which positional +arguments should be vertical versus which should be wrapped. Two planned +features (layout tags and positional semantics) should help to provide enough +control to get the layout you want in these lists. + +I acknowledge that it is not ideal for formatting to change between releases +but this is an unfortunate inevitability at this stage of development. The +changes in this release elminate a number of inconsistencies and also adds the +groundwork for future planned features and options. Hopefully we are getting +close to a stable state and a 1.0 release. + v0.5 series =========== @@ -16,7 +65,6 @@ This is a maintenance release fixing a few minor bugs and enhancements. One new feature is that the ``--config`` command line option now accepts a list of config files, which should allow for including multiple databases of command specifications - ------ v0.5.4 ------ @@ -26,6 +74,15 @@ documentation. One notable feature added is that, during in-place formatting, if the file content is unchanged ``cmake-format`` will no-longer write the file. +------ +v0.5.3 +------ + +This hotfix release fixes a bug that would crash cmake-format if no +configuration file was present. It also includes some small under-the-hood +changes in preparation for an overhaul of the formatting logic. + + ------ v0.5.2 ------ diff --git a/cmake_format/doc/todo.rst b/cmake_format/doc/todo.rst index eb315a7..d7a6245 100644 --- a/cmake_format/doc/todo.rst +++ b/cmake_format/doc/todo.rst @@ -2,37 +2,6 @@ TODO ==== -* Allow option to infer keywords for commands which don't have a specification -* Add option to break long strings to make them fit -* Use cmake --help-command --help-property --help-variable --help-module - and parse the output to get the list of commands, properties, variable - names, etc. This has been around since at least v2.8.8 so it's pretty - available. It can definitely be used to filter available commands. -* Consider getting rid of config.endl and instead using - ``io.open(newline='\n')`` or ``io.open(newline='\r\n')`` depending on config. - Then just write ``\n`` and let the streamwriter translation take care of - line endings. -* Make a distinction between argument comments and blocklevel or statement - comments. Argument comments must be reflowed to (config.linewidth-1) if - dangle_parens is false, while block level and statement comments may reflow - up to (config.linewidth). Can probably just make this a flag in the - CommentNode class rather than implementing a separate class for it. This - can help to eliminate the need for, or at least simplify, the - has_terminal_comment() hack. -* Deal with the case that the command name is so long or that the statement is - nested so far that the open paren doesn't fit on the line and needs to be - wrapped. -* Improve error messages for exceptions/assertions caused by malformed input. -* Implement per-command algorithm order allowing to change the wrap preferences - at a fine-grained level. -* Implement kwarg canonical ordering. Each kwarg parser has a canonical order - associated with it. The formatter can re-order arguments when formatting to - ensure that they are always written in the same order. -* Add a generic CMAKE_FORMAT_TAG token type matching ``# cmake-format: XXX`` - or ``# cmf: XXX`` strings. -* Implement an ``unpad_hashruler`` configuration option. If true, dont separate - hashrulers from the leading comment character by a space. - cmake-lint ========== @@ -45,6 +14,9 @@ cmake-lint * improper capitalization of statements, kwargs, or flags, keywords * optional kwargs not in canonical order * extra positional arguments + * local variable used but not assigned + * local variable/global variable doesn't match regex + * syntax hidden by variable expansion VS Code ======= @@ -56,6 +28,7 @@ VS Code * https://code.visualstudio.com/docs/extensions/example-language-server * https://microsoft.github.io/language-server-protocol/specification#textDocument_onTypeFormatting +* Implement partial formatting (i.e. format highlight). * VScode language server protocol does not currently support semantic highlighting: https://github.com/Microsoft/language-server-protocol/issues/18 * But it can be implemented using custom messages to the language server such @@ -63,8 +36,9 @@ VS Code * An existing cmake extension is pretty good, but I think we can do better on code-completion and semantic highlighting -Sortabe Arguments -================= + +Sortable Arguments +================== * Don't treat tag comment specially with regard to trailing comment assignment, allow them to be attached to a preceeding parg or kwarg or whatever. This @@ -87,33 +61,6 @@ Sortabe Arguments might be tagged sortable. In this case, break the pair of argument groups at the comment tag instead of after library/executable name. -Parser Refactor -=============== - -* Deal with the hack in parse_positionals working around - ``install(RUNTIME COMPONENT runtime)``. The current solution is to only break - if the subparser isn't expecting an exact number of tokens. The problem with - this is that we will consume an RPAREN if a statement is malformed... and - we probably shouldn't do that. -* Cleanup the HWRAP logic. Currently it's distributed in a number of places: - - * reflow() for everything but a StatementNode reduces it's linewidth by one - in HPACK - * PArgGroupNode._reflow() reduces the final linewidth by one if it's a - _statement_terminal and we're in HWRAP mode - * most nodes do the same work during HPACK and HWRAP - * many nodes like StatementNode have old code that isn't needed now that - ArgGroup exists - -* Replace the rest of the legacy cmdspec tree with new style map of statement - parse functions -* Add tests for all the different forms of ``install()`` and ``file()`` that - we've implemented. -* Add a config option for users to specify custom commands using custom - parse functions, rather than just the legacy dictionary specification. -* Split parse_funs into modules to better organize custom parsers -* Implement custom parser for the different forms of ``list()`` - Current Issues ============== @@ -121,17 +68,12 @@ Current Issues argument is a variable dereference. For instance `file(${descr})`. What should we do in that case? Should we infer based on remaining arguments, fallback to a standard parser with a large set of kwargs and flags? -* Right now adding a line comment forces the line into HVPACK, but for a - COMMAND we probably actually want HWRAP so that we can manually pack our - shell commands -* Idea: an empty line comment after the first argument forces VPACK. An empty - line comment after any other argument forces HPACK. Not perfect, but might - work OK. Cost Function ============= -Implement something better than max-subargs per line and algorithm order. +Implement something better than :code:`max-subgroups-hwrap` and +:code:`layout-passes`. Probably both of these can be rolled up into a some kind of cost function that accounts for both issues. For instance, a cost function which penalizes number of lines, number of arguments on a line, and indentation @@ -139,48 +81,129 @@ would generally perfer a single line, would perhaps allow HWRAP but only if it was at most two lines, would allow VPACK but only if the statment name is not too long. -Format Refactor -=============== - -* Separate the layout algorithm between two separate decisions for "nesting" - and "wrapping". The order in which to apply them will depend on the type of - node we are at, and can be influenced by the parser. See notes in case - studies. -* Implement additional criteria for triggering a wrap: - - * arguments overflow the column width - * exceed threshold in number or size of arguments - * presence of a line comment - * is an `always_wrap` node - -* Add a configuration option for nesting preference. The current order is - basically ``(nest,wrap)``: - - * (horizontal, horizontal) - * (horizontal, vertical) - * (vertical, vertical) - - and, in particular, I think that I want it to nest vertically almost always. - The one case not to nest vertically is if the command name is less than or - equal to tab width. We might include some configuration option for how many - characters over tab-width to continue allowing horizontal nesting. We should - always be able to fall back to vertical nesting in the case that horizontal - just can't fit. - -* Do a scan of TODO's in formatter.py. I'm leaving a bunch in there. -* Should we add a configuration option for maximum sub-groups per line - (like max-subargs per line?) -* Should we add a configuration option to prevent horizontal wrapping a - child PARGGROUP after another argument? See the current format failures - in cmake_format.command_tests.conditional_tests - Release Process =============== -Add cmake rules for ``release`` and ``test-release`` that will double check -certain things: +Add cmake rules for ``prep-release`` and ``test-release`` that will +automatically create a release commit and double check certain things: -1. Closes issues in changelog are also closed in the commit message +1. Closed issues in changelog are also closed in the commit message 2. Version number is not ``dev`` 3. Version number is incremented +4. Execute the screw-users test + +Parse Stack Refactor +==================== + +There are a couple of things that are a bit clunky about the current +implementation of the parser stack context. Currently we only have the +:code:`breakstack` in each parse function. I think we want some more context. +One thing that we should probably add is the stack of parse nodes that have +been opened up to the current context. The initiating token for each parse +node can be used to infer some properties of tokens as we process them. + +* Github issue #122: ambiguity in the nesting level of a line comment which + might belong to the end of a nested argument list, or might belong to the + parent argument list. +* Look-ahead at the next semantic token when deciding whether or not to + break out of the current scope. Otherwise comments will get globbed up in + the nested scope when they were originally written in outer scope. +* If the look-ahead indicates that the next semantic token would break us + up the stack, then any comment between "here" and that token belongs to + the child set of one of the nodes somewhere in that stack region. One way + of associating it consistently is to find the latest parse node that starts + no later than the current comment node. +* Deal with the hack in parse_positionals working around + ``install(RUNTIME COMPONENT runtime)``. The current solution is to only break + if the subparser isn't expecting an exact number of tokens. The problem with + this is that we will consume an RPAREN if a statement is malformed... and + we probably shouldn't do that. + + +After Refactor +============== + +01. Move as many tests as possible into :code:`.cmake` sidecar files. + +02. Split :code:`formatter.py` into :code:`format_tree.py` and + :code:`formatter.py` + +03. Implement :code:`wrap` tags. I'm not sure what the tag string should + be, maybe :code:`wrap`, :code:`list`, :code:`vertical/nest`, but whatever + it is, it should record within the parse node a forced wrap decition and + override the trial/error logic. That way an empty comment can be used to + force a line-wrap, but a specific comment can be used to force a more + specific decision. +04. There are still many more cmake functions that need parser mappings. + + * :code:`list()` + * :code:`target_compile_definitions()` + +05. Make a test that executes cmake to get the + list of function names and make sure they're all in the database. +06. Replace the rest of the legacy cmdspec tree with new style map of statement + parse functions +07. Add tests for all the different forms of ``install()`` and ``file()`` that + we've implemented. +08. Add a config option for users to specify custom commands using custom + parse functions, rather than just the legacy dictionary specification. +09. Add option to infer keywords for commands which don't have a specification +10. Add option to break long strings to make them fit +11. Use cmake --help-command --help-property --help-variable --help-module + and parse the output to get the list of commands, properties, variable + names, etc. This has been around since at least v2.8.8 so it's pretty + available. It can definitely be used to filter available commands. +12. Consider getting rid of config.endl and instead using + ``io.open(newline='\n')`` or ``io.open(newline='\r\n')`` depending on + config. Then just write ``\n`` and let the streamwriter translation take + care of line endings. +13. Deal with the case that the command name is so long or that the statement + is nested so far that the open paren doesn't fit on the line and needs to + be wrapped. +14. Improve error messages for exceptions/assertions caused by malformed input. +15. Implement kwarg canonical ordering. Each kwarg parser has a canonical order + associated with it. The formatter can re-order arguments when formatting to + ensure that they are always written in the same order. +16. Add a generic CMAKE_FORMAT_TAG token type matching ``# cmake-format: XXX`` + or ``# cmf: XXX`` strings and don't necessarily treat them like comments. +17. Implement an ``unpad_hashruler`` configuration option. If true, dont + separate hashrulers from the leading comment character by a space. +18. Enable an option to parse sentinel comments `#< comment here` as argument + comments (instead of relying on existing columnization to merge + multiple argument comment lines). +19. Deduplicate code in in :code:`consume_comment` and + :code:`consume_trailing_comment` which are pretty much the same now. +20. Figure out what to do the :code:`needs_wrap=True` logic in + :code:`ArgGroupNode._reflow` if an argument wraps internally. In some cases + it looks bad, but in some cases it looks good. See case studies for + internally wrapped positionals. +21. The _statement_terminal hack is insufficient for dealing with parengroups. + We need some other mechanism to deal with multiple closing parentheses. + Probably we need to pass down some kind of :code:`StackContext` including + a member of :code:`n_open_parens`. +22. Deduplicate the common reflow logic of :code:`StatementNode` and + :code:`KwargGroupNode` (both of which nest). Also potentially the reflow + logic of :code:`ArgGroupNode` and :code:`PargGroupNode` (both of which + verticalize). +23. Rename :code:`ParseNode.node_type` to :code:`ParseNode.type` +24. There are a couple more TODO's in :code:`formatter.py` +25. Currently it's rather challenging to know in the formatter whether or not + a particular comment would be re-parsed as an argument comment if we were + to move it. This tag/mark might be moot if we just allow special comments + to be argument comments (which would allow them to be attached to a + particular argument). See the logic in PargGroupNode :code:`_reflow()` + where a comment is matched. +26. :code:`max_prefix_chars` isn't exactly the right thing to switch on. What + we really want is something that depends on the current indentation level. + I think what we really want is :code:`min_suffix_chars`, or, rather, given + a :code:`prefix_length` and a current indentation, look at how many chars + are available for content. If that number is too small, then force nesting. + Think about this more and look at some cases based on a selection of + statement names and keywords. Maybe compute some statistics on these and + use that to inform the selection. +27. Implement columnization, (successive kwarg children share are vertically + aligned, share a column). +28. Implement per-command :code:`layout_passes` +29. Add :code:`include` config option, allowing to refer to an external + configuration file. diff --git a/cmake_format/doc/usage.rst b/cmake_format/doc/usage.rst index 0ecc3a6..f05bde8 100644 --- a/cmake_format/doc/usage.rst +++ b/cmake_format/doc/usage.rst @@ -34,8 +34,13 @@ pleasant way. # How many spaces to tab for indent tab_size = 2 - # If arglists are longer than this, break them always - max_subargs_per_line = 3 + # If an argument group contains more than this many sub-groups (parg or kwarg + # groups), then force it to a vertical layout. + max_subgroups_hwrap = 2 + + # If a positinal argument group contains more than this many arguments, then + # force it to a vertical layout. + max_pargs_hwrap = 6 # If true, separate flow control names from their parentheses with a space separate_ctrl_name_with_space = False @@ -44,13 +49,21 @@ pleasant way. separate_fn_name_with_space = False # If a statement is wrapped to more than one line, than dangle the closing - # parenthesis on it's own line + # parenthesis on it's own line. dangle_parens = False + # If the trailing parenthesis must be 'dangled' on it's on line, then align it + # to this reference: `prefix`: the start of the statement, `prefix-indent`: the + # start of the statement, plus one indentation level, `child`: align to the + # column of the arguments + dangle_align = 'prefix' + + min_prefix_chars = 4 + # If the statement spelling length (including space and parenthesis is larger # than the tab width by more than this amoung, then force reject un-nested # layouts. - max_prefix_chars = 2 + max_prefix_chars = 10 # If a candidate layout is wrapped horizontally but it exceeds this many lines, # then reject the layout. @@ -77,9 +90,6 @@ pleasant way. # A list of command names which should always be wrapped always_wrap = [] - # Specify the order of wrapping algorithms during successive reflow attempts - algorithm_order = [0, 1, 2, 3, 4] - # If true, the argument lists which are known to be sortable will be sorted # lexicographicall enable_sort = True @@ -97,6 +107,10 @@ pleasant way. # only `command_case` is supported. per_command = {} + # A dictionary mapping layout nodes to a list of wrap decisions. See the + # documentation for more information. + layout_passes = {} + # -------------------------- # Comment Formatting Options @@ -203,7 +217,7 @@ Usage -i, --in-place -o OUTFILE_PATH, --outfile-path OUTFILE_PATH Where to write the formatted file. Default is stdout. - -c CONFIG_FILE [CONFIG_FILE ...], --config-file CONFIG_FILE [CONFIG_FILE ...], --config-files CONFIG_FILE [CONFIG_FILE ...] + -c CONFIG_FILE [CONFIG_FILE ...], --config-file CONFIG_FILE [CONFIG_FILE ...], --config-files CONFIG_FILE [CONFIG_FILE ...], --config CONFIG_FILE [CONFIG_FILE ...] path to configuration file(s) Formatter Configuration: @@ -212,8 +226,13 @@ Usage --line-width LINE_WIDTH How wide to allow formatted cmake files --tab-size TAB_SIZE How many spaces to tab for indent - --max-subargs-per-line MAX_SUBARGS_PER_LINE - If arglists are longer than this, break them always + --max-subgroups-hwrap MAX_SUBGROUPS_HWRAP + If an argument group contains more than this many sub- + groups (parg or kwarg groups), then force it to a + vertical layout. + --max-pargs-hwrap MAX_PARGS_HWRAP + If a positinal argument group contains more than this + many arguments, then force it to a vertical layout. --separate-ctrl-name-with-space [SEPARATE_CTRL_NAME_WITH_SPACE] If true, separate flow control names from their parentheses with a space @@ -222,7 +241,14 @@ Usage a space --dangle-parens [DANGLE_PARENS] If a statement is wrapped to more than one line, than - dangle the closing parenthesis on it's own line + dangle the closing parenthesis on it's own line. + --dangle-align {prefix,prefix-indent,child,off} + If the trailing parenthesis must be 'dangled' on it's + on line, then align it to this reference: `prefix`: + the start of the statement, `prefix-indent`: the start + of the statement, plus one indentation level, `child`: + align to the column of the arguments + --min-prefix-chars MIN_PREFIX_CHARS --max-prefix-chars MAX_PREFIX_CHARS If the statement spelling length (including space and parenthesis is larger than the tab width by more than @@ -240,9 +266,6 @@ Usage case --always-wrap [ALWAYS_WRAP [ALWAYS_WRAP ...]] A list of command names which should always be wrapped - --algorithm-order [ALGORITHM_ORDER [ALGORITHM_ORDER ...]] - Specify the order of wrapping algorithms during - successive reflow attempts --enable-sort [ENABLE_SORT] If true, the argument lists which are known to be sortable will be sorted lexicographicall diff --git a/cmake_format/format_tests.py b/cmake_format/format_tests.py deleted file mode 100644 index fdb6d1c..0000000 --- a/cmake_format/format_tests.py +++ /dev/null @@ -1,1284 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=too-many-lines - -from __future__ import unicode_literals -import difflib -import io -import logging -import os -import sys -import unittest - -from cmake_format import __main__ -from cmake_format import configuration -from cmake_format import parse_funs - - -def strip_indent(content, indent=6): - """ - Strings used in this file are indented by 6-spaces to keep them readable - within the python code that they are embedded. Remove those 6-spaces from - the front of each line before running the tests. - """ - - # NOTE(josh): don't use splitlines() so that we get the same result - # regardless of windows or unix line endings in content. - return '\n'.join([line[indent:] for line in content.split('\n')]) - - -class TestCanonicalFormatting(unittest.TestCase): - """ - Given a bunch of example inputs, ensure that the output is as expected. - """ - - def __init__(self, *args, **kwargs): - super(TestCanonicalFormatting, self).__init__(*args, **kwargs) - self.config = configuration.Configuration() - self.parse_db = parse_funs.get_parse_db() - - def setUp(self): - self.config.fn_spec.add( - 'foo', - flags=['BAR', 'BAZ'], - kwargs={ - "HEADERS": '*', - "SOURCES": '*', - "DEPENDS": '*' - }) - self.parse_db.update( - parse_funs.get_legacy_parse(self.config.fn_spec).kwargs) - - def tearDown(self): - pass - - def do_format_test(self, input_str, output_str, strip_len=6): - """ - Run the formatter on the input string and assert that the result matches - the output string - """ - - input_str = strip_indent(input_str, strip_len) - output_str = strip_indent(output_str, strip_len) - - if sys.version_info[0] < 3: - assert isinstance(input_str, unicode) - actual_str = __main__.process_file(self.config, input_str) - delta_lines = list(difflib.unified_diff(output_str.split('\n'), - actual_str.split('\n'))) - delta = '\n'.join(delta_lines[2:]) - - if actual_str != output_str: - message = ('Input text:\n-----------------\n{}\n' - 'Output text:\n-----------------\n{}\n' - 'Expected Output:\n-----------------\n{}\n' - 'Diff:\n-----------------\n{}' - .format(input_str, - actual_str, - output_str, - delta)) - if sys.version_info[0] < 3: - message = message.encode('utf-8') - raise AssertionError(message) - - -class TestSomeExamples(TestCanonicalFormatting): - """ - Given a bunch of example inputs, ensure that the output is as expected. - """ - - def test_collapse_additional_newlines(self): - self.do_format_test("""\ - # The following multiple newlines should be collapsed into a single newline - - - - - cmake_minimum_required(VERSION 2.8.11) - project(cmake_format_test) - """, """\ - # The following multiple newlines should be collapsed into a single newline - - cmake_minimum_required(VERSION 2.8.11) - project(cmake_format_test) - """) - - def test_multiline_reflow(self): - self.do_format_test("""\ - # This multiline-comment should be reflowed - # into a single comment - # on one line - """, """\ - # This multiline-comment should be reflowed into a single comment on one line - """) - - def test_comment_before_command(self): - self.config.max_subargs_per_line = 6 - self.do_format_test("""\ - # This comment should remain right before the command call. - # Furthermore, the command call should be formatted - # to a single line. - add_subdirectories(foo bar baz - foo2 bar2 baz2) - """, """\ - # This comment should remain right before the command call. Furthermore, the - # command call should be formatted to a single line. - add_subdirectories(foo bar baz foo2 bar2 baz2) - """) - - def test_long_args_command_split(self): - self.do_format_test("""\ - # This very long command should be split to multiple lines - set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) - """, """\ - # This very long command should be split to multiple lines - set(HEADERS very_long_header_name_a.h very_long_header_name_b.h - very_long_header_name_c.h) - """) - - def test_lots_of_args_command_split(self): - self.do_format_test("""\ - # This command should be split into one line per entry because it has a long - # argument list. - set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc) - """, """\ - # This command should be split into one line per entry because it has a long - # argument list. - set(SOURCES - source_a.cc - source_b.cc - source_d.cc - source_e.cc - source_f.cc - source_g.cc) - """) - - # TODO(josh): figure out why this test elicits different behavior than the - # whole-file demo. - def test_string_preserved_during_split(self): - self.do_format_test("""\ - # The string in this command should not be split - set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") - """, """\ - # The string in this command should not be split - set_target_properties(foo bar baz - PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") - """) - - def test_long_arg_on_newline(self): - self.do_format_test("""\ - # This command has a very long argument and can't be aligned with the command - # end, so it should be moved to a new line with block indent + 1. - some_long_command_name("Some very long argument that really needs to be on the next line.") - """, """\ - # This command has a very long argument and can't be aligned with the command - # end, so it should be moved to a new line with block indent + 1. - some_long_command_name( - "Some very long argument that really needs to be on the next line.") - """) - - def test_long_kwargarg_on_newline(self): - self.do_format_test("""\ - # This situation is similar but the argument to a KWARG needs to be on a - # newline instead. - set(CMAKE_CXX_FLAGS "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") - """, """\ - # This situation is similar but the argument to a KWARG needs to be on a newline - # instead. - set(CMAKE_CXX_FLAGS - "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") - """) - - def test_argcomment_preserved_and_reflowed(self): - self.do_format_test("""\ - set(HEADERS header_a.h header_b.h # This comment should - # be preserved, moreover it should be split - # across two lines. - header_c.h header_d.h) - """, """\ - set(HEADERS - header_a.h - header_b.h # This comment should be preserved, moreover it should be split - # across two lines. - header_c.h - header_d.h) - """) - - def test_argcomments_force_reflow(self): - self.config.line_width = 140 - self.do_format_test("""\ - cmake_parse_arguments(ARG - "SILENT" # optional keywords - "" # one value keywords - "" # multi value keywords - ${ARGN}) - """, """\ - cmake_parse_arguments(ARG - "SILENT" # optional keywords - "" # one value keywords - "" # multi value keywords - ${ARGN}) - """) - - def test_format_off(self): - self.do_format_test("""\ - # This part of the comment should - # be formatted - # but... - # cmake-format: off - # This bunny should remain untouched: - # .   _ ∩ - #   レヘヽ| | - #     (・x・) - #    c( uu} - # cmake-format: on - # while this part should - # be formatted again - """, """\ - # This part of the comment should be formatted but... - # cmake-format: off - # This bunny should remain untouched: - # .   _ ∩ - #   レヘヽ| | - #     (・x・) - #    c( uu} - # cmake-format: on - # while this part should be formatted again - """) - - def test_paragraphs_preserved(self): - self.do_format_test("""\ - # This is a paragraph - # - # This is a second paragraph - # - # This is a third paragraph - """, """\ - # This is a paragraph - # - # This is a second paragraph - # - # This is a third paragraph - """) - - def test_todo_preserved(self): - self.do_format_test("""\ - # This is a comment - # that should be joined but - # TODO(josh): This todo should not be joined with the previous line. - # NOTE(josh): Also this should not be joined with the todo. - """, """\ - # This is a comment that should be joined but - # TODO(josh): This todo should not be joined with the previous line. - # NOTE(josh): Also this should not be joined with the todo. - """) - - def test_complex_nested_stuff(self): - self.config.autosort = False - self.do_format_test("""\ - if(foo) - if(sbar) - # This comment is in-scope. - add_library(foo_bar_baz foo.cc bar.cc # this is a comment for arg2 - # this is more comment for arg2, it should be joined with the first. - baz.cc) # This comment is part of add_library - - other_command(some_long_argument some_long_argument) # this comment is very long and gets split across some lines - - other_command(some_long_argument some_long_argument some_long_argument) # this comment is even longer and wouldn't make sense to pack at the end of the command so it gets it's own lines - endif() - endif() - """, """\ - if(foo) - if(sbar) - # This comment is in-scope. - add_library(foo_bar_baz - foo.cc - bar.cc # this is a comment for arg2 this is more comment for - # arg2, it should be joined with the first. - baz.cc) # This comment is part of add_library - - other_command(some_long_argument some_long_argument) # this comment is very - # long and gets split - # across some lines - - other_command(some_long_argument some_long_argument some_long_argument) - # this comment is even longer and wouldn't make sense to pack at the end of - # the command so it gets it's own lines - endif() - endif() - """) - - def test_custom_command(self): - self.do_format_test("""\ - # This very long command should be broken up along keyword arguments - foo(nonkwarg_a nonkwarg_b HEADERS a.h b.h c.h d.h e.h f.h SOURCES a.cc b.cc d.cc DEPENDS foo bar baz) - """, """\ - # This very long command should be broken up along keyword arguments - foo(nonkwarg_a nonkwarg_b - HEADERS a.h - b.h - c.h - d.h - e.h - f.h - SOURCES a.cc b.cc d.cc - DEPENDS foo - bar baz) - """) - - def test_always_wrap(self): - self.do_format_test("""\ - foo(nonkwarg_a HEADERS a.h SOURCES a.cc DEPENDS foo) - """, """\ - foo(nonkwarg_a HEADERS a.h SOURCES a.cc DEPENDS foo) - """) - self.config.always_wrap = ['foo'] - self.do_format_test("""\ - foo(nonkwarg_a HEADERS a.h SOURCES a.cc DEPENDS foo) - """, """\ - foo(nonkwarg_a - HEADERS a.h - SOURCES a.cc - DEPENDS foo) - """) - - def test_multiline_string(self): - self.do_format_test("""\ - foo(some_arg some_arg " - This string is on multiple lines - ") - """, """\ - foo(some_arg some_arg " - This string is on multiple lines - ") - """) - - def test_some_string_stuff(self): - self.do_format_test("""\ - # This command uses a string with escaped quote chars - foo(some_arg some_arg "This is a \\"string\\" within a string") - - # This command uses an empty string - foo(some_arg some_arg "") - - # This command uses a multiline string - foo(some_arg some_arg " - This string is on multiple lines - ") - """, """\ - # This command uses a string with escaped quote chars - foo(some_arg some_arg "This is a \\"string\\" within a string") - - # This command uses an empty string - foo(some_arg some_arg "") - - # This command uses a multiline string - foo(some_arg some_arg " - This string is on multiple lines - ") - """) - - def test_format_off_code(self): - self.do_format_test("""\ - # No, I really want this to look ugly - # cmake-format: off - add_library(a b.cc - c.cc d.cc - e.cc) - # cmake-format: on - """, """\ - # No, I really want this to look ugly - # cmake-format: off - add_library(a b.cc - c.cc d.cc - e.cc) - # cmake-format: on - """) - - def test_multiline_statement_comment_idempotent(self): - self.do_format_test("""\ - set(HELLO hello world!) # TODO(josh): fix this bad code with some change that - # takes mutiple lines to explain - """, """\ - set(HELLO hello world!) # TODO(josh): fix this bad code with some change that - # takes mutiple lines to explain - """) - - def test_function_def(self): - self.do_format_test("""\ - function(forbarbaz arg1) - do_something(arg1 ${ARGN}) - endfunction() - """, """\ - function(forbarbaz arg1) - do_something(arg1 ${ARGN}) - endfunction() - """) - - def test_macro_def(self): - self.do_format_test("""\ - macro(forbarbaz arg1) - do_something(arg1 ${ARGN}) - endmacro() - """, """\ - macro(forbarbaz arg1) - do_something(arg1 ${ARGN}) - endmacro() - """) - - def test_foreach(self): - self.config.max_subargs_per_line = 6 - self.do_format_test("""\ - foreach(forbarbaz arg1 arg2 arg3) - message(hello ${foobarbaz}) - endforeach() - """, """\ - foreach(forbarbaz arg1 arg2 arg3) - message(hello ${foobarbaz}) - endforeach() - """) - - def test_while(self): - self.config.max_subargs_per_line = 6 - self.do_format_test("""\ - - while(forbarbaz arg1 arg2 arg3) - message(hello ${foobarbaz}) - endwhile() - """, """\ - while(forbarbaz arg1 arg2 arg3) - message(hello ${foobarbaz}) - endwhile() - """) - - def test_ctrl_space(self): - self.config.separate_ctrl_name_with_space = True - self.do_format_test("""\ - if(foo) - myfun(foo bar baz) - endif() - """, """\ - if (foo) - myfun(foo bar baz) - endif () - """) - - def test_fn_space(self): - self.config.separate_fn_name_with_space = True - self.do_format_test("""\ - myfun(foo bar baz) - """, """\ - myfun (foo bar baz) - """) - - def test_preserve_separator(self): - self.do_format_test("""\ - # -------------------- - # This is some - # text that I expect - # to reflow - # -------------------- - """, """\ - # -------------------- - # This is some text that I expect to reflow - # -------------------- - """) - - self.do_format_test("""\ - # !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* - # This is some - # text that I expect - # to reflow - # !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* - """, """\ - # !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* - # This is some text that I expect to reflow - # !@#$^&*!@#$%^&*!@#$%^&*!@#$%^&* - """) - - self.do_format_test("""\ - # ----Not Supported---- - # This is some - # text that I expect - # to reflow - # ----Not Supported---- - """, """\ - # ----Not Supported---- - # This is some text that I expect to reflow - # ----Not Supported---- - """) - - def test_bullets(self): - self.do_format_test("""\ - # This is a bulleted list: - # - # * item 1 - # * item 2 - # this line gets merged with item 2 - # * item 3 is really long and needs to be wrapped to a second line because it wont all fit on one line without wrapping. - # - # But the list has ended and this line is free. And - # * this is not a bulleted list - # * and it will be - # * merged - """, """\ - # This is a bulleted list: - # - # * item 1 - # * item 2 this line gets merged with item 2 - # * item 3 is really long and needs to be wrapped to a second line because it - # wont all fit on one line without wrapping. - # - # But the list has ended and this line is free. And * this is not a bulleted - # list * and it will be * merged - """) - - def test_enum_lists(self): - self.do_format_test("""\ - # This is a bulleted list: - # - # 1. item - # 2. item - # 3. item - # - # 4. item - # 5. item - # 6. item - # - # 1. item - # 3. item - # 5. item - # 6. item - # 6. item is really long and needs to be wrapped to a second line because it wont all fit on one line without wrapping. - # 7. item - # 9. item - # 9. item - # 9. item - # 9. item - # 9. item - # - """, """\ - # This is a bulleted list: - # - # 1. item - # 2. item - # 3. item - # - # 1. item - # 2. item - # 3. item - # - # 1. item - # 2. item - # 3. item - # 4. item - # 5. item is really long and needs to be wrapped to a second line because it wont - # all fit on one line without wrapping. - # 6. item - # 7. item - # 8. item - # 9. item - # 10. item - # 11. item - # - """) - - def test_nested_bullets(self): - self.do_format_test("""\ - # This is a bulleted list: - # - # * item 1 - # * item 2 - # - # * item 3 - # * item 4 - # - # * item 5 - # * item 6 - # - # * item 7 - # * item 8 - """, """\ - # This is a bulleted list: - # - # * item 1 - # * item 2 - # - # * item 3 - # * item 4 - # - # * item 5 - # * item 6 - # - # * item 7 - # * item 8 - """) - - def test_comment_fence(self): - self.do_format_test("""\ - # ~~~~~~ - # This is some - # verbatim text - # that should not be - # formatted - # ``````` - """, """\ - # ~~~ - # This is some - # verbatim text - # that should not be - # formatted - # ~~~ - """) - - def test_bracket_comments(self): - - self.do_format_test("""\ - #[[This is a bracket comment. - It is preserved verbatim, but trailing whitespace is removed. - So things like --this-- Are fine:]] - """, """\ - #[[This is a bracket comment. - It is preserved verbatim, but trailing whitespace is removed. - So things like --this-- Are fine:]] - """) - - self.do_format_test("""\ - if(foo) - #[==[This is a bracket comment at some nested level - # it is preserved verbatim, but trailing - # whitespace is removed.]==] - endif() - """, """\ - if(foo) - #[==[This is a bracket comment at some nested level - # it is preserved verbatim, but trailing - # whitespace is removed.]==] - endif() - """) - - # Make sure bracket comments are kept inline in their function call - self.do_format_test("""\ - message("First Argument" #[[Bracket Comment]] "Second Argument") - """, """\ - message("First Argument" #[[Bracket Comment]] "Second Argument") - """) - - def test_comment_after_command(self): - self.do_format_test("""\ - foo_command() # comment - """, """\ - foo_command() # comment - """) - - self.do_format_test("""\ - foo_command() # this is a long comment that exceeds the desired page width and will be wrapped to a newline - """, """\ - foo_command() # this is a long comment that exceeds the desired page width and - # will be wrapped to a newline - """) - - def test_arg_just_fits(self): - """ - Ensure that if an argument *just* fits that it isn't superfluously wrapped - """ - - self.do_format_test("""\ - message(FATAL_ERROR "81 character line ----------------------------------------") - """, """\ - message( - FATAL_ERROR "81 character line ----------------------------------------") - """) - - self.do_format_test("""\ - message(FATAL_ERROR - "100 character line ----------------------------------------------------------" - ) # Closing parenthesis is indented one space! - """, """\ - message( - FATAL_ERROR - "100 character line ----------------------------------------------------------" - ) # Closing parenthesis is indented one space! - """) - - self.do_format_test("""\ - message( - "100 character line ----------------------------------------------------------------------" - ) # Closing parenthesis is indented one space! - """, """\ - message( - "100 character line ----------------------------------------------------------------------" - ) # Closing parenthesis is indented one space! - """) - - def test_dangle_parens(self): - self.config.dangle_parens = True - self.config.max_subargs_per_line = 6 - self.do_format_test("""\ - foo_command() - foo_command(arg1) - foo_command(arg1) # comment - """, """\ - foo_command() - foo_command(arg1) - foo_command(arg1) # comment - """) - - self.do_format_test("""\ - some_long_command_name(longargname longargname longargname longargname longargname) - """, """\ - some_long_command_name(longargname longargname longargname longargname - longargname) - """) - - self.do_format_test("""\ - if(foo) - some_long_command_name(longargname longargname longargname longargname longargname) - endif() - """, """\ - if(foo) - some_long_command_name(longargname longargname longargname longargname - longargname) - endif() - """) - - self.do_format_test("""\ - some_long_command_name(longargname longargname longargname longargname longargname longargname longargname longargname) - """, """\ - some_long_command_name( - longargname - longargname - longargname - longargname - longargname - longargname - longargname - longargname - ) - """) - - self.do_format_test("""\ - target_include_directories(target INTERFACE $) - """, """\ - target_include_directories( - target - INTERFACE $ - ) - """) - - def test_windows_line_endings_input(self): - self.do_format_test( - " #[[*********************************************\r\n" - " * Information line 1\r\n" - " * Information line 2\r\n" - " ************************************************]]\r\n", """\ - #[[********************************************* - * Information line 1 - * Information line 2 - ************************************************]]\n""") - - def test_windows_line_endings_output(self): - config_dict = self.config.as_dict() - config_dict['line_ending'] = 'windows' - self.config = configuration.Configuration(**config_dict) - - self.do_format_test( - """\ - #[[********************************************* - * Information line 1 - * Information line 2 - ************************************************]]""", - " #[[*********************************************\r\n" - " * Information line 1\r\n" - " * Information line 2\r\n" - " ************************************************]]\r\n") - - def test_auto_line_endings(self): - config_dict = self.config.as_dict() - config_dict['line_ending'] = 'auto' - self.config = configuration.Configuration(**config_dict) - - self.do_format_test( - " #[[*********************************************\r\n" - " * Information line 1\r\n" - " * Information line 2\r\n" - " ************************************************]]\r\n", - " #[[*********************************************\r\n" - " * Information line 1\r\n" - " * Information line 2\r\n" - " ************************************************]]\r\n") - - def test_keyword_case(self): - config_dict = self.config.as_dict() - config_dict['keyword_case'] = 'upper' - self.config = configuration.Configuration(**config_dict) - self.do_format_test( - """\ - foo(bar baz) - """, """\ - foo(BAR BAZ) - """) - - config_dict = self.config.as_dict() - config_dict['keyword_case'] = 'lower' - self.config = configuration.Configuration(**config_dict) - - self.do_format_test( - """\ - foo(bar baz) - """, """\ - foo(bar baz) - """) - - config_dict = self.config.as_dict() - config_dict['command_case'] = 'unchanged' - self.config = configuration.Configuration(**config_dict) - - self.do_format_test("""\ - foo(BaR bAz) - """, """\ - foo(bar baz) - """) - - def test_command_case(self): - self.do_format_test( - """\ - FOO(bar baz) - """, """\ - foo(bar baz) - """) - - config_dict = self.config.as_dict() - config_dict['command_case'] = 'upper' - self.config = configuration.Configuration(**config_dict) - - self.do_format_test( - """\ - foo(bar baz) - """, """\ - FOO(bar baz) - """) - - config_dict = self.config.as_dict() - config_dict['command_case'] = 'unchanged' - self.config = configuration.Configuration(**config_dict) - - self.do_format_test("""\ - FoO(bar baz) - """, """\ - FoO(bar baz) - """) - - def test_comment_in_statement(self): - self.do_format_test("""\ - add_library(foo - # This comment is not attached to an argument - bar.cc - foo.cc) - """, """\ - add_library(foo - # This comment is not attached to an argument - bar.cc foo.cc) - """) - - def test_comment_at_end_of_statement(self): - self.do_format_test("""\ - add_library(foo bar.cc foo.cc - # This comment is not attached to an argument - ) - """, """\ - add_library(foo - bar.cc foo.cc - # This comment is not attached to an argument - ) - """) - - self.do_format_test("""\ - target_link_libraries(libraryname PUBLIC - ${COMMON_LIBRARIES} - # add more library dependencies here - ) - """, """\ - target_link_libraries(libraryname - PUBLIC ${COMMON_LIBRARIES} - # add more library dependencies here - ) - """) - - self.do_format_test("""\ - find_package(foobar REQUIRED - COMPONENTS some_component - # some_other_component - # This is a very long comment, and actually the second comment in this row. - ) - """, """\ - find_package(foobar REQUIRED - COMPONENTS some_component - # some_other_component - # This is a very long comment, and actually the second - # comment in this row. - ) - """) - - def test_comment_in_kwarg(self): - self.do_format_test("""\ - install(TARGETS foob - ARCHIVE DESTINATION foobar - # this is a line comment, not a comment on foobar - COMPONENT baz) - """, """\ - install(TARGETS foob - ARCHIVE DESTINATION foobar - # this is a line comment, not a comment on foobar - COMPONENT baz) - """) - - def test_algoorder_preference(self): - self.config.max_subargs_per_line = 10 - self.do_format_test("""\ - some_long_command_name(longargument longargument longargument longargument - longargument longargument) - """, """\ - some_long_command_name(longargument longargument longargument longargument - longargument longargument) - """) - - self.config.algorithm_order = [0, 3] - self.do_format_test("""\ - some_long_command_name(longargument longargument longargument longargument - longargument longargument) - """, """\ - some_long_command_name( - longargument longargument longargument longargument longargument longargument) - """) - - def test_elseif(self): - self.do_format_test("""\ - if(MSVC) - - elseif((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - OR CMAKE_COMPILER_IS_GNUCC - OR CMAKE_COMPILER_IS_GNUCXX) - - endif() - """, """\ - if(MSVC) - - elseif((CMAKE_CXX_COMPILER_ID STREQUAL "Clang") - OR CMAKE_COMPILER_IS_GNUCC - OR CMAKE_COMPILER_IS_GNUCXX) - - endif() - """) - - def test_elseif_else_control_space(self): - self.config.separate_ctrl_name_with_space = True - self.do_format_test("""\ - if(foo) - elseif(bar) - else() - endif() - """, """\ - if (foo) - - elseif (bar) - - else () - - endif () - """) - - def test_disable_markup(self): - self.config.enable_markup = False - self.do_format_test("""\ - # don't reflow - # or parse markup - # for these lines - """, """\ - # don't reflow - # or parse markup - # for these lines - """) - - def test_literal_first_comment(self): - self.do_format_test("""\ - # This comment - # is reflowed - - # This comment - # is reflowed - """, """\ - # This comment is reflowed - - # This comment is reflowed - """) - - self.config.first_comment_is_literal = True - self.do_format_test("""\ - # This comment - # is not reflowed - - # This comment - # is reflowed - """, """\ - # This comment - # is not reflowed - - # This comment is reflowed - """) - - def test_shebang_preserved(self): - self.do_format_test("""\ - #!/usr/bin/cmake -P - """, """\ - #!/usr/bin/cmake -P - """) - - def test_preserve_copyright(self): - self.do_format_test("""\ - # Copyright 2018: Josh Bialkowski - # This text should not be reflowed - # because it's a copyright - """, """\ - # Copyright 2018: Josh Bialkowski This text should not be reflowed because it's - # a copyright - """) - - self.config.literal_comment_pattern = " Copyright.*" - self.do_format_test("""\ - # Copyright 2018: Josh Bialkowski - # This text should not be reflowed - # because it's a copyright - """, """\ - # Copyright 2018: Josh Bialkowski - # This text should not be reflowed - # because it's a copyright - """) - - def test_kwarg_match_consumes(self): - self.do_format_test("""\ - add_test(NAME myTestName COMMAND testCommand --run_test=@quick) - """, """\ - add_test(NAME myTestName COMMAND testCommand --run_test=@quick) - """) - - def test_byte_order_mark(self): - self.do_format_test("""\ - \ufeffcmake_minimum_required(VERSION 2.8.11) - project(cmake_format_test) - """, """\ - cmake_minimum_required(VERSION 2.8.11) - project(cmake_format_test) - """) - - self.config.emit_byteorder_mark = True - self.do_format_test("""\ - cmake_minimum_required(VERSION 2.8.11) - project(cmake_format_test) - """, """\ - \ufeffcmake_minimum_required(VERSION 2.8.11) - project(cmake_format_test) - """) - - def test_percommand_override(self): - self.do_format_test("""\ - FoO(bar baz) - """, """\ - foo(bar baz) - """) - - self.config.per_command["foo"] = { - "command_case": "unchanged" - } - self.do_format_test("""\ - FoO(bar baz) - """, """\ - FoO(bar baz) - """) - - def test_quoted_assignment_literal(self): - self.do_format_test("""\ - target_compile_definitions(foo PUBLIC BAR="Quoted String" BAZ_______________________Z) - """, """\ - target_compile_definitions(foo - PUBLIC - BAR="Quoted String" - BAZ_______________________Z) - """) - - def test_keyword_comment(self): - self.do_format_test("""\ - find_package(package REQUIRED - COMPONENTS # -------------------------------------- - # @TODO: This has to be filled manually - # -------------------------------------- - this_is_a_really_long_word_foo) - """, """\ - find_package(package REQUIRED - COMPONENTS # -------------------------------------- - # @TODO: This has to be filled manually - # -------------------------------------- - this_is_a_really_long_word_foo) - """) - - def test_example_file(self): - thisdir = os.path.dirname(__file__) - infile_path = os.path.join(thisdir, 'test', 'test_in.cmake') - outfile_path = os.path.join(thisdir, 'test', 'test_out.cmake') - - with io.open(infile_path, 'r', encoding='utf8') as infile: - infile_text = infile.read() - with io.open(outfile_path, 'r', encoding='utf8') as outfile: - outfile_text = outfile.read() - - self.do_format_test(infile_text, outfile_text, strip_len=0) - - def test_one_char_short_hpack_rparen_case(self): - # This was a particularly rare edge case. The situation is that the - # the arguments are one character shy of fitting in the configured line - # width, the statement column is the same as the indent column, and - # all the arguments are positional. The problem was that the hpack if - # possible logic did not account for the final paren. A fix is in place. - self.config.line_width = 132 - self.config.tab_size = 4 - self.do_format_test(""" - set(cubepp_HDRS - ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/AppDelegate.h - ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/DemoViewController.h) - """, """\ - set(cubepp_HDRS ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/AppDelegate.h - ${CMAKE_CURRENT_SOURCE_DIR}/macOS/cubepp/DemoViewController.h) - """) - - def test_rulers_preserved_without_markup(self): - self.config.enable_markup = False - self.do_format_test(""" - ######################################################################### - # Custom targets - ######################################################################### - """, """\ - ######################################################################### - # Custom targets - ######################################################################### - """) - - def test_canonical_spelling(self): - self.do_format_test("""\ - ExternalProject_Add( - foobar - URL https://foobar.baz/latest.tar.gz - TLS_VERIFY TRUE - CONFIGURE_COMMAND configure - BUILD_COMMAND make - INSTALL_COMMAND make install) - """, """ - ExternalProject_Add(foobar - URL https://foobar.baz/latest.tar.gz - TLS_VERIFY TRUE - CONFIGURE_COMMAND configure - BUILD_COMMAND make - INSTALL_COMMAND make install) - """[1:]) - - def test_comment_hashrulers(self): - self.config.line_width = 74 - self.do_format_test(""" - ################## - # This comment has a long block before it. - ############# - """, """\ - # ######################################################################## - # This comment has a long block before it. - # ######################################################################## - """) - self.do_format_test(""" - ############################################### - # This is a section in the CMakeLists.txt file. - ############ - # This stuff below here - # should get re-flowed like - # normal comments. Across multiple - #lines and - # beyond. - """, """\ - # ######################################################################## - # This is a section in the CMakeLists.txt file. - # ######################################################################## - # This stuff below here should get re-flowed like normal comments. Across - # multiple lines and beyond. - """) - - # verify the original behavior is as described (they truncate to one #) - self.config.hashruler_min_length = 1000 - self.do_format_test(""" - ########################################################################## - # This comment has a long block before it. - ########################################################################## - """, """\ - # - # This comment has a long block before it. - # - """) - self.do_format_test(""" - ########################################################################## - # This is a section in the CMakeLists.txt file. - ########################################################################## - # This stuff below here - # should get re-flowed like - # normal comments. Across multiple - #lines and - # beyond. - """, """\ - # - # This is a section in the CMakeLists.txt file. - # - # This stuff below here should get re-flowed like normal comments. Across - # multiple lines and beyond. - """) - - # make sure changing hashruler_min_length works correctly - # self.config.enable_markup = False - for min_width in {3, 5, 7, 9}: - self.config.hashruler_min_length = min_width - - # NOTE(josh): these tests use short rulers that wont be picked up by - # the default pattern - self.config.ruler_pattern = (r'#{%d}#*' % (min_width - 1)) - - just_shy = '#' * (min_width - 1) - just_right = '#' * min_width - longer = '#' * (min_width + 2) - full_line = '#' * (self.config.line_width - 2) - - self.do_format_test(""" - # A comment: min_width={min_width}, just_shy - {just_shy} - """.format(min_width=min_width, just_shy=just_shy), """\ - # A comment: min_width={min_width}, just_shy - # - """.format(min_width=min_width)) - - self.do_format_test(""" - # A comment: min_width={min_width}, just_right - {just_right} - """.format(min_width=min_width, just_right=just_right), """\ - # A comment: min_width={min_width}, just_right - # {full_line} - """.format(min_width=min_width, full_line=full_line)) - - self.do_format_test(""" - # A comment: min_width={min_width}, longer - {longer} - """.format(min_width=min_width, longer=longer), """\ - # A comment: min_width={min_width}, longer - # {full_line} - """.format(min_width=min_width, full_line=full_line)) - - -if __name__ == '__main__': - format_str = '[%(levelname)-4s] %(filename)s:%(lineno)-3s: %(message)s' - logging.basicConfig(level=logging.DEBUG, - format=format_str, - datefmt='%Y-%m-%d %H:%M:%S', - filemode='w') - unittest.main() diff --git a/cmake_format/formatter.py b/cmake_format/formatter.py index 34d496b..a81cd93 100644 --- a/cmake_format/formatter.py +++ b/cmake_format/formatter.py @@ -3,12 +3,12 @@ from __future__ import print_function from __future__ import unicode_literals +import contextlib import io import logging import re import sys -from cmake_format import common from cmake_format import lexer from cmake_format import markup from cmake_format import parser @@ -28,9 +28,6 @@ MATCH_TYPES = BLOCK_TYPES + GROUP_TYPES + SCALAR_TYPES + PAREN_TYPES -LAYOUT_PASS_COUNT = 4 -USE_NEW_ALGORITHM = False - def clamp(value, min_value, max_value): """Simple double-ended saturation function.""" @@ -90,22 +87,6 @@ def normalize_line_endings(instr): return re.sub('[ \t\f\v]*((\r?\n)|(\r\n?))', '\n', instr) -# TODO(josh): remove this -class WrapAlgo(common.EnumObject): - """ - Packing algorithm used - """ - _id_map = {} - - -WrapAlgo.HPACK = WrapAlgo(0) # Horizontal packing: no wrapping -WrapAlgo.HWRAP = WrapAlgo(1) # Horizontal wrapping -WrapAlgo.VPACK = WrapAlgo(2) # Vertical packing: each element on it's own line -WrapAlgo.KWNVPACK = WrapAlgo(3) # keyword nested vertical packing -WrapAlgo.PNVPACK = WrapAlgo(4) # parentheses nested vertical packing -WrapAlgo.COUNT = WrapAlgo(5) - - def need_paren_space(spelling, config): """ Return whether or not we need a space between the statement name and the @@ -124,6 +105,23 @@ def need_paren_space(spelling, config): return config.separate_fn_name_with_space +def is_line_comment(node): + """ + Return true if the node is a pure parser node holding a line comment (i.e. + not a bracket comment) + """ + if isinstance(node, CommentNode): + node = node.pnode + + if not isinstance(node, parser.TreeNode): + return False + + if not node.children: + return False + + return node.children[-1].type == TokenType.COMMENT + + class Cursor(object): """ Lightweight class to encode integer positions in a 2d grid. @@ -137,12 +135,19 @@ def __init__(self, x, y): self.y = y # col def __add__(self, other): + """Cursor addition is element-wise (i.e. vector) addition""" return Cursor(self.x + other[0], self.y + other[1]) def __sub__(self, other): + """Cursor subtraction is element-wise (i.e. vector) subtraction""" return Cursor(self.x - other[0], self.y - other[1]) def __getitem__(self, idx): + """ + A cursor can be accessed as a two-element array. For a cursor `c`, + `c[0]` refers to the row (`x`) and `c[1]` refers to the column (`y`). + """ + if idx == 0: return self.x if idx == 1: @@ -151,6 +156,11 @@ def __getitem__(self, idx): raise IndexError('Cursor indices must be 0 or 1') def __setitem__(self, idx, value): + """ + Cursor elements can be assigned as a two-element array. For a cursor `c`, + `c[0]` refers to the row (`x`) and `c[1]` refers to the column (`y`). + """ + if idx == 0: self.x = value return @@ -161,79 +171,37 @@ def __setitem__(self, idx, value): raise IndexError('Cursor indices must be 0 or 1') def __repr__(self): + """ + String representation is like "Cursor(12,34)" + """ return "Cursor({},{})".format(self.x, self.y) + def clone(self): + """ + Return a new `Cursor` object with the same value as this one. + """ + return Cursor(*self) -def default_accept_layout( - config, node_path, nested, vertical, start_extent, end_extent): + +class StackContext(object): """ - Return true if the given layout is acceptable. + Aggregate information about the current stack. This object is passed down + through all of the nested :code:`reflow()` function calls. """ - # If the bounding box overflows the column limit then the layout is - # automatically voided - if end_extent[1] > config.linewidth: - return False - - # Commands are never wrapped vertically - if node_path[-1] == "COMMAND": - if vertical: - return False - - size = end_extent - start_extent - depth = len(node_path) - 1 - - prefix_width = len(node_path[-1]) - if depth == 0: - # If depth is zero, then we are at statement-depth, and we need to account - # for extra characters when determining the width of the prefix - prefix_width += 1 # For the left-paren - if need_paren_space(node_path[-1], config): - prefix_width += 1 # For the space before the paren - - if not vertical: - # Regardless of nesting, if the content is wrapped horizontally but it - # exceeds the configured maximum number of lines we must reject it - # TODO(josh): figure out how to subtract out any terminal comment - # contributions to the size, as noted in the algorithm doc. - if size[0] > config.max_lines_hwrap: - return False - - # Or if this nodepath is marked to always be vertical layout - pathstr = "/".join(node_path) - if pathstr in config.always_wrap: - return False - - if nested: - # If the statement or keyword spelling is too short, then nesting doesn't - # make sense because (nest + tab-width) will take us right back to the - # same column as without nesting. - if prefix_width <= config.tab_size: - return False - else: - # If the statement or keyword spelling is too long, then any wrapping also - # requires nesting. - if size[0] > 1: - if prefix_width - config.tab_size > config.max_prefix_chars: - return False - - return True - + def __init__(self, config): + self.config = config + self.node_path = [] -def get_nest_wrap(passno): - """ - Return (nest, wrap) booleans depending on the integer passno - """ - if passno > 3: - logger.warning("Invalid passno: %d", passno) - passno = 3 - # raise ValueError("Invalid passno: {}".format(passno)) - return [ - (False, False), - (False, True), - (True, False), - (True, True) - ][passno] + @contextlib.contextmanager + def push_node(self, node): + """ + Push `node` onto the `node_path` and yield a context manager. Pop `node` + off of `node_path` when the context manager `__exit__()s` + """ + self.node_path.append(node) + yield None + self.node_path.pop(-1) class LayoutNode(object): @@ -246,7 +214,6 @@ class LayoutNode(object): def __init__(self, pnode): self.pnode = pnode - self._wrap = WrapAlgo.HPACK self._position = Cursor(0, 0) # NOTE(josh): (row, col) self._size = Cursor(0, 0) # NOTE(josh): (rows, cols) @@ -278,48 +245,88 @@ def __init__(self, pnode): # `subtree_depth` have been computed and stored. self._locked = False + # Map subpass number to (passno, wrap) decisions + self._layout_passes = [(0, False)] + + # Set to true if this node's wrap is activated. Note that _wrap may + # refer to nesting or vertical layout, depending on what type of node + # we are. This is value only really needs to be communicated between + # `reflow()` and `_reflow()` but we store it so that we can render it + # when viewing the tree for debugging. + self._wrap = False + assert isinstance(pnode, parser.TreeNode) @property def name(self): - # pylint: disable=protected-access + """ + The class name of the derived node type. + """ return self.__class__.__name__ + @property + def passno(self): + """ + The active pass-number which contributed the current layout of the + subtree rooted at this node. + """ + return self._passno + @property def colextent(self): + """ + The column index of the right-most character in the layout of the + subtree rooted at this node. In other words, the width of the + bounding box for the subtree rooted at this node. + """ return self._colextent @property def reflow_valid(self): + """ + A boolean flag indicating whether or not the current layout is accepted. + If False, then further layout passes are required. + """ return self._reflow_valid @property def position(self): + """ + A cursor with the (row,col) of the first (i.e. top,left) character in the + subtree rooted at this node. + """ return Cursor(*self._position) - # TODO(josh): rename this @property - def type(self): + def node_type(self): + """ + Return the `parser.NodeType` of the corresponding parser node that generated + this layout node. + """ return self.pnode.node_type # NOTE(josh): making children a property disallows direct assignment @property def children(self): + """ + A list of children layout nodes + """ return self._children - @property - def wrap(self): - return self._wrap - def __repr__(self): - return "{},{}({}) p({},{}) ce:{}".format( - self.type.name, - self._wrap.name, - self._passno, + boolmap = {True: "T", False: "F"} + return "{},(passno={},wrap={}) pos:({},{}) colextent:{}".format( + self.node_type.name, + self._passno, boolmap[self._wrap], self.position[0], self.position[1], self.colextent) def has_terminal_comment(self): + """ + Return true if this node has a terminal line comment. In particular, this + implies that no other node may be packed at the output cursor of this + node's layout, and a line-wrap is required. + """ return False def get_depth(self): @@ -336,12 +343,18 @@ def get_depth(self): # Compute `stmt_depth` and `subtree_depth`. Replace the children list with # a tuple. def lock(self, config, stmt_depth=0): + """ + Lock the tree structure (topology) and prevent further updates. This is + mostly for sanity checking. It also computes topological statistics such + as `stmt_depth` and `subtree_depth`, and replaces the mutable list of + children with an immuatable tuple. + """ self._stmt_depth = stmt_depth self._subtree_depth = self.get_depth() self._children = tuple(self._children) self._locked = True - if self.type == NodeType.STATEMENT: + if self.node_type == NodeType.STATEMENT: nextdepth = 1 elif stmt_depth > 0: nextdepth = stmt_depth + 1 @@ -351,63 +364,88 @@ def lock(self, config, stmt_depth=0): for child in self._children: child.lock(config, nextdepth) - # TODO(josh): get rid of this - def _get_wrap(self, config, passno): - algoidx = clamp(passno, 0, len(config.algorithm_order)) - algoid = config.algorithm_order[algoidx] + def _reflow(self, stack_context, cursor, passno): + """ + Overridden by concrete classes to implement the layout of characters. + """ + raise NotImplementedError() + + def _validate_layout( + self, stack_context, start_extent, end_extent): + """ + Return true if the layout is acceptable according to several checks. For + example, returns false if the content overflows the columnt limit. + """ - # No vpack if dangling parens - if config.dangle_parens and algoid == WrapAlgo.VPACK.value: - algoidx += 1 - algoid = config.algorithm_order[algoidx] - return WrapAlgo.from_id(algoid) + config = stack_context.config - def _reflow(self, config, cursor, passno): - raise NotImplementedError() + # If the bounding box overflows the column limit then the layout is + # automatically voided + if end_extent[1] > config.linewidth: + return False - def _reflow_new(self, config, cursor, passno): - return self._reflow(config, cursor, passno) + size = end_extent - start_extent - def reflow(self, config, cursor, passno=0): + if not self._wrap: + # Regardless of nesting, if the content is wrapped horizontally but it + # exceeds the configured maximum number of lines we must reject it + # TODO(josh): figure out how to subtract out any terminal comment + # contributions to the size, as noted in the algorithm doc. + if size[0] > config.max_lines_hwrap: + return False + + # Or if this nodepath is marked to always be vertical layout + pathstr = "/".join(node.name for node in stack_context.node_path) + if pathstr in config.always_wrap: + return False + + return True + + def reflow(self, stack_context, cursor, parent_passno=0): """ (re-)compute the layout of this node under the assumption that it should - be placed at the given `cursor` on the current `passno`. The wrap algorithm - used is given by the node's statement depth and the current `passno`. + be placed at the given `cursor` on the current `parent_passno`. """ assert self._locked assert isinstance(self.pnode, parser.TreeNode) - self._position = Cursor(*cursor) + self._position = cursor.clone() outcursor = None - for repass in range(passno + 1): - self._passno = repass - self._wrap = self._get_wrap(config, repass) - self._reflow_valid = True - if USE_NEW_ALGORITHM: - outcursor = self._reflow_new(config, Cursor(*cursor), repass) - else: - outcursor = self._reflow(config, Cursor(*cursor), repass) - # TODO(josh): this is too conservative. We don't need every row to - # reserve a character for the final parenthesis, we just need the final - # row to reserve a character. - if self._wrap == WrapAlgo.HPACK: - linewidth = config.linewidth - 1 - else: - linewidth = config.linewidth - if self._reflow_valid and self._colextent <= linewidth: - break + + layout_passes = \ + stack_context.config.layout_passes.get( + self.__class__.__name__, self._layout_passes) + + with stack_context.push_node(self): + for passno, wrap in layout_passes: + if passno > parent_passno: + break + self._passno = passno + self._wrap = wrap + self._reflow_valid = True + start_extent = cursor.clone() + outcursor = self._reflow( + stack_context, cursor.clone(), passno) + end_extent = Cursor(outcursor[0], self._colextent) + self._reflow_valid &= self._validate_layout( + stack_context, start_extent, end_extent) + if self._reflow_valid: + break assert outcursor is not None return outcursor - def _write(self, config, ctx): + def write(self, config, ctx): + """ + Output text content given the currently configured layout. + """ for child in self._children: child.write(config, ctx) - def write(self, config, ctx): - self._write(config, ctx) - @staticmethod def create(pnode): + """ + Create a new layout node associated with then given parser node. + """ # pylint: disable=too-many-return-statements if pnode.node_type in SCALAR_TYPES: return ScalarNode(pnode) @@ -443,17 +481,17 @@ class ParenNode(LayoutNode): @property def name(self): - return self.type.name + return self.node_type.name - def _reflow(self, config, cursor, passno): + def _reflow(self, stack_context, cursor, passno): """There is only one possible layout for this node.""" self._colextent = cursor[1] + 1 return cursor + (0, 1) - def _write(self, config, ctx): - if self.type == NodeType.LPAREN: + def write(self, config, ctx): + if self.node_type == NodeType.LPAREN: ctx.outfile.write_at(self.position, '(') - elif self.type == NodeType.RPAREN: + elif self.node_type == NodeType.RPAREN: ctx.outfile.write_at(self.position, ')') else: raise ValueError("Unrecognized paren type") @@ -461,7 +499,7 @@ def _write(self, config, ctx): class ScalarNode(LayoutNode): """ - Holdes scalar tokens such as statement names, parentheses, or keyword or + Holds scalar tokens such as statement names, parentheses, or keyword or positional arguments. """ @@ -475,69 +513,11 @@ def has_terminal_comment(self): # Note: bracket comments do not count as terminal comments return self.children[-1].pnode.children[0].type == TokenType.COMMENT - def _reflow(self, config, cursor, passno): - """ - Reflow is pretty trivial for a scalar node. We don't have any choices to - make, there is only one possible rendering. - """ - - assert self.pnode.children - token = self.pnode.children[0] - assert isinstance(token, lexer.Token) - - # This might be a multiline string or a multiline bracket argument. In - # that case we need to normalize line endings and flow each line - lines = normalize_line_endings(token.spelling).split('\n') - line = lines.pop(0) - cursor[1] += len(line) - self._colextent = cursor[1] - - while lines: - cursor[0] += 1 - cursor[1] = 0 - line = lines.pop(0) - cursor[1] += len(line) - self._colextent = max(self._colextent, cursor[1]) - - # Scalar nodes might have terminal comments associated with them. They show - # up as children in the layout graph. This is the only possible child of - # a scalar node. - children = list(self.children) - if children: - child = children.pop(0) - - # We should not have more than one terminal comment associated with a - # given scalar node - assert not children - - # The only kind of children we store for a scalar node are argument - # comments. - assert child.type == NodeType.COMMENT - - # Reflow the comment after the scalar - cursor = child.reflow(config, cursor + (0, 1), passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - if child.pnode.children[0].type == TokenType.BRACKET_COMMENT: - # If the argument comment is a bracket comment then we are done - return cursor - - # If the argument commment is a line comment then we must invalidate - # HPACK since there is no way we can render a single line. - if self.wrap in (WrapAlgo.HPACK, WrapAlgo.HWRAP): - # NOTE(josh): if we have a non-bracket comment child associated with - # this argument, then we cannot hpack - self._reflow_valid = False - - return cursor - - def _reflow_new(self, config, cursor, passno): + def _reflow(self, stack_context, cursor, passno): """ Reflow is pretty trivial for a scalar node. We don't have any choices to make, there is only one possible rendering. """ - assert self.pnode.children token = self.pnode.children[0] assert isinstance(token, lexer.Token) @@ -559,31 +539,23 @@ def _reflow_new(self, config, cursor, passno): # Scalar nodes might have terminal comments associated with them. They show # up as children in the layout graph. This is the only possible child of # a scalar node. - children = list(self.children) - if children: - child = children.pop(0) - + if self.children: # We should not have more than one terminal comment associated with a # given scalar node - assert not children + assert len(self.children) == 1 # The only kind of children we store for a scalar node are argument # comments. - assert child.type == NodeType.COMMENT + child = self.children[0] + assert child.node_type == NodeType.COMMENT # Reflow the comment after the scalar - cursor = child.reflow(config, cursor + (0, 1), passno) + cursor = child.reflow(stack_context, cursor + (0, 1), passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) - - # TODO(josh): should scalar nodes have a notion of acceptance? - # self._reflow_valid = \ - # default_accept_layout( - # config, [], False, False, start_cursor, - # Cursor(cursor[0], self._colextent)) return cursor - def _write(self, config, ctx): + def write(self, config, ctx): if not ctx.is_active(): return @@ -592,7 +564,7 @@ def _write(self, config, ctx): assert isinstance(token, lexer.Token) spelling = normalize_line_endings(token.spelling) - if self.type == NodeType.FUNNAME: + if self.node_type == NodeType.FUNNAME: command_case = config.resolve_for_command(token.spelling, "command_case") if command_case in ("lower", "upper"): spelling = getattr(token.spelling, command_case)() @@ -602,7 +574,7 @@ def _write(self, config, ctx): else: assert command_case == "unchanged", ( "Unrecognized command case {}".format(command_case)) - elif (self.type in (NodeType.KEYWORD, NodeType.FLAG) + elif (self.node_type in (NodeType.KEYWORD, NodeType.FLAG) and config.keyword_case in ("lower", "upper")): spelling = getattr(token.spelling, config.keyword_case)() @@ -610,7 +582,7 @@ def _write(self, config, ctx): children = list(self.children) if children: child = children.pop(0) - assert child.type == NodeType.COMMENT + assert child.node_type == NodeType.COMMENT child.write(config, ctx) assert not children @@ -629,7 +601,7 @@ def name(self): def has_terminal_comment(self): return True - def _reflow(self, config, cursor, passno): + def _reflow(self, stack_context, cursor, passno): """ There is only one possible flow as this is a single-token """ @@ -648,7 +620,7 @@ def _reflow(self, config, cursor, passno): self._colextent = cursor[1] return cursor - def _write(self, config, ctx): + def write(self, config, ctx): assert self.pnode.children token = self.pnode.children[0] assert isinstance(token, lexer.Token) @@ -674,488 +646,341 @@ def _write(self, config, ctx): ctx.outfile.write_at(self.position, spelling) -# TODO(josh): add more than just the primary algorithm to the passes that we -# take. These four should be the first four passes, but we also want to -# add a pass where we move statement comment to the next line, or we dangle -# the paren. The statement comment pass may be something we want to do *foreach* -# of the other options, while the dangle paren is probably the last thing we -# want to do ever (i.e. if we're already wrapped as much as possible and the -# paren still wont fit). class StatementNode(LayoutNode): + """ + Top-level node for a statement. + """ + def __init__(self, pnode): + super(StatementNode, self).__init__(pnode) + self._layout_passes = [ + (0, False), + (1, True), + (2, True), + (3, True), + (4, True), + (5, True), + ] + + def reflow(self, stack_context, cursor, _=0): # pylint: disable=unused-argument + return super(StatementNode, self).reflow( + stack_context, cursor, + max(passno for passno, _ in self._layout_passes)) + + def get_prefix_width(self, config): + prefix_width = len(self.name) + 1 + if need_paren_space(self.name, config): + prefix_width += 1 # For the space before the paren + return prefix_width - # NOTE(josh): StatementNode is the only node that overrides reflow(), the - # rest override just _reflow() - # TODO(josh): at this point the difference in the override is pretty minimal - # so we should probably figure out how to remove it. - def reflow(self, config, cursor, _=0): # pylint: disable=unused-argument - assert self._locked - assert isinstance(self.pnode, parser.TreeNode) + @property + def name(self): + return self.children[0].pnode.children[0].spelling.lower() - self._position = Cursor(*cursor) - outcursor = None + def _validate_layout( + self, stack_context, start_extent, end_extent): + config = stack_context.config - for repass in range(0, WrapAlgo.COUNT.value): - self._passno = repass - self._wrap = self._get_wrap(config, repass) - self._reflow_valid = True - if USE_NEW_ALGORITHM: - outcursor = self._reflow_new(config, Cursor(*cursor), repass) - else: - outcursor = self._reflow(config, Cursor(*cursor), repass) - if self._reflow_valid and self.colextent <= config.linewidth: - break - assert outcursor is not None - return outcursor + # If the bounding box overflows the column limit then the layout is + # automatically voided + if end_extent[1] > config.linewidth: + return False - def _reflow_horizontal(self, config, cursor, passno, children): + size = end_extent - start_extent + if not self._wrap: + # If the statement spelling is very long and there is enough content that + # the content is forced to wrap, then we require the statement content to + # nest. + if (size[0] > 1 and + self.get_prefix_width(config) > config.max_prefix_chars): + return False + return True + + def _reflow(self, stack_context, cursor, passno): + # pylint: disable=too-many-statements + config = stack_context.config + start_cursor = cursor.clone() + self._colextent = cursor[1] + + # Layout of the statement name is always the same, we just write it out + # at the current cursor + children = list(self.children) assert children child = children.pop(0) - assert child.type == NodeType.LPAREN - cursor = child.reflow(config, cursor, passno) + assert child.node_type == NodeType.FUNNAME + + cursor = child.reflow(stack_context, cursor, passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) - while children: - prev = child - child = children.pop(0) - - if (child.type == NodeType.COMMENT and - child.pnode.children[0].type is TokenType.COMMENT - and prev.type != NodeType.RPAREN): - self._reflow_valid = False - - # TODO(josh): RESUME HERE -- the following is only valid for HPACK. - # For HWRAP we need to try the cursor + '0', and if that fails then - # try the column_corsor + '\n'. - - # The first node after an LPAREN and the RPAREN itself are not padded - if not(prev.type == NodeType.LPAREN or child.type == NodeType.RPAREN): - cursor[1] += len(' ') + funname = child + token = funname.pnode.children[0] - if (children and children[0].type == NodeType.RPAREN): - child.statement_terminal = True + assert isinstance(token, lexer.Token) + if need_paren_space(token.spelling.lower(), config): + cursor[1] += 1 - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - # NOTE(josh): we must keep updating the extent after each child because - # the child might be an argument with a multiline string or a bracket - # argument... in which case HPACK might actually wrap to a newline. - self._colextent = max(self._colextent, child.colextent) - return cursor + if self.get_prefix_width(config) <= config.min_prefix_chars: + # If the statement or keyword spelling is too short, then nesting doesn't + # make sense because (nest + tab-width) will take us right back to the + # same column as without nesting. + self._wrap = False - def _reflow_vertical(self, config, cursor, passno, children, start_cursor): - # pylint: disable=too-many-statements assert children child = children.pop(0) - assert child.type == NodeType.LPAREN - cursor = child.reflow(config, cursor, passno) + assert child.node_type == NodeType.LPAREN + cursor = child.reflow(stack_context, cursor, passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) - if self._wrap == WrapAlgo.VPACK: - column_cursor = cursor - elif self._wrap in (WrapAlgo.KWNVPACK, WrapAlgo.PNVPACK): - column_cursor = start_cursor + Cursor(1, config.tab_size) + if self._wrap: + column_cursor = start_cursor + (1, config.tab_size) + cursor = Cursor(*column_cursor) else: - raise RuntimeError("Unexepected wrap algorithm") - - # NOTE(josh): this logic is common to both VPACK and NVPACK, the only - # difference is the starting position of the column_cursor - prev = None - cursor = Cursor(*column_cursor) - scalar_seq = analyze_scalar_sequence(children) - - while children: - if children[0].type == NodeType.RPAREN: - break - - if (prev is None) or (prev.type not in SCALAR_TYPES): - scalar_seq = analyze_scalar_sequence(children) - child = children.pop(0) - - # TODO(josh): remove scalar_squence code - # If both the previous and current nodes are scalar nodes and the two - # are not part of a particularly long (by a configurable margin) sequence - # of scalar nodes, then advance the cursor horizontally by one space and - # try to pack the next child on the same line as the current one - if prev is None: - cursor = child.reflow(config, cursor, passno) - elif (prev.type in SCALAR_TYPES - and child.type in SCALAR_TYPES - and scalar_seq.length <= config.max_subargs_per_line - and not scalar_seq.has_comment): - cursor[1] += len(' ') - - # But if the cursor has overflowed the line width allocation, then - # we cannot. Note that if this is the last argument in a statement, - # then we will pack an RPAREN at it's end... so we should include the - # size of that RPAREN when determining if we overflow. - cursor = child.reflow(config, cursor, passno) - realized_extent = child.colextent - if children[0].type == NodeType.RPAREN: - realized_extent += 1 - if realized_extent > config.linewidth: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - else: - # Otherwise the node is not special and we vpack as usual - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) + column_cursor = cursor.clone() - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - column_cursor[0] = cursor[0] - prev = child + # NOTE(josh): STATEMENTs should have at most one ARGGROUP child between + # parentheses. + child = children.pop(0) + assert child.node_type is NodeType.ARGGROUP, ( + "Expected ARGGROUP node, but got {} at {}" + .format(child.node_type, child.pnode)) + # Whether or not an ARGGROUP is statement terminal is kind of a weird + # notion, but as a sentinel for the future "number of open parens" we'll + # go ahead and mark it. + child.statement_terminal = True + cursor = child.reflow(stack_context, cursor, passno) + self._reflow_valid &= child.reflow_valid + # We must keep updating the extent after each child because + # the child might be an argument with a multiline string or a bracket + # argument... in which case HPACK might actually wrap to a newline. + self._colextent = max(self._colextent, child.colextent) + column_cursor[0] = cursor[0] assert children + prev = child child = children.pop(0) - assert child.type == NodeType.RPAREN, \ - "Expected RPAREN but got {}".format(child.type) + assert child.node_type == NodeType.RPAREN, \ + "Expected RPAREN but got {}".format(child.node_type) # NOTE(josh): dangle parens if it wont fit on the current line or # if the user has requested us to always do so + dangle_parens = False if config.dangle_parens and cursor[0] > start_cursor[0]: - column_cursor[0] += 1 - cursor = Cursor(column_cursor[0], start_cursor[1]) - elif (cursor[1] >= config.linewidth - and self._wrap.value > WrapAlgo.VPACK.value): - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - elif prev.type == NodeType.COMMENT: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) + # If the configuration requests dangling parentheses, then honor that + # request so long as the statement doesn't fit on a single line + dangle_parens = True + elif cursor[1] >= config.linewidth: + # If the child reflow was unable to reserve a column for us to place our + # parenthesis, then we must dangle it + dangle_parens = True + # But we really want to nest first in this case + if not self._wrap: + self._reflow_valid = False elif prev.has_terminal_comment(): - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) + # If the final token in an argument list is a line comment, then we must + # dangle the parenthesis, or it will become part of the line comment + dangle_parens = True + + column_cursor[0] += 1 + if config.dangle_align == "prefix": + dangle_cursor = Cursor(column_cursor[0], start_cursor[1]) + elif config.dangle_align == "prefix-indent": + dangle_cursor = Cursor( + column_cursor[0], start_cursor[1] + config.tab_size) + elif config.dangle_align == "child": + dangle_cursor = Cursor(*column_cursor) + else: + raise ValueError( + "Unexpected config.dangle_align: {}".format(config.dangle_align)) - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) + if dangle_parens: + cursor = dangle_cursor.clone() + + rparen = child + initial_rparen_cursor = cursor.clone() + cursor = rparen.reflow(stack_context, initial_rparen_cursor, passno) + self._reflow_valid &= rparen.reflow_valid + # NOTE(josh): don't max rparen.colextent here, since we may decide to + # dangle it below # Trailing comment if children: cursor[1] += 1 child = children.pop(0) - assert child.type == NodeType.COMMENT, \ - "Expected COMMENT after RPAREN but got {}".format(child.type) + assert child.node_type == NodeType.COMMENT, \ + "Expected COMMENT after RPAREN but got {}".format(child.node_type) assert not children - savecursor = Cursor(*cursor) - cursor = child.reflow(config, cursor, passno) - + savecursor = cursor.clone() + cursor = child.reflow(stack_context, cursor, passno) + + # If the statement trailing comment does not fit in the column after the + # rparen, then dangle the rparen and try again. + if not dangle_parens and child.colextent > config.linewidth: + cursor = rparen.reflow(stack_context, dangle_cursor, passno) + self._reflow_valid &= rparen.reflow_valid + # NOTE(josh): don't max rparen.colextent here, since we may reverse our + # dangle decision in the next if block + savecursor = cursor.clone() + cursor = child.reflow(stack_context, cursor, passno) + + # If the statement trailing comment still does not fit in the current + # column then just move it to the next line. if child.colextent > config.linewidth: - cursor = child.reflow(config, (savecursor[0] + 1, start_cursor[1]), - passno) + # NOTE(josh): potentially undangle the paren: if the only reason to + # dangle it was due to the oversized comment line, then we need to + # undangle it since the oversized comment didn't fit anyway. + cursor = rparen.reflow(stack_context, initial_rparen_cursor, passno) + cursor = child.reflow( + stack_context, (savecursor[0] + 1, start_cursor[1]), + passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) + self._colextent = max(self._colextent, rparen.colextent) return cursor + def write(self, config, ctx): + if not ctx.is_active(): + return + super(StatementNode, self).write(config, ctx) + + +class KwargGroupNode(LayoutNode): + """ + A keyword argument group. Contains a keyword, followed by an argument group. + """ + def __init__(self, pnode): + super(KwargGroupNode, self).__init__(pnode) + self._layout_passes = [ + (0, False), + (1, False), + (2, False), + (3, False), + (4, False), + (5, True), + ] + + def has_terminal_comment(self): + if self.children[-1].node_type is NodeType.COMMENT: + return True + return self.children[-1].has_terminal_comment() + @property def name(self): - return self.children[0].pnode.children[0].spelling.lower() + return self.children[0].pnode.children[0].spelling.upper() - def _reflow(self, config, cursor, passno): - """ - Compute the size of a statement which is nominally allocated `linewidth` - columns for packing. `linewidth` is only considered for hpacking of - consecutive scalar arguments - """ - funname = self.children[0] - assert funname.type == NodeType.FUNNAME + def _validate_layout( + self, stack_context, start_extent, end_extent): + config = stack_context.config - token = funname.pnode.children[0] - assert isinstance(token, lexer.Token) + # If the bounding box overflows the column limit then the layout is + # automatically voided + if end_extent[1] > config.linewidth: + return False + + size = end_extent - start_extent + if not self._wrap: + # If the statement spelling is very long and there is enough content that + # the content is forced to wrap, then we require the statement content to + # nest. + if size[0] > 1 and len(self.name) > config.max_prefix_chars: + return False + return True - start_cursor = Cursor(*cursor) + def _reflow(self, stack_context, cursor, passno): + config = stack_context.config + start_cursor = cursor.clone() self._colextent = cursor[1] - # Layout of the statement name is always the same, we just write it out + # Keyword node should have exactly two children, the keyword and the + # argument group. + assert len(self.children) <= 2, ( + "Expected at most 2 children in KWargGroup, got {}" + .format(len(self.children)) + ) + + # Layout of the keyword is always the same, we just write it out # at the current cursor - children = list(self.children) - assert children - child = children.pop(0) - assert child.type == NodeType.FUNNAME - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - if need_paren_space(token.spelling, config): - cursor[1] += 1 - - if self._wrap in (WrapAlgo.HPACK, WrapAlgo.HWRAP): - if token.spelling.lower() in config.always_wrap: - self._reflow_valid = False - return self._reflow_horizontal(config, cursor, passno, children) - - return self._reflow_vertical(config, cursor, passno, children, start_cursor) - - def _reflow_new(self, config, cursor, passno): - """ - Compute the size of a statement which is nominally allocated `linewidth` - columns for packing. `linewidth` is only considered for hpacking of - consecutive scalar arguments - """ - # pylint: disable=too-many-statements - funname = self.children[0] - assert funname.type == NodeType.FUNNAME - token = funname.pnode.children[0] - assert isinstance(token, lexer.Token) + child = self.children[0] + assert child.node_type == NodeType.KEYWORD - start_cursor = Cursor(*cursor) - self._colextent = cursor[1] + if len(self.name) <= config.min_prefix_chars: + # If the statement or keyword spelling is too short, then nesting doesn't + # make sense because (nest + tab-width) will take us right back to the + # same column as without nesting. + self._wrap = False - # Layout of the statement name is always the same, we just write it out - # at the current cursor - children = list(self.children) - assert children - child = children.pop(0) - assert child.type == NodeType.FUNNAME - cursor = child.reflow(config, cursor, passno) + cursor = child.reflow(stack_context, cursor, passno) self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - if need_paren_space(token.spelling, config): - cursor[1] += 1 + self._colextent = child.colextent - assert children - child = children.pop(0) - assert child.type == NodeType.LPAREN - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) + # NOTE(josh): Empty kwarg. These are rare, and I think they might actually + # be syntax errors, but we currently support it so let's not limit the + # possibility for now + if len(self.children) == 1: + return cursor - nest, vertical = get_nest_wrap(passno) - if nest: - column_cursor = start_cursor + (1, config.tab_size) - cursor = Cursor(*column_cursor) + # keyword = parser.get_normalized_kwarg(child.pnode.children[0]) + if self._wrap: + column_cursor = Cursor(cursor[0] + 1, start_cursor[1] + config.tab_size) else: - column_cursor = Cursor(*cursor) - - # NOTE(josh): STATEMENTs should have at most one ARGGROUP child between - # parentheses. This logic is redundant, but it is left so that we can - # compare against other nodes and hopefully unify them. - while children and children[0].type != NodeType.RPAREN: - prev = child - child = children.pop(0) - - if prev.type == NodeType.LPAREN: - # This is the first child of the statement the cursor is already in the - # right position, regardless of current nesting or wrapping - pass - elif (prev.type == NodeType.COMMENT - or prev.has_terminal_comment() - or vertical): - cursor[1] = column_cursor[1] - cursor[0] += 1 - else: - cursor[1] += 1 - - if (children and children[0].type == NodeType.RPAREN): - child.statement_terminal = True + column_cursor = cursor + (0, 1) - cursor = child.reflow(config, cursor, passno) - if not vertical: - # If we are in horizontal wrapping mode, then we need to check if the - # child has overflowed the column. If so, then we need to move to the - # next line and try again - realized_extent = child.colextent - if children[0].type == NodeType.RPAREN: - # If this is the last node before the parenthesis then we need to - # account for one extra character (the closing parenthesis) - realized_extent += 1 - if realized_extent > config.linewidth: - # If the realized extent overflows the column limit then we need to - # insert a newline and try again - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - - self._reflow_valid &= child.reflow_valid - # We must keep updating the extent after each child because - # the child might be an argument with a multiline string or a bracket - # argument... in which case HPACK might actually wrap to a newline. - self._colextent = max(self._colextent, child.colextent) - column_cursor[0] = cursor[0] - - assert children - prev = child - child = children.pop(0) - assert child.type == NodeType.RPAREN, \ - "Expected RPAREN but got {}".format(child.type) + child = self.children[1] + cursor = Cursor(*column_cursor) - # NOTE(josh): dangle parens if it wont fit on the current line or - # if the user has requested us to always do so - if config.dangle_parens and cursor[0] > start_cursor[0]: - column_cursor[0] += 1 - cursor = Cursor(column_cursor[0], start_cursor[1]) - elif (cursor[1] >= config.linewidth - and self._wrap.value > WrapAlgo.VPACK.value): - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - elif prev.type == NodeType.COMMENT: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - elif prev.has_terminal_comment(): - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) + if self.statement_terminal: + child.statement_terminal = True - cursor = child.reflow(config, cursor, passno) + cursor = child.reflow(stack_context, cursor, passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) - - # Trailing comment - if children: - cursor[1] += 1 - child = children.pop(0) - assert child.type == NodeType.COMMENT, \ - "Expected COMMENT after RPAREN but got {}".format(child.type) - assert not children - savecursor = Cursor(*cursor) - cursor = child.reflow(config, cursor, passno) - - if child.colextent > config.linewidth: - # If the statement trailing comment does not fit in the current column - # just move it to the next line. - # TODO(josh): actually, if the RPAREN is not already dangling then - # we should dangle the paren and keep the comment trailing the statement - cursor = child.reflow(config, (savecursor[0] + 1, start_cursor[1]), - passno) - - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - self._reflow_valid = \ - default_accept_layout( - config, [token.spelling], nest, vertical, - start_cursor, Cursor(cursor[0], self._colextent)) + column_cursor[0] = cursor[0] return cursor - def _write(self, config, ctx): - if not ctx.is_active(): - return - super(StatementNode, self)._write(config, ctx) +def filename_node_key(layout_node): + """ + Return the sort key for sortable arguments nodes. This is the + case-insensitive spelling of the first token in the node. + """ + return layout_node.pnode.children[0].spelling.lower() -class KwargGroupNode(LayoutNode): - - def has_terminal_comment(self): - if self.children[-1].type is NodeType.COMMENT: - return True - return self.children[-1].has_terminal_comment() - - @property - def name(self): - self.children[0].pnode.chidren[0].spelling.upper() - - def _reflow(self, config, cursor, passno): - """ - Compute the size of a keyword argument group which is nominally allocated - `linewidth` columns for packing. `linewidth` is only considered for hpacking - of consecutive scalar arguments - """ - - start_cursor = Cursor(*cursor) - - children = list(self.children) - assert children - child = children.pop(0) - assert child.type == NodeType.KEYWORD - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - keyword = parser.get_normalized_kwarg(child.pnode.children[0]) - self._colextent = child.colextent - - # KWARG groups cannot be HWRAPPED. If they need to wrap, they must - # do so vertically. - if self._wrap == WrapAlgo.HWRAP: - self._reflow_valid = False - - if self._wrap in (WrapAlgo.HPACK, WrapAlgo.HWRAP): - if len(children) > config.max_subargs_per_line: - self._reflow_valid = False - - while children: - cursor[1] += len(' ') - child = children.pop(0) - - if (child.type == NodeType.COMMENT and - child.pnode.children[0].type is TokenType.COMMENT): - self._reflow_valid = False - - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - return cursor - if self._wrap == WrapAlgo.VPACK: - cursor[1] += len(' ') - column_cursor = Cursor(*cursor) - elif self._wrap in (WrapAlgo.KWNVPACK, WrapAlgo.PNVPACK): - column_cursor = start_cursor + Cursor(1, config.tab_size) +def count_arguments(children): + """ + Count the number of positional arguments (excluding line comments and + whitespace) within a parg group. + """ + count = 0 + for child in children: + if child.node_type is NodeType.COMMENT: + continue else: - raise RuntimeError("Unexepected wrap algorithm") - - # NOTE(josh): this logic is common to both VPACK and NVPACK, the only - # difference is the starting position of the column_cursor - prev = None - cursor = Cursor(*column_cursor) - scalar_seq = analyze_scalar_sequence(children) - - while children: - if (prev is None) or (prev.type not in SCALAR_TYPES): - scalar_seq = analyze_scalar_sequence(children) - child = children.pop(0) - - # If both the previous and current nodes are scalar nodes and the two - # are not part of a particularly long (by a configurable margin) sequence - # of scalar nodes, then advance the cursor horizontally by one space and - # try to pack the next child on the same line as the current one - if prev is None: - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - elif (prev.type in SCALAR_TYPES - and child.type in SCALAR_TYPES - and (scalar_seq.length <= config.max_subargs_per_line - or keyword == 'COMMAND' - or keyword.startswith('--')) - and not scalar_seq.has_comment): - cursor[1] += len(' ') - - # But if the cursor has overflowed the line width allocation, then - # we cannot - cursor = child.reflow(config, cursor, passno) - if child.colextent > config.linewidth: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - # Otherwise we fall back to vpack - else: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - column_cursor[0] = cursor[0] - prev = child - - return cursor - - -def filename_node_key(layout_node): - return layout_node.pnode.children[0].spelling + count += 1 + return count class PargGroupNode(LayoutNode): + """ + A group of positional arguments. + """ + + def __init__(self, pnode): + super(PargGroupNode, self).__init__(pnode) + self._layout_passes = [ + (0, False), + (1, False), + (2, False), + (3, False), + (4, True), + ] def has_terminal_comment(self): if not self.children: return False - if self.children[-1].type is NodeType.COMMENT: + if self.children[-1].node_type is NodeType.COMMENT: return True return self.children[-1].has_terminal_comment() @@ -1184,178 +1009,77 @@ def lock(self, config, stmt_depth=0): super(PargGroupNode, self).lock(config, stmt_depth) - def _reflow_hpack(self, config, cursor, passno): - if len(self.children) > config.max_subargs_per_line: - self._reflow_valid = False - - children = list(self.children) - prev = None - - while children: - if prev is not None: - cursor[1] += len(' ') - child = children.pop(0) - - if (child.type == NodeType.COMMENT and - child.pnode.children[0].type is TokenType.COMMENT): - self._reflow_valid = False - - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - prev = child - return cursor - - def _reflow_hwrap(self, config, cursor, passno): - # TODO(josh): max_subargs_per_line doesn't invalidate hwrap - if len(self.children) > config.max_subargs_per_line: - self._reflow_valid = False - - column_cursor = Cursor(*cursor) - children = list(self.children) - prev = None - while children: - if prev is not None: - cursor[1] += len(' ') - child = children.pop(0) - - if (child.type == NodeType.COMMENT and - child.pnode.children[0].type is TokenType.COMMENT): - self._reflow_valid = False - - cursor = child.reflow(config, cursor, passno) - linewidth = config.linewidth - - # Reserve an extra character if we are the positional group of a - # statement and we are in wrap mode - if self.statement_terminal and not children: - linewidth -= 1 - - if child.colextent > linewidth: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - prev = child - - return cursor - - def _reflow_vpack(self, config, cursor, passno): - # NOTE(josh): this logic is common to both VPACK and NVPACK, the only - # difference is the starting position of the column_cursor - children = list(self.children) - prev = None - column_cursor = Cursor(*cursor) - scalar_seq = analyze_scalar_sequence(children) - - while children: - if (prev is None) or (prev.type not in SCALAR_TYPES): - scalar_seq = analyze_scalar_sequence(children) - child = children.pop(0) - - # If both the previous and current nodes are scalar nodes and the two - # are not part of a particularly long (by a configurable margin) sequence - # of scalar nodes, then advance the cursor horizontally by one space and - # try to pack the next child on the same line as the current one - if prev is None: - cursor = child.reflow(config, cursor, passno) - elif (prev.type in SCALAR_TYPES - and child.type in SCALAR_TYPES - and (scalar_seq.length <= config.max_subargs_per_line) - and not scalar_seq.has_comment): - cursor[1] += len(' ') - - # But if the cursor has overflowed the line width allocation, then - # we cannot - cursor = child.reflow(config, cursor, passno) - if child.colextent > config.linewidth: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - # Otherwise we fall back to vpack - else: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - column_cursor[0] = cursor[0] - prev = child - - return cursor - - def _reflow(self, config, cursor, passno): - """ - Compute the size of a positional argument group - """ - self._colextent = cursor.y - - if self._wrap is WrapAlgo.HPACK: - return self._reflow_hpack(config, cursor, passno) - - if self._wrap is WrapAlgo.HWRAP: - return self._reflow_hwrap(config, cursor, passno) - - return self._reflow_vpack(config, cursor, passno) - - def _reflow_new(self, config, cursor, passno): - """ - Compute the size of the group of all children - """ + def _reflow(self, stack_context, cursor, passno): + config = stack_context.config children = list(self.children) self._colextent = cursor.y prev = None child = None - # PARG groups do not nest, they nest by their parents - _, vertical = get_nest_wrap(passno) - column_cursor = Cursor(*cursor) + column_cursor = cursor.clone() + numpargs = count_arguments(children) + + # "COMMAND" arguments are never columnized, since there is no way for us + # to make that look good + if stack_context.node_path[-3].name == "COMMAND": + self._wrap = False - numgroups = 0 while children: prev = child child = children.pop(0) + cursor_is_at_column = True if prev is None: # This is the first child of the arg group so the cursor is already # at the right location and theres nothing for us to update pass - elif (prev.type == NodeType.COMMENT + elif (is_line_comment(prev) or prev.has_terminal_comment() - or vertical): - cursor[1] = column_cursor[1] - cursor[0] += 1 + or self._wrap): + column_cursor[0] = cursor[0] + 1 + cursor = Cursor(*column_cursor) else: + cursor_is_at_column = False cursor[1] += 1 if self.statement_terminal and not children: child.statement_terminal = True - cursor = child.reflow(config, cursor, passno) - if not vertical: - # If we are in horizontal wrapping mode, then we need to check if the - # child has overflowed the available column width. If so, then we need - # to wrap to the next line and try again + cursor = child.reflow(stack_context, cursor, passno) + if (not cursor_is_at_column) and (not self._wrap): + # If we are in horizontal wrapping mode, then we need to look at a + # couple of things thaty may cause us to wrap. needs_wrap = False + + # The obvious case is overflow: + # If the realized extent overflows the column limit then we need to + # insert a newline and try again + if child.colextent > config.linewidth: + needs_wrap = True + + # If this is the last node before the closing parenthesis of the + # statement then we need to extra character (the closing parenthesis) + # on the last line. If we overlow that extra character slot, then + # wrap to a new line if self.statement_terminal: - # If this is the last node before the parenthesis then we need to - # account for one extra character (the closing parenthesis) if cursor[1] + 1 > config.linewidth: needs_wrap = True - if child.colextent > config.linewidth: - # If the realized extent overflows the column limit then we need to - # insert a newline and try again + # If the current node is a comment, then we must have parsed it as a + # line comment (not an argument comment) and so we should preserve + # it's status as a line comment and not put it after another argument. + # Note that tag comments are always line comments, and never consumed + # as argument comments. + if ((prev and prev.node_type in SCALAR_TYPES) and + child.node_type is NodeType.COMMENT and + not child.is_tag()): needs_wrap = True if needs_wrap: - column_cursor[0] += 1 + column_cursor[0] = cursor[0] + 1 cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) + cursor = child.reflow(stack_context, cursor, passno) # NOTE(josh): we must keep updating the extent after each child because # the child might be an argument with a multiline string or a bracket @@ -1363,16 +1087,54 @@ def _reflow_new(self, config, cursor, passno): self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) - if isinstance(child, (PargGroupNode, KwargGroupNode)): - numgroups += 1 + # NOTE(josh): there is a subtle distinction between invalidating a reflow + # and forcing mode=vertical. The difference is whether or not a parent + # node has to advance it's decision state. If we force to vertical at + # the start of this function, the parent Statement wont nest this + # ArgGroup. Therefore, we must invalidate here, rather than forcing + # _vertical above. + if numpargs > config.max_pargs_hwrap: + self._reflow_valid &= self._wrap - # TODO(josh): Should this number 2 be a configuration parameter? - if numgroups > 2: - self._reflow_valid = False return cursor +def count_subgroups(children): + """ + Count the number of positional or kwarg sub groups in an argument group. + Ignore comments, and assert that no other types of children are found. + """ + numgroups = 0 + for child in children: + if child.node_type in (NodeType.KWARGGROUP, NodeType.PARGGROUP, + NodeType.PARENGROUP): + numgroups += 1 + elif child.node_type is NodeType.COMMENT: + continue + else: + raise ValueError( + "Unexpected node type {} as child of ArgGroupNode" + .format(child.node_type)) + return numgroups + + class ArgGroupNode(LayoutNode): + """ + A group of arguments. This is the single child node of either a + `StatementNode` or `KwargGroupNode` which then contains any further + group nodes. + """ + + def __init__(self, pnode): + super(ArgGroupNode, self).__init__(pnode) + self._layout_passes = [ + (0, False), + (1, False), + (2, False), + (3, False), + (4, True), + (5, True), + ] def has_terminal_comment(self): """ @@ -1380,71 +1142,20 @@ def has_terminal_comment(self): KWARGGROUP subtrees. Any terminal comment will belong to one of it's children. """ - return self.children and (self.children[-1].type is NodeType.COMMENT or + return self.children and (self.children[-1].node_type is NodeType.COMMENT or self.children[-1].has_terminal_comment()) - def _reflow(self, config, cursor, passno): - """ - Compute the size of the group of all children - """ - # TODO(josh): breakup this function - # pylint: disable=too-many-statements - - children = list(self.children) - self._colextent = cursor.y - - prev = None - if self._wrap in (WrapAlgo.HPACK, WrapAlgo.HWRAP): - while children: - child = children.pop(0) - - if (child.type == NodeType.COMMENT and - child.pnode.children[0].type is TokenType.COMMENT): - self._reflow_valid = False - - if prev is not None: - cursor[1] += len(' ') - - if self.statement_terminal and not children: - child.statement_terminal = True - - cursor = child.reflow(config, cursor, passno) - prev = child - - # NOTE(josh): we must keep updating the extent after each child because - # the child might be an argument with a multiline string or a bracket - # argument... in which case HPACK might actually wrap to a newline. - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - return cursor - - column_cursor = cursor - while children: - child = children.pop(0) - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - column_cursor[0] = cursor[0] + 1 - prev = child - return cursor - - def _reflow_new(self, config, cursor, passno): - """ - Compute the size of the group of all children - """ + def _reflow(self, stack_context, cursor, passno): + config = stack_context.config children = list(self.children) self._colextent = cursor.y prev = None child = None - # Argument groups cannot nest since they have no prefix, they are nested by - # their parent - _, vertical = get_nest_wrap(passno) - column_cursor = Cursor(*cursor) + column_cursor = cursor.clone() + numgroups = count_subgroups(children) - numgroups = 0 while children: prev = child child = children.pop(0) @@ -1452,24 +1163,26 @@ def _reflow_new(self, config, cursor, passno): if prev is None: # This is the first child of the arg group so the cursor is already # at the right location and theres nothing for us to update - pass - elif (prev.type == NodeType.COMMENT + is_first_in_row = True + elif (is_line_comment(prev) or prev.has_terminal_comment() - or vertical): - cursor[1] = column_cursor[1] - cursor[0] += 1 + or self._wrap): + column_cursor[0] += 1 + cursor = column_cursor.clone() + is_first_in_row = True else: cursor[1] += 1 + is_first_in_row = False if self.statement_terminal and not children: child.statement_terminal = True - cursor = child.reflow(config, cursor, passno) - if not vertical: + start_cursor = cursor + cursor = child.reflow(stack_context, cursor, passno) + if not is_first_in_row and not self._wrap: # If we are in horizontal wrapping mode, then we need to check if the - # child has overflowed the available columnt width. If so, then we need + # child has overflowed the available column width. If so, then we need # to wrap to the next line and try again - needs_wrap = False if child.statement_terminal: # If this is the last node before the parenthesis then we need to @@ -1482,39 +1195,62 @@ def _reflow_new(self, config, cursor, passno): # insert a newline and try again needs_wrap = True + if (cursor - start_cursor)[0] > 1: + # If the argument is wrapped internally (i.e. has more than two lines) + # then move it to it's own line + # TODO(josh): Instead of needs_wrap = True unconditionally in this + # case, let's layout both options and pick which ever one is best + needs_wrap = True + if needs_wrap: column_cursor[0] += 1 cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) + cursor = child.reflow(stack_context, cursor, passno) # NOTE(josh): we must keep updating the extent after each child because # the child might be an argument with a multiline string or a bracket # argument... in which case HPACK might actually wrap to a newline. self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) + column_cursor[0] = cursor[0] - if isinstance(child, (PargGroupNode, KwargGroupNode)): - numgroups += 1 + # NOTE(josh): there is a subtle distinction between invalidating a reflow + # and forcing mode=vertical. The difference is whether or not a parent + # node has to advance it's decision state. If we force to vertical at + # the start of this function, the parent Statement wont nest this + # ArgGroup. Therefore, we must invalidate here, rather than forcing + # _vertical above. + if numgroups > config.max_subgroups_hwrap: + self._reflow_valid &= self._wrap - # TODO(josh): Should this number 2 be a configuration parameter? - if numgroups > 2: - self._reflow_valid = False return cursor - def _write(self, config, ctx): + def write(self, config, ctx): if not ctx.is_active(): return - super(ArgGroupNode, self)._write(config, ctx) + super(ArgGroupNode, self).write(config, ctx) class ParenGroupNode(LayoutNode): - # TODO(josh): does ParenGroupNode also need to override reflow() to make - # sure that the trailing paren has a column slot in HPACK mode? i.e. the - # same as StatementNode. + """ + A parenthetical group. According to cmake syntax rules, this necessarily + implies a boolean logical expression. + """ + + def __init__(self, pnode): + super(ParenGroupNode, self).__init__(pnode) + self._layout_passes = [ + (0, False), + (1, False), + (2, False), + (3, False), + (4, False), + (5, True), + ] def has_terminal_comment(self): children = list(self.children) - while children and children[0].type != NodeType.RPAREN: + while children and children[0].node_type != NodeType.RPAREN: children.pop(0) if children: @@ -1523,178 +1259,24 @@ def has_terminal_comment(self): return (children and children[-1].pnode.children[0].type == TokenType.COMMENT) - def _reflow_hwrap(self, config, cursor, passno): - """ - Logic is the same for HPACK and HWRAP - """ - self._colextent = cursor[1] - children = list(self.children) - if not children: - return cursor - - assert children - child = children.pop(0) - assert child.type == NodeType.LPAREN - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - while children: - prev = child - child = children.pop(0) - - if (child.type == NodeType.COMMENT and - child.pnode.children[0].type is TokenType.COMMENT): - self._reflow_valid = False - - # The first node after an LPAREN and the RPAREN itself are not padded - if not(prev.type == NodeType.LPAREN or child.type == NodeType.RPAREN): - cursor[1] += len(' ') - - cursor = child.reflow(config, cursor, passno) - # NOTE(josh): we must keep updating the extent after each child because - # the child might be an argument with a multiline string or a bracket - # argument... in which case HPACK might actually wrap to a newline. - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - return cursor - - def _reflow_vertical(self, config, cursor, passno): - # pylint: disable=too-many-statements - start_cursor = Cursor(*cursor) - self._colextent = cursor[1] - - children = list(self.children) - if not children: - return cursor - - assert children - child = children.pop(0) - assert child.type == NodeType.LPAREN - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - if self._wrap in (WrapAlgo.VPACK, WrapAlgo.KWNVPACK): - column_cursor = cursor - elif self._wrap == WrapAlgo.PNVPACK: - column_cursor = start_cursor + Cursor(1, config.tab_size) - else: - raise RuntimeError("Unexpected wrap algorithm") - - # NOTE(josh): this logic is common to both VPACK and NVPACK, the only - # difference is the starting position of the column_cursor - prev = None - cursor = Cursor(*column_cursor) - scalar_seq = analyze_scalar_sequence(children) - - while children: - if children[0].type == NodeType.RPAREN: - break - - if (prev is None) or (prev.type not in SCALAR_TYPES): - scalar_seq = analyze_scalar_sequence(children) - child = children.pop(0) - - # If both the previous and current nodes are scalar nodes and the two - # are not part of a particularly long (by a configurable margin) sequence - # of scalar nodes, then advance the cursor horizontally by one space and - # try to pack the next child on the same line as the current one - if prev is None: - cursor = child.reflow(config, cursor, passno) - elif (prev.type in SCALAR_TYPES - and child.type in SCALAR_TYPES - and scalar_seq.length <= config.max_subargs_per_line - and not scalar_seq.has_comment): - cursor[1] += len(' ') - - # But if the cursor has overflowed the line width allocation, then - # we cannot - cursor = child.reflow(config, cursor, passno) - if child.colextent > config.linewidth: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - # Otherwise we fall back to vpack - else: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) - - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - column_cursor[0] = cursor[0] - prev = child - - assert children - child = children.pop(0) - assert child.type == NodeType.RPAREN, \ - "Expected RPAREN but got {}".format(child.type) - - # NOTE(josh); dangle parens if it wont fit on the current line or - # if the user has requested us to always do so - if config.dangle_parens and cursor[0] > start_cursor[0]: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - elif (cursor[1] >= config.linewidth - and self._wrap.value > WrapAlgo.VPACK.value): - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - elif prev.type == NodeType.COMMENT: - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - elif prev.has_terminal_comment(): - column_cursor[0] += 1 - cursor = Cursor(*column_cursor) - - cursor = child.reflow(config, cursor, passno) - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - - # Trailing comment - if children: - cursor[1] += 1 - child = children.pop(0) - assert child.type == NodeType.COMMENT, \ - "Expected COMMENT after RPAREN but got {}".format(child.type) - assert not children - cursor = child.reflow(config, cursor, passno) - - if child.colextent > config.linewidth: - cursor[0] += 1 - cursor[1] = start_cursor[1] - cursor = child.reflow(config, cursor, passno) - - self._reflow_valid &= child.reflow_valid - self._colextent = max(self._colextent, child.colextent) - return cursor - - def _reflow(self, config, cursor, passno): - """ - Compute the size of a parenthtetical argument group which is nominally - allocated `linewidth` columns for packing. `linewidth` is only considered - for hpacking of consecutive scalar arguments - """ - if self._wrap in (WrapAlgo.HPACK, WrapAlgo.HWRAP): - return self._reflow_hwrap(config, cursor, passno) - - return self._reflow_vertical(config, cursor, passno) - - def _reflow_new(self, config, cursor, passno): - """ - Compute the size of the group of all children - """ + def _reflow(self, stack_context, cursor, passno): + config = stack_context.config children = list(self.children) self._colextent = cursor.y + # ParenGroupNode is composed of: + # * a left parenthensis + # * an argument group + # * a right parenthesis + # * an optional trailing comment + assert len(children) in (3, 4) + prev = None child = None - # Argument groups cannot nest since they have no prefix, they are nested by - # their parent - _, vertical = get_nest_wrap(passno) - column_cursor = Cursor(*cursor) + # Paren groups do not themselves nest or wrap, they only have one real + # child: the primary argument group. + column_cursor = cursor.clone() - numgroups = 0 while children: prev = child child = children.pop(0) @@ -1703,19 +1285,25 @@ def _reflow_new(self, config, cursor, passno): # This is the first child of the arg group so the cursor is already # at the right location and theres nothing for us to update pass - elif (prev.type == NodeType.COMMENT + elif prev.node_type == NodeType.LPAREN: + # No space after LParen + pass + elif (is_line_comment(prev) or prev.has_terminal_comment() - or vertical): + or self._wrap): cursor[1] = column_cursor[1] cursor[0] += 1 + elif child.node_type == NodeType.RPAREN: + # No space before RPAREN + pass else: cursor[1] += 1 if self.statement_terminal and not children: child.statement_terminal = True - cursor = child.reflow(config, cursor, passno) - if not vertical: + cursor = child.reflow(stack_context, cursor, passno) + if not self._wrap: # If we are in horizontal wrapping mode, then we need to check if the # child has overflowed the available columnt width. If so, then we need # to wrap to the next line and try again @@ -1732,41 +1320,39 @@ def _reflow_new(self, config, cursor, passno): # insert a newline and try again needs_wrap = True + if not child.reflow_valid: + needs_wrap = True + if needs_wrap: column_cursor[0] += 1 cursor = Cursor(*column_cursor) - cursor = child.reflow(config, cursor, passno) + cursor = child.reflow(stack_context, cursor, passno) - # NOTE(josh): we must keep updating the extent after each child because - # the child might be an argument with a multiline string or a bracket - # argument... in which case HPACK might actually wrap to a newline. self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) - - if isinstance(child, (PargGroupNode, KwargGroupNode)): - numgroups += 1 - - # TODO(josh): Should this number 2 be a configuration parameter? - if numgroups > 2: - self._reflow_valid = False + column_cursor[0] = cursor[0] return cursor - def _write(self, config, ctx): + def write(self, config, ctx): if not ctx.is_active(): return - super(ParenGroupNode, self)._write(config, ctx) + super(ParenGroupNode, self).write(config, ctx) class BodyNode(LayoutNode): + """ + Top-level node for a given "scope" depth. This node is the root of a document, + or the root of any nested statement scopes. + """ - def _reflow(self, config, cursor, passno): + def _reflow(self, stack_context, cursor, passno): """ Compute the size of a body block """ - column_cursor = cursor + column_cursor = cursor.clone() self._colextent = 0 for child in self.children: - cursor = child.reflow(config, column_cursor, passno) + cursor = child.reflow(stack_context, column_cursor, passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) column_cursor[0] = cursor[0] + 1 @@ -1775,27 +1361,33 @@ def _reflow(self, config, cursor, passno): class FlowControlNode(LayoutNode): + """ + Top-Level node composed of a flow-control statement and it's associated + `BodyNodes`. + """ - def _reflow(self, config, cursor, passno): + def _reflow(self, stack_context, cursor, passno): """ Compute the size of a flowcontrol block """ + config = stack_context.config self._colextent = 0 - column_cursor = Cursor(*cursor) + column_cursor = cursor.clone() children = list(self.children) assert children child = children.pop(0) - assert child.type == NodeType.STATEMENT - cursor = child.reflow(config, column_cursor, passno) + assert child.node_type == NodeType.STATEMENT + cursor = child.reflow(stack_context, column_cursor, passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) column_cursor[0] = cursor[0] + 1 assert children child = children.pop(0) - assert child.type == NodeType.BODY - cursor = child.reflow(config, column_cursor + (0, config.tab_size), passno) + assert child.node_type == NodeType.BODY + cursor = child.reflow( + stack_context, column_cursor + (0, config.tab_size), passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) column_cursor[0] = cursor[0] + 1 @@ -1803,8 +1395,8 @@ def _reflow(self, config, cursor, passno): while True: assert children child = children.pop(0) - assert child.type == NodeType.STATEMENT - cursor = child.reflow(config, column_cursor, passno) + assert child.node_type == NodeType.STATEMENT + cursor = child.reflow(stack_context, column_cursor, passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) column_cursor[0] = cursor[0] + 1 @@ -1812,8 +1404,8 @@ def _reflow(self, config, cursor, passno): if not children: break child = children.pop(0) - assert child.type == NodeType.BODY - cursor = child.reflow(config, column_cursor + + assert child.node_type == NodeType.BODY + cursor = child.reflow(stack_context, column_cursor + (0, config.tab_size), passno) self._reflow_valid &= child.reflow_valid self._colextent = max(self._colextent, child.colextent) @@ -1823,11 +1415,20 @@ def _reflow(self, config, cursor, passno): class CommentNode(LayoutNode): + """ + A line comment or bracket comment. If parented by a group node then this + comment acts as an argument. If parented by a scalar node, then this comment + acts like an argument comment. + """ - def _reflow(self, config, cursor, passno): + def is_tag(self): + return parser.comment_is_tag(self.pnode.children[0]) + + def _reflow(self, stack_context, cursor, passno): """ Compute the size of a comment block """ + config = stack_context.config if (len(self.pnode.children) == 1 and isinstance(self.pnode.children[0], lexer.Token) and self.pnode.children[0].type == TokenType.BRACKET_COMMENT): @@ -1855,7 +1456,7 @@ def _reflow(self, config, cursor, passno): increment = (len(lines) - 1, len(lines[-1])) return cursor + increment - def _write(self, config, ctx): + def write(self, config, ctx): if not ctx.is_active(): return @@ -1872,15 +1473,18 @@ def _write(self, config, ctx): class WhitespaceNode(LayoutNode): + """ + A series of newlines + """ - def _reflow(self, config, cursor, passno): + def _reflow(self, stack_context, cursor, passno): """ Compute the size of a whitespace block """ self._colextent = 0 return cursor - def _write(self, config, ctx): + def write(self, config, ctx): return @@ -1888,38 +1492,12 @@ def get_scalar_sequence_len(box_children): length = 0 for child in box_children: # Child is not a scalar type - if child.type not in SCALAR_TYPES: + if child.node_type not in SCALAR_TYPES: return length length += 1 return length -def get_scalar_sequence_has_comment(box_children): - # TODO(josh): rename this function. It really just indicates whether or - # not a scalar is allowed to be hpacked. - - for child in box_children: - # Child is not a scalar type - if child.type not in SCALAR_TYPES: - return False - - if len(child.pnode.children) > 1: - return True - - return False - - -class ScalarSeq(object): - def __init__(self, length, has_comment): - self.length = length - self.has_comment = has_comment - - -def analyze_scalar_sequence(box_children): - return ScalarSeq(get_scalar_sequence_len(box_children), - get_scalar_sequence_has_comment(box_children)) - - def create_box_tree(pnode): """ Recursively construct a layout tree from the given parse tree @@ -1928,21 +1506,11 @@ def create_box_tree(pnode): layout_root = LayoutNode.create(pnode) child_queue = list(pnode.children) - found_primary_arggroup = False while child_queue: pchild = child_queue.pop(0) if not isinstance(pchild, parser.TreeNode): continue - # NOTE(josh); the primary argument group gets formatted specially because - # the parens belong to the statement, not the group. - if (pnode.node_type == NodeType.STATEMENT - and pchild.node_type == NodeType.ARGGROUP - and not found_primary_arggroup): - child_queue = list(pchild.children) + child_queue - found_primary_arggroup = True - continue - if (pchild.node_type == NodeType.WHITESPACE and pchild.count_newlines() < 2): continue @@ -1953,12 +1521,19 @@ def create_box_tree(pnode): def layout_tree(parsetree_root, config, linewidth=None): + """ + Top-level function to construct a layout tree from a parse tree, and then + iterate through layout passes until the entire tree is satisfactory. Returns + the root of the layout tree. + """ + if linewidth is None: linewidth = config.line_width root_box = create_box_tree(parsetree_root) root_box.lock(config) - root_box.reflow(config, (0, 0)) + stack_context = StackContext(config) + root_box.reflow(stack_context, Cursor(0, 0)) return root_box @@ -2069,7 +1644,7 @@ def dump_tree_for_test(nodes, outfile=None, indent=None, increment=None): for node in nodes: outfile.write(indent) outfile.write("({}, {}, {}, {}, {}, [".format( - node.type, node.wrap, + node.node_type, node.passno, node.position[0], node.position[1], node.colextent)) if hasattr(node, 'children') and node.children: @@ -2161,7 +1736,7 @@ def getvalue(self): return self._fobj.getvalue() + self._config.endl -class Global(object): +class WriteContext(object): """ Global state for the writing functions """ @@ -2186,7 +1761,7 @@ def write_tree(root_box, config, infile_content): """ Format the tree for size only, then print all of the boxes to outfile """ - ctx = Global(config, infile_content) + ctx = WriteContext(config, infile_content) root_box.write(config, ctx) if not ctx.is_active(): diff --git a/cmake_format/invocation_tests.py b/cmake_format/invocation_tests.py index 2dc638a..0a6ec09 100644 --- a/cmake_format/invocation_tests.py +++ b/cmake_format/invocation_tests.py @@ -32,7 +32,7 @@ def setUp(self): @contextlib.contextmanager def subTest(self, msg=None, **params): # pylint: disable=no-member - if sys.version_info < (3, 0, 0): + if sys.version_info < (3, 4, 0): yield None else: yield super(TestInvocations, self).subTest(msg=msg, **params) diff --git a/cmake_format/layout_tests.py b/cmake_format/layout_tests.py index 57770fd..8fed5ba 100644 --- a/cmake_format/layout_tests.py +++ b/cmake_format/layout_tests.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import contextlib import logging import unittest +import sys from cmake_format import __main__ from cmake_format import configuration @@ -11,7 +13,6 @@ from cmake_format import parse_funs from cmake_format import formatter from cmake_format.parser import NodeType -from cmake_format.formatter import WrapAlgo def strip_indent(content, indent=6): @@ -65,7 +66,7 @@ def assert_tree(test, nodes, tups, tree=None, history=None): subhistory = history + [node] message = (" for node {} at\n {} \n\n\n" "If the actual result is expected, then update the test with" - " this:\n{}" + " this:\n# pylint: disable=bad-continuation\n# noqa: E122\n{}" .format(node, formatter.tree_string(tree, subhistory), formatter.test_string(tree))) @@ -73,12 +74,12 @@ def assert_tree(test, nodes, tups, tree=None, history=None): test.assertIsNotNone(tup, msg="Extra node" + message) if len(tup) == 6: ntype, wrap, row, col, colextent, expect_children = tup - test.assertEqual(node.wrap, wrap, + test.assertEqual(node.passno, wrap, msg="Expected wrap={}".format(wrap) + message) else: ntype, row, col, colextent, expect_children = tup - test.assertEqual(node.type, ntype, + test.assertEqual(node.node_type, ntype, msg="Expected type={}".format(ntype) + message) test.assertEqual(node.position[0], row, msg="Expected row={}".format(row) + message) @@ -115,6 +116,14 @@ def setUp(self): def tearDown(self): pass + @contextlib.contextmanager + def subTest(self, msg=None, **params): + # pylint: disable=no-member + if sys.version_info < (3, 4, 0): + yield None + else: + yield super(TestCanonicalLayout, self).subTest(msg=msg, **params) + def do_layout_test(self, input_str, expect_tree, strip_len=6): """ Run the formatter on the input string and assert that the result matches @@ -131,19 +140,25 @@ def test_simple_statement(self): self.do_layout_test("""\ cmake_minimum_required(VERSION 2.8.11) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 38, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 38, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 22, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 22, 23, []), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 0, 23, 37, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 0, 23, 30, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 31, 37, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 31, 37, []), - ]), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 37, 38, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 38, [ + (NodeType.STATEMENT, 0, 0, 0, 38, [ + (NodeType.FUNNAME, 0, 0, 0, 22, []), + (NodeType.LPAREN, 0, 0, 22, 23, []), + (NodeType.ARGGROUP, 0, 0, 23, 37, [ + (NodeType.KWARGGROUP, 0, 0, 23, 37, [ + (NodeType.KEYWORD, 0, 0, 23, 30, []), + (NodeType.ARGGROUP, 0, 0, 31, 37, [ + (NodeType.PARGGROUP, 0, 0, 31, 37, [ + (NodeType.ARGUMENT, 0, 0, 31, 37, []), ]), + ]), + ]), + ]), + (NodeType.RPAREN, 0, 0, 37, 38, []), + ]), +]), ]) def test_collapse_additional_newlines(self): @@ -155,21 +170,27 @@ def test_collapse_additional_newlines(self): cmake_minimum_required(VERSION 2.8.11) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 75, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 0, 75, []), - (NodeType.WHITESPACE, WrapAlgo.HPACK, 1, 0, 0, []), - (NodeType.STATEMENT, WrapAlgo.HPACK, 2, 0, 38, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 2, 0, 22, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 2, 22, 23, []), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 2, 23, 37, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 2, 23, 30, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 2, 31, 37, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 31, 37, []), - ]), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 37, 38, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 75, [ + (NodeType.COMMENT, 0, 0, 0, 75, []), + (NodeType.WHITESPACE, 0, 1, 0, 0, []), + (NodeType.STATEMENT, 0, 2, 0, 38, [ + (NodeType.FUNNAME, 0, 2, 0, 22, []), + (NodeType.LPAREN, 0, 2, 22, 23, []), + (NodeType.ARGGROUP, 0, 2, 23, 37, [ + (NodeType.KWARGGROUP, 0, 2, 23, 37, [ + (NodeType.KEYWORD, 0, 2, 23, 30, []), + (NodeType.ARGGROUP, 0, 2, 31, 37, [ + (NodeType.PARGGROUP, 0, 2, 31, 37, [ + (NodeType.ARGUMENT, 0, 2, 31, 37, []), ]), + ]), + ]), + ]), + (NodeType.RPAREN, 0, 2, 37, 38, []), + ]), +]), ]) def test_multiline_reflow(self): @@ -188,22 +209,27 @@ def test_long_args_command_split(self): # This very long command should be split to multiple lines set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 63, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 0, 58, []), - (NodeType.STATEMENT, WrapAlgo.HWRAP, 1, 0, 63, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 1, 0, 3, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 1, 3, 4, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 1, 4, 11, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 4, 11, []), - ]), - (NodeType.PARGGROUP, WrapAlgo.HWRAP, 1, 12, 63, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 12, 37, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 38, 63, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 12, 37, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 37, 38, []), - ]), - ]), +# pylint: disable=bad-continuation +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 63, [ + (NodeType.COMMENT, 0, 0, 0, 58, []), + (NodeType.STATEMENT, 0, 1, 0, 63, [ + (NodeType.FUNNAME, 0, 1, 0, 3, []), + (NodeType.LPAREN, 0, 1, 3, 4, []), + (NodeType.ARGGROUP, 0, 1, 4, 63, [ + (NodeType.PARGGROUP, 0, 1, 4, 11, [ + (NodeType.ARGUMENT, 0, 1, 4, 11, []), + ]), + (NodeType.PARGGROUP, 0, 1, 12, 63, [ + (NodeType.ARGUMENT, 0, 1, 12, 37, []), + (NodeType.ARGUMENT, 0, 1, 38, 63, []), + (NodeType.ARGUMENT, 0, 2, 12, 37, []), + ]), + ]), + (NodeType.RPAREN, 0, 2, 37, 38, []), + ]), +]), ]) def test_long_arg_on_newline(self): @@ -212,17 +238,21 @@ def test_long_arg_on_newline(self): # end, so it should be moved to a new line with block indent + 1. some_long_command_name("Some very long argument that really needs to be on the next line.") """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 77, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 0, 77, []), - (NodeType.STATEMENT, WrapAlgo.KWNVPACK, 2, 0, 70, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 2, 0, 22, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 2, 22, 23, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 3, 2, 69, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 3, 2, 69, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 3, 69, 70, []), - ]), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 77, [ + (NodeType.COMMENT, 0, 0, 0, 77, []), + (NodeType.STATEMENT, 1, 2, 0, 70, [ + (NodeType.FUNNAME, 0, 2, 0, 22, []), + (NodeType.LPAREN, 0, 2, 22, 23, []), + (NodeType.ARGGROUP, 0, 3, 2, 69, [ + (NodeType.PARGGROUP, 0, 3, 2, 69, [ + (NodeType.ARGUMENT, 0, 3, 2, 69, []), + ]), + ]), + (NodeType.RPAREN, 0, 3, 69, 70, []), + ]), +]), ]) def test_argcomment_preserved_and_reflowed(self): @@ -232,24 +262,28 @@ def test_argcomment_preserved_and_reflowed(self): # across two lines. header_c.h header_d.h) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 78, [ - (NodeType.STATEMENT, WrapAlgo.VPACK, 0, 0, 78, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 3, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 3, 4, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 4, 11, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 4, 11, []), - ]), - (NodeType.PARGGROUP, WrapAlgo.VPACK, 1, 4, 78, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 4, 14, []), - (NodeType.ARGUMENT, WrapAlgo.VPACK, 2, 4, 78, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 2, 15, 78, []), - ]), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 4, 4, 14, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 5, 4, 14, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 5, 14, 15, []), - ]), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 80, [ + (NodeType.STATEMENT, 4, 0, 0, 80, [ + (NodeType.FUNNAME, 0, 0, 0, 3, []), + (NodeType.LPAREN, 0, 0, 3, 4, []), + (NodeType.ARGGROUP, 4, 0, 4, 80, [ + (NodeType.PARGGROUP, 0, 0, 4, 11, [ + (NodeType.ARGUMENT, 0, 0, 4, 11, []), + ]), + (NodeType.PARGGROUP, 0, 1, 4, 80, [ + (NodeType.ARGUMENT, 0, 1, 4, 14, []), + (NodeType.ARGUMENT, 0, 1, 15, 80, [ + (NodeType.COMMENT, 0, 1, 26, 80, []), + ]), + (NodeType.ARGUMENT, 0, 3, 4, 14, []), + (NodeType.ARGUMENT, 0, 3, 15, 25, []), + ]), + ]), + (NodeType.RPAREN, 0, 3, 25, 26, []), + ]), +]), ]) def test_complex_nested_stuff(self): @@ -270,75 +304,92 @@ def test_complex_nested_stuff(self): """, [ # pylint: disable=bad-continuation # noqa: E122 -(NodeType.BODY, WrapAlgo.HPACK, 0, 0, 79, [ - (NodeType.FLOW_CONTROL, WrapAlgo.HPACK, 0, 0, 79, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 7, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 2, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 2, 3, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 3, 6, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 6, 7, []), - ]), - (NodeType.BODY, WrapAlgo.HPACK, 1, 2, 79, [ - (NodeType.FLOW_CONTROL, WrapAlgo.HPACK, 1, 2, 79, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 1, 2, 10, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 1, 2, 4, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 1, 4, 5, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 5, 9, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 1, 9, 10, []), +(NodeType.BODY, 0, 0, 0, 80, [ + (NodeType.FLOW_CONTROL, 0, 0, 0, 80, [ + (NodeType.STATEMENT, 0, 0, 0, 7, [ + (NodeType.FUNNAME, 0, 0, 0, 2, []), + (NodeType.LPAREN, 0, 0, 2, 3, []), + (NodeType.ARGGROUP, 0, 0, 3, 6, [ + (NodeType.PARGGROUP, 0, 0, 3, 6, [ + (NodeType.ARGUMENT, 0, 0, 3, 6, []), ]), - (NodeType.BODY, WrapAlgo.HPACK, 2, 4, 79, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 2, 4, 31, []), - (NodeType.STATEMENT, WrapAlgo.VPACK, 3, 4, 76, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 3, 4, 15, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 3, 15, 16, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 3, 16, 27, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 3, 16, 27, []), + ]), + (NodeType.RPAREN, 0, 0, 6, 7, []), + ]), + (NodeType.BODY, 0, 1, 2, 80, [ + (NodeType.FLOW_CONTROL, 0, 1, 2, 80, [ + (NodeType.STATEMENT, 0, 1, 2, 10, [ + (NodeType.FUNNAME, 0, 1, 2, 4, []), + (NodeType.LPAREN, 0, 1, 4, 5, []), + (NodeType.ARGGROUP, 0, 1, 5, 9, [ + (NodeType.PARGGROUP, 0, 1, 5, 9, [ + (NodeType.ARGUMENT, 0, 1, 5, 9, []), ]), - (NodeType.PARGGROUP, WrapAlgo.VPACK, 4, 16, 76, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 4, 16, 22, []), - (NodeType.ARGUMENT, WrapAlgo.VPACK, 5, 16, 76, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 5, 23, 76, []), + ]), + (NodeType.RPAREN, 0, 1, 9, 10, []), + ]), + (NodeType.BODY, 0, 2, 4, 80, [ + (NodeType.COMMENT, 0, 2, 4, 31, []), + (NodeType.STATEMENT, 4, 3, 4, 74, [ + (NodeType.FUNNAME, 0, 3, 4, 15, []), + (NodeType.LPAREN, 0, 3, 15, 16, []), + (NodeType.ARGGROUP, 4, 4, 6, 74, [ + (NodeType.PARGGROUP, 0, 4, 6, 17, [ + (NodeType.ARGUMENT, 0, 4, 6, 17, []), + ]), + (NodeType.PARGGROUP, 0, 5, 6, 74, [ + (NodeType.ARGUMENT, 0, 5, 6, 12, []), + (NodeType.ARGUMENT, 0, 5, 13, 48, [ + (NodeType.COMMENT, 0, 5, 20, 48, []), + ]), + (NodeType.COMMENT, 0, 6, 6, 74, []), + (NodeType.ARGUMENT, 0, 7, 6, 12, []), ]), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 7, 16, 22, []), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 7, 22, 23, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 7, 24, 61, []), + (NodeType.RPAREN, 0, 7, 12, 13, []), + (NodeType.COMMENT, 0, 7, 14, 51, []), ]), - (NodeType.WHITESPACE, WrapAlgo.HPACK, 8, 4, 0, []), - (NodeType.STATEMENT, WrapAlgo.HPACK, 9, 4, 79, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 9, 4, 17, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 9, 17, 18, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 9, 18, 55, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 9, 18, 36, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 9, 37, 55, []), + (NodeType.WHITESPACE, 0, 8, 4, 0, []), + (NodeType.STATEMENT, 1, 9, 4, 76, [ + (NodeType.FUNNAME, 0, 9, 4, 17, []), + (NodeType.LPAREN, 0, 9, 17, 18, []), + (NodeType.ARGGROUP, 0, 10, 6, 43, [ + (NodeType.PARGGROUP, 0, 10, 6, 43, [ + (NodeType.ARGUMENT, 0, 10, 6, 24, []), + (NodeType.ARGUMENT, 0, 10, 25, 43, []), + ]), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 9, 55, 56, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 9, 57, 79, []), + (NodeType.RPAREN, 0, 10, 43, 44, []), + (NodeType.COMMENT, 0, 10, 45, 76, []), ]), - (NodeType.WHITESPACE, WrapAlgo.HPACK, 12, 4, 0, []), - (NodeType.STATEMENT, WrapAlgo.VPACK, 13, 4, 79, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 13, 4, 17, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 13, 17, 18, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 13, 18, 74, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 13, 18, 36, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 13, 37, 55, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 13, 56, 74, []), + (NodeType.WHITESPACE, 0, 12, 4, 0, []), + (NodeType.STATEMENT, 5, 13, 4, 80, [ + (NodeType.FUNNAME, 0, 13, 4, 17, []), + (NodeType.LPAREN, 0, 13, 17, 18, []), + (NodeType.ARGGROUP, 0, 14, 6, 62, [ + (NodeType.PARGGROUP, 0, 14, 6, 62, [ + (NodeType.ARGUMENT, 0, 14, 6, 24, []), + (NodeType.ARGUMENT, 0, 14, 25, 43, []), + (NodeType.ARGUMENT, 0, 14, 44, 62, []), + ]), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 13, 74, 75, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 14, 4, 79, []), + (NodeType.RPAREN, 0, 14, 62, 63, []), + (NodeType.COMMENT, 0, 14, 64, 80, []), ]), ]), - (NodeType.STATEMENT, WrapAlgo.HPACK, 16, 2, 9, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 16, 2, 7, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 16, 7, 8, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 16, 8, 9, []), + (NodeType.STATEMENT, 0, 23, 2, 9, [ + (NodeType.FUNNAME, 0, 23, 2, 7, []), + (NodeType.LPAREN, 0, 23, 7, 8, []), + (NodeType.ARGGROUP, 0, 23, 8, 8, []), + (NodeType.RPAREN, 0, 23, 8, 9, []), ]), ]), ]), - (NodeType.STATEMENT, WrapAlgo.HPACK, 17, 0, 7, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 17, 0, 5, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 17, 5, 6, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 17, 6, 7, []), + (NodeType.STATEMENT, 0, 24, 0, 7, [ + (NodeType.FUNNAME, 0, 24, 0, 5, []), + (NodeType.LPAREN, 0, 24, 5, 6, []), + (NodeType.ARGGROUP, 0, 24, 6, 6, []), + (NodeType.RPAREN, 0, 24, 6, 7, []), ]), ]), ]), @@ -349,47 +400,57 @@ def test_custom_command(self): # This very long command should be broken up along keyword arguments foo(nonkwarg_a nonkwarg_b HEADERS a.h b.h c.h d.h e.h f.h SOURCES a.cc b.cc d.cc DEPENDS foo bar baz) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 68, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 0, 68, []), - (NodeType.STATEMENT, WrapAlgo.VPACK, 1, 0, 26, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 1, 0, 3, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 1, 3, 4, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 1, 4, 25, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 4, 14, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 15, 25, []), - ]), - (NodeType.KWARGGROUP, WrapAlgo.VPACK, 2, 4, 15, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 2, 4, 11, []), - (NodeType.PARGGROUP, WrapAlgo.VPACK, 2, 12, 15, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 12, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 3, 12, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 4, 12, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 5, 12, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 6, 12, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 7, 12, 15, []), - ]), - ]), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 8, 4, 26, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 8, 4, 11, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 8, 12, 26, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 8, 12, 16, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 8, 17, 21, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 8, 22, 26, []), - ]), - ]), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 9, 4, 15, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 9, 4, 11, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 9, 12, 15, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 9, 12, 15, []), - ]), - ]), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 10, 4, 11, [ - (NodeType.FLAG, WrapAlgo.HPACK, 10, 4, 7, []), - (NodeType.FLAG, WrapAlgo.HPACK, 10, 8, 11, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 10, 11, 12, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 68, [ + (NodeType.COMMENT, 0, 0, 0, 68, []), + (NodeType.STATEMENT, 4, 1, 0, 35, [ + (NodeType.FUNNAME, 0, 1, 0, 3, []), + (NodeType.LPAREN, 0, 1, 3, 4, []), + (NodeType.ARGGROUP, 4, 1, 4, 35, [ + (NodeType.PARGGROUP, 0, 1, 4, 25, [ + (NodeType.ARGUMENT, 0, 1, 4, 14, []), + (NodeType.ARGUMENT, 0, 1, 15, 25, []), + ]), + (NodeType.KWARGGROUP, 0, 2, 4, 35, [ + (NodeType.KEYWORD, 0, 2, 4, 11, []), + (NodeType.ARGGROUP, 0, 2, 12, 35, [ + (NodeType.PARGGROUP, 0, 2, 12, 35, [ + (NodeType.ARGUMENT, 0, 2, 12, 15, []), + (NodeType.ARGUMENT, 0, 2, 16, 19, []), + (NodeType.ARGUMENT, 0, 2, 20, 23, []), + (NodeType.ARGUMENT, 0, 2, 24, 27, []), + (NodeType.ARGUMENT, 0, 2, 28, 31, []), + (NodeType.ARGUMENT, 0, 2, 32, 35, []), + ]), + ]), + ]), + (NodeType.KWARGGROUP, 0, 3, 4, 26, [ + (NodeType.KEYWORD, 0, 3, 4, 11, []), + (NodeType.ARGGROUP, 0, 3, 12, 26, [ + (NodeType.PARGGROUP, 0, 3, 12, 26, [ + (NodeType.ARGUMENT, 0, 3, 12, 16, []), + (NodeType.ARGUMENT, 0, 3, 17, 21, []), + (NodeType.ARGUMENT, 0, 3, 22, 26, []), + ]), + ]), + ]), + (NodeType.KWARGGROUP, 0, 4, 4, 15, [ + (NodeType.KEYWORD, 0, 4, 4, 11, []), + (NodeType.ARGGROUP, 0, 4, 12, 15, [ + (NodeType.PARGGROUP, 0, 4, 12, 15, [ + (NodeType.ARGUMENT, 0, 4, 12, 15, []), ]), + ]), + ]), + (NodeType.PARGGROUP, 0, 5, 4, 11, [ + (NodeType.FLAG, 0, 5, 4, 7, []), + (NodeType.FLAG, 0, 5, 8, 11, []), + ]), + ]), + (NodeType.RPAREN, 0, 5, 11, 12, []), + ]), +]), ]) def test_multiline_string(self): @@ -398,18 +459,22 @@ def test_multiline_string(self): This string is on multiple lines ") """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 36, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 36, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 3, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 3, 4, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 4, 36, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 4, 12, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 13, 21, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 22, 36, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 1, 2, []), - ]), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 36, [ + (NodeType.STATEMENT, 0, 0, 0, 36, [ + (NodeType.FUNNAME, 0, 0, 0, 3, []), + (NodeType.LPAREN, 0, 0, 3, 4, []), + (NodeType.ARGGROUP, 0, 0, 4, 36, [ + (NodeType.PARGGROUP, 0, 0, 4, 36, [ + (NodeType.ARGUMENT, 0, 0, 4, 12, []), + (NodeType.ARGUMENT, 0, 0, 13, 21, []), + (NodeType.ARGUMENT, 0, 0, 22, 36, []), + ]), + ]), + (NodeType.RPAREN, 0, 2, 1, 2, []), + ]), +]), ]) def test_nested_parens(self): @@ -421,157 +486,196 @@ def test_nested_parens(self): """, [ # pylint: disable=bad-continuation # noqa: E122 -(NodeType.BODY, WrapAlgo.HPACK, 0, 0, 40, [ - (NodeType.FLOW_CONTROL, WrapAlgo.HPACK, 0, 0, 40, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 40, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 2, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 2, 3, []), - (NodeType.PARENGROUP, WrapAlgo.HPACK, 0, 3, 14, [ - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 3, 4, []), - (NodeType.ARGGROUP, WrapAlgo.HPACK, 0, 4, 13, [ - (NodeType.FLAG, WrapAlgo.HPACK, 0, 4, 7, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 8, 13, []), +(NodeType.BODY, 0, 0, 0, 40, [ + (NodeType.FLOW_CONTROL, 0, 0, 0, 40, [ + (NodeType.STATEMENT, 0, 0, 0, 40, [ + (NodeType.FUNNAME, 0, 0, 0, 2, []), + (NodeType.LPAREN, 0, 0, 2, 3, []), + (NodeType.ARGGROUP, 0, 0, 3, 39, [ + (NodeType.PARENGROUP, 0, 0, 3, 14, [ + (NodeType.LPAREN, 0, 0, 3, 4, []), + (NodeType.ARGGROUP, 0, 0, 4, 13, [ + (NodeType.PARGGROUP, 0, 0, 4, 13, [ + (NodeType.FLAG, 0, 0, 4, 7, []), + (NodeType.ARGUMENT, 0, 0, 8, 13, []), + ]), + ]), + (NodeType.RPAREN, 0, 0, 13, 14, []), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 13, 14, []), - ]), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 0, 15, 39, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 0, 15, 17, []), - (NodeType.ARGGROUP, WrapAlgo.HPACK, 0, 18, 39, [ - (NodeType.PARENGROUP, WrapAlgo.HPACK, 0, 18, 39, [ - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 18, 19, []), - (NodeType.ARGGROUP, WrapAlgo.HPACK, 0, 19, 38, [ - (NodeType.FLAG, WrapAlgo.HPACK, 0, 19, 22, []), - (NodeType.FLAG, WrapAlgo.HPACK, 0, 23, 29, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 30, 38, []), + (NodeType.KWARGGROUP, 0, 0, 15, 39, [ + (NodeType.KEYWORD, 0, 0, 15, 17, []), + (NodeType.ARGGROUP, 0, 0, 18, 39, [ + (NodeType.PARENGROUP, 0, 0, 18, 39, [ + (NodeType.LPAREN, 0, 0, 18, 19, []), + (NodeType.ARGGROUP, 0, 0, 19, 38, [ + (NodeType.PARGGROUP, 0, 0, 19, 38, [ + (NodeType.FLAG, 0, 0, 19, 22, []), + (NodeType.FLAG, 0, 0, 23, 29, []), + (NodeType.ARGUMENT, 0, 0, 30, 38, []), + ]), + ]), + (NodeType.RPAREN, 0, 0, 38, 39, []), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 38, 39, []), ]), ]), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 39, 40, []), + (NodeType.RPAREN, 0, 0, 39, 40, []), ]), - (NodeType.BODY, WrapAlgo.HPACK, 1, 2, 39, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 1, 2, 39, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 1, 2, 9, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 1, 9, 10, []), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 1, 10, 38, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 1, 10, 17, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 1, 18, 38, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 18, 38, []), + (NodeType.BODY, 0, 1, 2, 39, [ + (NodeType.STATEMENT, 0, 1, 2, 39, [ + (NodeType.FUNNAME, 0, 1, 2, 9, []), + (NodeType.LPAREN, 0, 1, 9, 10, []), + (NodeType.ARGGROUP, 0, 1, 10, 38, [ + (NodeType.KWARGGROUP, 0, 1, 10, 38, [ + (NodeType.KEYWORD, 0, 1, 10, 17, []), + (NodeType.ARGGROUP, 0, 1, 18, 38, [ + (NodeType.PARGGROUP, 0, 1, 18, 38, [ + (NodeType.ARGUMENT, 0, 1, 18, 38, []), + ]), + ]), ]), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 1, 38, 39, []), + (NodeType.RPAREN, 0, 1, 38, 39, []), ]), - (NodeType.STATEMENT, WrapAlgo.HPACK, 2, 2, 19, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 2, 2, 5, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 2, 5, 6, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 2, 6, 12, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 6, 12, []), - ]), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 2, 13, 18, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 13, 18, []), + (NodeType.STATEMENT, 0, 2, 2, 19, [ + (NodeType.FUNNAME, 0, 2, 2, 5, []), + (NodeType.LPAREN, 0, 2, 5, 6, []), + (NodeType.ARGGROUP, 0, 2, 6, 18, [ + (NodeType.PARGGROUP, 0, 2, 6, 12, [ + (NodeType.ARGUMENT, 0, 2, 6, 12, []), + ]), + (NodeType.PARGGROUP, 0, 2, 13, 18, [ + (NodeType.ARGUMENT, 0, 2, 13, 18, []), + ]), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 18, 19, []), + (NodeType.RPAREN, 0, 2, 18, 19, []), ]), ]), - (NodeType.STATEMENT, WrapAlgo.HPACK, 3, 0, 7, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 3, 0, 5, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 3, 5, 6, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 3, 6, 7, []), + (NodeType.STATEMENT, 0, 3, 0, 7, [ + (NodeType.FUNNAME, 0, 3, 0, 5, []), + (NodeType.LPAREN, 0, 3, 5, 6, []), + (NodeType.ARGGROUP, 0, 3, 6, 6, []), + (NodeType.RPAREN, 0, 3, 6, 7, []), ]), ]), ]), ]) def test_comment_after_command(self): - self.do_layout_test("""\ - foo_command() # comment - """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 23, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 23, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 11, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 11, 12, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 12, 12, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 12, 13, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 14, 23, []), - ]), - ]), - ]) + with self.subTest(sub=1): + self.do_layout_test("""\ + foo_command() # comment + """, [ +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 23, [ + (NodeType.STATEMENT, 0, 0, 0, 23, [ + (NodeType.FUNNAME, 0, 0, 0, 11, []), + (NodeType.LPAREN, 0, 0, 11, 12, []), + (NodeType.ARGGROUP, 0, 0, 12, 12, []), + (NodeType.RPAREN, 0, 0, 12, 13, []), + (NodeType.COMMENT, 0, 0, 14, 23, []), + ]), +]), + ]) - self.do_layout_test("""\ - foo_command() # this is a long comment that exceeds the desired page width and will be wrapped to a newline - """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 78, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 78, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 11, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 11, 12, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 12, 12, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 12, 13, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 14, 78, []), - ]), - ]), - ]) + with self.subTest(sub=2): + self.do_layout_test("""\ + foo_command() # this is a long comment that exceeds the desired page width and will be wrapped to a newline + """, [ +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 78, [ + (NodeType.STATEMENT, 0, 0, 0, 78, [ + (NodeType.FUNNAME, 0, 0, 0, 11, []), + (NodeType.LPAREN, 0, 0, 11, 12, []), + (NodeType.ARGGROUP, 0, 0, 12, 12, []), + (NodeType.RPAREN, 0, 0, 12, 13, []), + (NodeType.COMMENT, 0, 0, 14, 78, []), + ]), +]), + + ]) def test_arg_just_fits(self): """ Ensure that if an argument *just* fits that it isn't superfluously wrapped """ - self.do_layout_test("""\ + with self.subTest(chars=81): + self.do_layout_test("""\ message(FATAL_ERROR "81 character line ----------------------------------------") """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 75, [ - (NodeType.STATEMENT, WrapAlgo.KWNVPACK, 0, 0, 75, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 7, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 7, 8, []), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 1, 2, 74, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 1, 2, 13, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 1, 14, 74, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 14, 74, []), - ]), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 1, 74, 75, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 75, [ + (NodeType.STATEMENT, 1, 0, 0, 75, [ + (NodeType.FUNNAME, 0, 0, 0, 7, []), + (NodeType.LPAREN, 0, 0, 7, 8, []), + (NodeType.ARGGROUP, 0, 1, 2, 74, [ + (NodeType.KWARGGROUP, 0, 1, 2, 74, [ + (NodeType.KEYWORD, 0, 1, 2, 13, []), + (NodeType.ARGGROUP, 0, 1, 14, 74, [ + (NodeType.PARGGROUP, 0, 1, 14, 74, [ + (NodeType.ARGUMENT, 0, 1, 14, 74, []), + ]), ]), + ]), + ]), + (NodeType.RPAREN, 0, 1, 74, 75, []), + ]), +]), ]) - self.do_layout_test("""\ + with self.subTest(chars=100, with_prefix=True): + self.do_layout_test("""\ message(FATAL_ERROR "100 character line ----------------------------------------------------------" ) # Closing parenthesis is indented one space! """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 83, [ - (NodeType.STATEMENT, WrapAlgo.PNVPACK, 0, 0, 83, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 7, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 7, 8, []), - (NodeType.KWARGGROUP, WrapAlgo.PNVPACK, 1, 2, 83, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 1, 2, 13, []), - (NodeType.PARGGROUP, WrapAlgo.PNVPACK, 2, 4, 83, [ - (NodeType.ARGUMENT, WrapAlgo.PNVPACK, 2, 4, 83, []), - ]), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 3, 2, 3, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 3, 4, 48, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 83, [ + (NodeType.STATEMENT, 5, 0, 0, 83, [ + (NodeType.FUNNAME, 0, 0, 0, 7, []), + (NodeType.LPAREN, 0, 0, 7, 8, []), + (NodeType.ARGGROUP, 5, 1, 2, 83, [ + (NodeType.KWARGGROUP, 5, 1, 2, 83, [ + (NodeType.KEYWORD, 0, 1, 2, 13, []), + (NodeType.ARGGROUP, 5, 2, 4, 83, [ + (NodeType.PARGGROUP, 4, 2, 4, 83, [ + (NodeType.ARGUMENT, 0, 2, 4, 83, []), + ]), ]), + ]), + ]), + (NodeType.RPAREN, 0, 3, 0, 1, []), + (NodeType.COMMENT, 0, 3, 2, 46, []), + ]), +]), ]) - self.do_layout_test("""\ + with self.subTest(chars=100): + self.do_layout_test("""\ message( "100 character line ----------------------------------------------------------------------" ) # Closing parenthesis is indented one space! """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 93, [ - (NodeType.STATEMENT, WrapAlgo.PNVPACK, 0, 0, 93, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 7, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 7, 8, []), - (NodeType.PARGGROUP, WrapAlgo.PNVPACK, 1, 2, 93, [ - (NodeType.ARGUMENT, WrapAlgo.PNVPACK, 1, 2, 93, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 2, 3, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 2, 4, 48, []), - ]), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 93, [ + (NodeType.STATEMENT, 5, 0, 0, 93, [ + (NodeType.FUNNAME, 0, 0, 0, 7, []), + (NodeType.LPAREN, 0, 0, 7, 8, []), + (NodeType.ARGGROUP, 5, 1, 2, 93, [ + (NodeType.PARGGROUP, 4, 1, 2, 93, [ + (NodeType.ARGUMENT, 0, 1, 2, 93, []), + ]), + ]), + (NodeType.RPAREN, 0, 2, 0, 1, []), + (NodeType.COMMENT, 0, 2, 2, 46, []), + ]), +]), ]) def test_string_preserved_during_split(self): @@ -579,26 +683,32 @@ def test_string_preserved_during_split(self): # The string in this command should not be split set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 74, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 0, 48, []), - (NodeType.STATEMENT, WrapAlgo.VPACK, 1, 0, 74, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 1, 0, 21, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 1, 21, 22, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 1, 22, 33, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 22, 25, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 26, 29, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 30, 33, []), - ]), - (NodeType.KWARGGROUP, WrapAlgo.HPACK, 2, 22, 73, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 2, 22, 32, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 2, 33, 73, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 33, 46, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 2, 47, 73, []), - ]), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 73, 74, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 72, [ + (NodeType.COMMENT, 0, 0, 0, 48, []), + (NodeType.STATEMENT, 0, 1, 0, 72, [ + (NodeType.FUNNAME, 0, 1, 0, 21, []), + (NodeType.LPAREN, 0, 1, 21, 22, []), + (NodeType.ARGGROUP, 0, 1, 22, 71, [ + (NodeType.PARGGROUP, 0, 1, 22, 33, [ + (NodeType.ARGUMENT, 0, 1, 22, 25, []), + (NodeType.ARGUMENT, 0, 1, 26, 29, []), + (NodeType.ARGUMENT, 0, 1, 30, 33, []), + ]), + (NodeType.KWARGGROUP, 0, 1, 34, 71, [ + (NodeType.KEYWORD, 0, 1, 34, 44, []), + (NodeType.ARGGROUP, 0, 1, 45, 71, [ + (NodeType.PARGGROUP, 0, 1, 45, 71, [ + (NodeType.ARGUMENT, 0, 1, 45, 58, []), + (NodeType.ARGUMENT, 0, 2, 45, 71, []), ]), + ]), + ]), + ]), + (NodeType.RPAREN, 0, 2, 71, 72, []), + ]), +]), ]) def test_while(self): @@ -609,32 +719,39 @@ def test_while(self): """, [ # pylint: disable=bad-continuation # noqa: E122 -(NodeType.BODY, WrapAlgo.HPACK, 0, 0, 32, [ - (NodeType.FLOW_CONTROL, WrapAlgo.HPACK, 0, 0, 32, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 32, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 5, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 5, 6, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 6, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 16, 20, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 21, 26, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 27, 31, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 31, 32, []), +(NodeType.BODY, 0, 0, 0, 32, [ + (NodeType.FLOW_CONTROL, 0, 0, 0, 32, [ + (NodeType.STATEMENT, 0, 0, 0, 32, [ + (NodeType.FUNNAME, 0, 0, 0, 5, []), + (NodeType.LPAREN, 0, 0, 5, 6, []), + (NodeType.ARGGROUP, 0, 0, 6, 31, [ + (NodeType.PARGGROUP, 0, 0, 6, 31, [ + (NodeType.ARGUMENT, 0, 0, 6, 15, []), + (NodeType.ARGUMENT, 0, 0, 16, 20, []), + (NodeType.ARGUMENT, 0, 0, 21, 26, []), + (NodeType.ARGUMENT, 0, 0, 27, 31, []), + ]), + ]), + (NodeType.RPAREN, 0, 0, 31, 32, []), ]), - (NodeType.BODY, WrapAlgo.HPACK, 1, 2, 29, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 1, 2, 29, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 1, 2, 9, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 1, 9, 10, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 1, 10, 28, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 10, 15, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 1, 16, 28, []), + (NodeType.BODY, 0, 1, 2, 29, [ + (NodeType.STATEMENT, 0, 1, 2, 29, [ + (NodeType.FUNNAME, 0, 1, 2, 9, []), + (NodeType.LPAREN, 0, 1, 9, 10, []), + (NodeType.ARGGROUP, 0, 1, 10, 28, [ + (NodeType.PARGGROUP, 0, 1, 10, 28, [ + (NodeType.ARGUMENT, 0, 1, 10, 15, []), + (NodeType.ARGUMENT, 0, 1, 16, 28, []), + ]), ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 1, 28, 29, []), + (NodeType.RPAREN, 0, 1, 28, 29, []), ]), ]), - (NodeType.STATEMENT, WrapAlgo.HPACK, 2, 0, 10, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 2, 0, 8, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 2, 8, 9, []), - (NodeType.RPAREN, WrapAlgo.HPACK, 2, 9, 10, []), + (NodeType.STATEMENT, 0, 2, 0, 10, [ + (NodeType.FUNNAME, 0, 2, 0, 8, []), + (NodeType.LPAREN, 0, 2, 8, 9, []), + (NodeType.ARGGROUP, 0, 2, 9, 9, []), + (NodeType.RPAREN, 0, 2, 9, 10, []), ]), ]), ]), @@ -648,26 +765,32 @@ def test_keyword_comment(self): # -------------------------------------- this_is_a_really_long_word_foo) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 64, [ - (NodeType.STATEMENT, WrapAlgo.VPACK, 0, 0, 64, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 12, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 12, 13, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 13, 29, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 13, 20, []), - (NodeType.FLAG, WrapAlgo.HPACK, 0, 21, 29, []), - ]), - (NodeType.KWARGGROUP, WrapAlgo.VPACK, 1, 13, 64, [ - (NodeType.KEYWORD, WrapAlgo.HPACK, 1, 13, 23, []), - (NodeType.PARGGROUP, WrapAlgo.VPACK, 1, 24, 64, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 1, 24, 64, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 2, 24, 63, []), - (NodeType.COMMENT, WrapAlgo.HPACK, 3, 24, 64, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 4, 24, 54, []), - ]), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 4, 54, 55, []), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 53, [ + (NodeType.STATEMENT, 4, 0, 0, 53, [ + (NodeType.FUNNAME, 0, 0, 0, 12, []), + (NodeType.LPAREN, 0, 0, 12, 13, []), + (NodeType.ARGGROUP, 4, 1, 2, 53, [ + (NodeType.PARGGROUP, 0, 1, 2, 18, [ + (NodeType.ARGUMENT, 0, 1, 2, 9, []), + (NodeType.FLAG, 0, 1, 10, 18, []), + ]), + (NodeType.KWARGGROUP, 4, 2, 2, 53, [ + (NodeType.KEYWORD, 0, 2, 2, 12, []), + (NodeType.ARGGROUP, 4, 2, 13, 53, [ + (NodeType.COMMENT, 0, 2, 13, 53, []), + (NodeType.COMMENT, 0, 3, 13, 52, []), + (NodeType.COMMENT, 0, 4, 13, 53, []), + (NodeType.PARGGROUP, 0, 5, 13, 43, [ + (NodeType.ARGUMENT, 0, 5, 13, 43, []), ]), + ]), + ]), + ]), + (NodeType.RPAREN, 0, 5, 43, 44, []), + ]), +]), ]) def test_sortable_set(self): @@ -675,22 +798,26 @@ def test_sortable_set(self): self.do_layout_test("""\ set(SOURCES #[[cmf:sortable]] foo.cc bar.cc baz.cc) """, [ - (NodeType.BODY, WrapAlgo.HPACK, 0, 0, 51, [ - (NodeType.STATEMENT, WrapAlgo.HPACK, 0, 0, 51, [ - (NodeType.FUNNAME, WrapAlgo.HPACK, 0, 0, 3, []), - (NodeType.LPAREN, WrapAlgo.HPACK, 0, 3, 4, []), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 4, 11, [ - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 4, 11, []), - ]), - (NodeType.PARGGROUP, WrapAlgo.HPACK, 0, 12, 50, [ - (NodeType.COMMENT, WrapAlgo.HPACK, 0, 12, 29, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 30, 36, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 37, 43, []), - (NodeType.ARGUMENT, WrapAlgo.HPACK, 0, 44, 50, []), - ]), - (NodeType.RPAREN, WrapAlgo.HPACK, 0, 50, 51, []), - ]), - ]), +# pylint: disable=bad-continuation +# noqa: E122 +(NodeType.BODY, 0, 0, 0, 51, [ + (NodeType.STATEMENT, 0, 0, 0, 51, [ + (NodeType.FUNNAME, 0, 0, 0, 3, []), + (NodeType.LPAREN, 0, 0, 3, 4, []), + (NodeType.ARGGROUP, 0, 0, 4, 50, [ + (NodeType.PARGGROUP, 0, 0, 4, 11, [ + (NodeType.ARGUMENT, 0, 0, 4, 11, []), + ]), + (NodeType.PARGGROUP, 0, 0, 12, 50, [ + (NodeType.COMMENT, 0, 0, 12, 29, []), + (NodeType.ARGUMENT, 0, 0, 30, 36, []), + (NodeType.ARGUMENT, 0, 0, 37, 43, []), + (NodeType.ARGUMENT, 0, 0, 44, 50, []), + ]), + ]), + (NodeType.RPAREN, 0, 0, 50, 51, []), + ]), +]), ]) diff --git a/cmake_format/parse_funs/__init__.py b/cmake_format/parse_funs/__init__.py index db569ef..1870d19 100644 --- a/cmake_format/parse_funs/__init__.py +++ b/cmake_format/parse_funs/__init__.py @@ -46,7 +46,7 @@ def split_legacy_spec(cmdspec): elif kwarg == "COMMAND": subparser = parser.parse_shell_command elif isinstance(subspec, parser.IMPLICIT_PARG_TYPES): - subparser = parser.PositionalParser(subspec, []) + subparser = parser.StandardParser(subspec) elif isinstance(subspec, (commands.CommandSpec)): subparser = get_legacy_parse(subspec) else: diff --git a/cmake_format/parse_funs/file.py b/cmake_format/parse_funs/file.py index 85d26c2..2ad8f3f 100644 --- a/cmake_format/parse_funs/file.py +++ b/cmake_format/parse_funs/file.py @@ -9,6 +9,7 @@ parse_positionals, parse_standard, PositionalParser, + StandardParser, TreeNode, ) @@ -375,23 +376,23 @@ def parse_file(tokens, breakstack): "TIMESTAMP": parse_file_timestamp, "WRITE": parse_file_write, "APPEND": parse_file_write, - "TOUCH": PositionalParser('+', ["TOUCH"]), - "TOUCH_NO_CREATE": PositionalParser('+', ["TOUCH_NO_CREATE"]), + "TOUCH": StandardParser('+', flags=["TOUCH"]), + "TOUCH_NO_CREATE": StandardParser('+', flags=["TOUCH_NO_CREATE"]), "GENERATE": parse_file_generate_output, "GLOB": parse_file_glob, "GLOB_RECURSE": parse_file_glob, - "RENAME": PositionalParser(3, ["RENAME"]), - "REMOVE": PositionalParser('+', ["REMOVE"]), - "REMOVE_RECURSE": PositionalParser('+', ["REMOVE_RECURSE"]), - "MAKE_DIRECTORY": PositionalParser('+', ["MAKE_DIRECTORY"]), + "RENAME": StandardParser(3, flags=["RENAME"]), + "REMOVE": StandardParser('+', flags=["REMOVE"]), + "REMOVE_RECURSE": StandardParser('+', flags=["REMOVE_RECURSE"]), + "MAKE_DIRECTORY": StandardParser('+', flags=["MAKE_DIRECTORY"]), "COPY": parse_file_copy, "INSTALL": parse_file_copy, - "SIZE": PositionalParser(3, ["SIZE"]), - "READ_SYMLINK": PositionalParser(3, ["READ_SYMLINK"]), + "SIZE": StandardParser(3, flags=["SIZE"]), + "READ_SYMLINK": StandardParser(3, flags=["READ_SYMLINK"]), "CREATE_LINK": parse_file_create_link, - "RELATIVE_PATH": PositionalParser(4, ["RELATIVE_PATH"]), - "TO_CMAKE_PATH": PositionalParser(3, ["TO_CMAKE_PATH"]), - "TO_NATIVE_PATH": PositionalParser(3, ["TO_NATIVE_PATH"]), + "RELATIVE_PATH": StandardParser(4, flags=["RELATIVE_PATH"]), + "TO_CMAKE_PATH": StandardParser(3, flags=["TO_CMAKE_PATH"]), + "TO_NATIVE_PATH": StandardParser(3, flags=["TO_NATIVE_PATH"]), "DOWNLOAD": parse_file_xfer, "UPLOAD": parse_file_xfer, "LOCK": parse_file_lock diff --git a/cmake_format/parser.py b/cmake_format/parser.py index e4a953d..6ec9c99 100644 --- a/cmake_format/parser.py +++ b/cmake_format/parser.py @@ -134,29 +134,40 @@ def consume_comment(tokens): """ node = TreeNode(NodeType.COMMENT) - comment_tokens = node.children + if tokens[0].type == lexer.TokenType.BRACKET_COMMENT: + node.children.append(tokens.pop(0)) + return node + + comment_tokens = [] while tokens and tokens[0].type in COMMENT_TOKENS: - comment_tokens.append(tokens.pop(0)) + # If the next comment token is not column-aligned, then we don't + # merge it with the previous comment line + if (comment_tokens and + not are_column_aligned(comment_tokens[-1], tokens[0])): + break + + comment_token = tokens.pop(0) + comment_tokens.append(comment_token) + node.children.append(comment_token) # Multiple comments separated by only one newline are joined together into # a single block if (len(tokens) > 1 # pylint: disable=bad-continuation and tokens[0].type == lexer.TokenType.NEWLINE - and tokens[1].type in COMMENT_TOKENS): - comment_tokens.append(tokens.pop(0)) + and tokens[1].type in COMMENT_TOKENS): + node.children.append(tokens.pop(0)) # Multiple comments separated only by one newline and some whitespace are # joined together into a single block - # TODO(josh): maybe match only on comment tokens that start at the same - # column elif (len(tokens) > 2 # pylint: disable=bad-continuation and tokens[0].type == lexer.TokenType.NEWLINE and tokens[1].type == lexer.TokenType.WHITESPACE and tokens[2].type in COMMENT_TOKENS): - comment_tokens.append(tokens.pop(0)) - comment_tokens.append(tokens.pop(0)) + node.children.append(tokens.pop(0)) + node.children.append(tokens.pop(0)) + return node @@ -179,6 +190,13 @@ def is_valid_trailing_comment(token): not comment_is_tag(token)) +def are_column_aligned(token_a, token_b): + """ + Return true if both tokens are on the same column. + """ + return token_a.begin.col == token_b.begin.col + + def next_is_trailing_comment(tokens): """ Return true if there is a trailing comment in the token stream @@ -214,15 +232,22 @@ def consume_trailing_comment(parent, tokens): parent.children.append(node) comment_tokens = node.children + comment_tokens = [] while tokens and is_valid_trailing_comment(tokens[0]): - comment_tokens.append(tokens.pop(0)) + if (comment_tokens and + not are_column_aligned(comment_tokens[-1], tokens[0])): + break + + comment_token = tokens.pop(0) + comment_tokens.append(comment_token) + node.children.append(comment_token) # Multiple comments separated by only one newline are joined together into # a single block if (len(tokens) > 1 and tokens[0].type == lexer.TokenType.NEWLINE and is_valid_trailing_comment(tokens[1])): - comment_tokens.append(tokens.pop(0)) + node.children.append(tokens.pop(0)) # Multiple comments separated only by one newline and some whitespace are # joined together into a single block @@ -232,8 +257,8 @@ def consume_trailing_comment(parent, tokens): tokens[0].type == lexer.TokenType.NEWLINE and tokens[1].type == lexer.TokenType.WHITESPACE and is_valid_trailing_comment(tokens[2])): - comment_tokens.append(tokens.pop(0)) - comment_tokens.append(tokens.pop(0)) + node.children.append(tokens.pop(0)) + node.children.append(tokens.pop(0)) def get_normalized_kwarg(token): @@ -356,6 +381,29 @@ def consume_whitespace_and_comments(tokens, tree): break +def iter_syntactic_tokens(tokens): + """ + Return a generator over the list of tokens yielding only those that are + not whitespace + """ + skip_tokens = (lexer.TokenType.WHITESPACE, + lexer.TokenType.NEWLINE) + + for token in tokens: + if token.type in skip_tokens: + continue + yield token + + +def get_next_syntactic_token(tokens): + """ + return the first non-whitespace token in the list + """ + for token in iter_syntactic_tokens(tokens): + return token + return None + + def iter_semantic_tokens(tokens): """ Return a generator over the list of tokens yielding only those that @@ -457,31 +505,30 @@ def parse_positionals(tokens, npargs, flags, breakstack, sortable=False): tree.children.append(subtree) continue - # Otherwise we will consume the token - token = tokens.pop(0) - # If it is a whitespace token then put it directly in the parse tree at # the current depth - if token.type in WHITESPACE_TOKENS: - tree.children.append(token) + if tokens[0].type in WHITESPACE_TOKENS: + tree.children.append(tokens.pop(0)) continue # If it's a comment token not associated with an argument, then put it # directly into the parse tree at the current depth - if token.type in (lexer.TokenType.COMMENT, - lexer.TokenType.BRACKET_COMMENT): - child = TreeNode(NodeType.COMMENT) + if tokens[0].type in (lexer.TokenType.COMMENT, + lexer.TokenType.BRACKET_COMMENT): + before = len(tokens) + child = consume_comment(tokens) + assert len(tokens) < before, \ + "consume_comment didn't consume any tokens" tree.children.append(child) - child.children.append(token) continue # Otherwise is it is a positional argument, so add it to the tree as such - if get_normalized_kwarg(token) in flags: + if get_normalized_kwarg(tokens[0]) in flags: child = TreeNode(NodeType.FLAG) else: child = TreeNode(NodeType.ARGUMENT) - child.children.append(token) + child.children.append(tokens.pop(0)) consume_trailing_comment(child, tokens) tree.children.append(child) nconsumed += 1 @@ -797,14 +844,15 @@ def parse_conditional(tokens, breakstack): continue # Otherwise is it is a positional argument, so add it to the tree as such - token = tokens.pop(0) - if get_normalized_kwarg(token) in flags: - child = TreeNode(NodeType.FLAG) - else: - child = TreeNode(NodeType.ARGUMENT) - - child.children.append(token) - consume_trailing_comment(child, tokens) + child = parse_positionals(tokens, '+', flags, child_breakstack) + # token = tokens.pop(0) + # if get_normalized_kwarg(token) in flags: + # child = TreeNode(NodeType.FLAG) + # else: + # child = TreeNode(NodeType.ARGUMENT) + + # child.children.append(token) + # consume_trailing_comment(child, tokens) tree.children.append(child) return tree @@ -849,24 +897,6 @@ def is_shell_flag(token): return token.spelling.startswith("-") -def parse_shell_kwarg(tokens, breakstack): - """ - Parse a standard `--long-flag foo bar baz` sequence - """ - assert tokens[0].spelling.startswith("--") - - tree = TreeNode(NodeType.KWARGGROUP) - kwnode = TreeNode(NodeType.KEYWORD) - kwnode.children.append(tokens.pop(0)) - tree.children.append(kwnode) - - ntokens = len(tokens) - subtree = parse_positionals(tokens, "*", [], breakstack + [is_shell_flag]) - if len(tokens) < ntokens: - tree.children.append(subtree) - return tree - - def parse_shell_command(tokens, breakstack): """ Parser for the COMMAND kwarg lists in the form of:: @@ -876,54 +906,7 @@ def parse_shell_command(tokens, breakstack): The parser acts very similar to a standard parser where `--xxx` is treated as a keyword argument and `-x` is treated as a flag. """ - return parse_positionals(tokens, '*', [], breakstack) - - -def deprecated_parse_shell_command(tokens, breakstack): - """ - Parser for the COMMAND kwarg lists in the form of:: - - COMMAND foo --long-flag1 arg1 arg2 --long-flag2 -a -b -c arg3 arg4 - - The parser acts very similar to a standard parser where `--xxx` is treated - as a keyword argument and `-x` is treated as a flag. - """ - - # TODO(josh): remove dead code, or figure out when to enable it - tree = TreeNode(NodeType.ARGGROUP) - - # If it is a whitespace token then put it directly in the parse tree at - # the current depth - while tokens and tokens[0].type in WHITESPACE_TOKENS: - tree.children.append(tokens.pop(0)) - continue - - while tokens: - # Break if the next token belongs to a parent parser, i.e. if it - # matches a keyword argument of something higher in the stack, or if - # it closes a parent group. - if should_break(tokens[0], breakstack): - break - - # If it is a whitespace token then put it directly in the parse tree at - # the current depth - if tokens[0].type in WHITESPACE_TOKENS: - tree.children.append(tokens.pop(0)) - continue - - ntokens = len(tokens) - if tokens[0].spelling == "--": - subtree = parse_positionals(tokens, "*", [], breakstack) - elif tokens[0].spelling.startswith("--"): - subtree = parse_shell_kwarg(tokens, breakstack) - elif tokens[0].spelling.startswith("-"): - subtree = parse_shell_flags(tokens, breakstack) - else: - subtree = parse_positionals(tokens, "*", [], breakstack + [is_shell_flag]) - - assert len(tokens) < ntokens, "at {}".format(tokens[0]) - tree.children.append(subtree) - return tree + return parse_standard(tokens, '*', {}, [], breakstack) class PositionalParser(object): @@ -1034,7 +1017,7 @@ def consume_statement(tokens, parse_db): continue breakstack = [ParenBreaker()] - parse_fun = parse_db.get(fnname, PositionalParser()) + parse_fun = parse_db.get(fnname, StandardParser()) subtree = parse_fun(tokens, breakstack) node.children.append(subtree) diff --git a/cmake_format/parser_tests.py b/cmake_format/parser_tests.py index f27f5a2..c328971 100644 --- a/cmake_format/parser_tests.py +++ b/cmake_format/parser_tests.py @@ -127,8 +127,10 @@ def test_collapse_additional_newlines(self): (NodeType.ARGGROUP, [ (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + ]), ]), ]), ]), @@ -180,19 +182,23 @@ def test_nested_kwargs(self): ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), ]), @@ -221,27 +227,33 @@ def test_custom_command(self): ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.PARGGROUP, [ @@ -269,8 +281,10 @@ def test_shellcommand_parse(self): (NodeType.ARGGROUP, [ (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ @@ -278,21 +292,25 @@ def test_shellcommand_parse(self): ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + (NodeType.ARGUMENT, []), + ]), ]), ]), (NodeType.KWARGGROUP, [ (NodeType.KEYWORD, []), - (NodeType.PARGGROUP, [ - (NodeType.ARGUMENT, []), + (NodeType.ARGGROUP, [ + (NodeType.PARGGROUP, [ + (NodeType.ARGUMENT, []), + ]), ]), ]), ]), diff --git a/cmake_format/screw_users_test.py b/cmake_format/screw_users_test.py index d0be5c8..126ab3e 100644 --- a/cmake_format/screw_users_test.py +++ b/cmake_format/screw_users_test.py @@ -147,6 +147,17 @@ def assertCanFormat(self): self.assertFalse(failed_files) + def test_thisrepo(self): + thisdir = os.path.realpath(os.path.dirname(__file__)) + head, _ = os.path.split(thisdir) + head, _ = os.path.split(thisdir) + self.repository = head + self.configpath = ".cmake-format.py" + self.last_known_good = "efb0d6256fcbc4a3cfad9f5ebdaa34172d230ea2" + self.exclude_patterns += [ + r"test_latin.*\.cmake" + ] + def test_elektra(self): self.repository = "https://github.com/sanssecours/elektra" self.configpath = ".cmake-format.yaml" @@ -161,7 +172,8 @@ def test_arrow(self): self.configpath = "cmake-format.py" self.last_known_good = "e32c0b3279df76d68c93724f476dfcf0c1c44fa5" self.exclude_patterns += [ - r".*\.h\.cmake" + r".*\.h\.cmake", + r".*Dockerfile\.cmake", ] def test_aom(self): @@ -176,8 +188,8 @@ def test_vulkan_tools(self): def test_vulkan_validation_layers(self): self.repository = "https://github.com/KhronosGroup/Vulkan-ValidationLayers" - self.configpath = ".cmake-format.py" - self.last_known_good = "7212065b5a78afb13e7527379175a3b2ee939a14" + self.configpath = "scripts/cmake-format.py" + self.last_known_good = "adbfa85caafb5f240474dd2ae2414f0b78026a3f" self.exclude_patterns += [ ".*build-android/cmake/layerlib/CMakeLists.txt" ] diff --git a/cmake_format/test/test_in.cmake b/cmake_format/test/test_in.cmake index 8ea598d..a588b34 100644 --- a/cmake_format/test/test_in.cmake +++ b/cmake_format/test/test_in.cmake @@ -16,12 +16,11 @@ project(cmake_format_test) add_subdirectories(foo bar baz foo2 bar2 baz2) -# This very long command should be split to multiple lines +# This very long command should be wrapped set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) -# This command should be split into one line per entry because it has a long -# argument list. -set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc) +# This command should be split into one line per entry because it has a long argument list. +set(SOURCES source_a.cc source_b.cc source_d.cc source_e.cc source_f.cc source_g.cc source_h.cc) # The string in this command should not be split set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") @@ -68,7 +67,7 @@ if(foo) if(sbar) # This comment is in-scope. add_library(foo_bar_baz foo.cc bar.cc # this is a comment for arg2 - # this is more comment for arg2, it should be joined with the first. + # this is more comment for arg2, it should be joined with the first. baz.cc) # This comment is part of add_library other_command(some_long_argument some_long_argument) # this comment is very long and gets split across some lines diff --git a/cmake_format/test/test_out.cmake b/cmake_format/test/test_out.cmake index d6d5bed..0633af7 100644 --- a/cmake_format/test/test_out.cmake +++ b/cmake_format/test/test_out.cmake @@ -7,14 +7,9 @@ project(cmake_format_test) # This comment should remain right before the command call. Furthermore, the # command call should be formatted to a single line. -add_subdirectories(foo - bar - baz - foo2 - bar2 - baz2) - -# This very long command should be split to multiple lines +add_subdirectories(foo bar baz foo2 bar2 baz2) + +# This very long command should be wrapped set(HEADERS very_long_header_name_a.h very_long_header_name_b.h very_long_header_name_c.h) @@ -26,11 +21,12 @@ set(SOURCES source_d.cc source_e.cc source_f.cc - source_g.cc) + source_g.cc + source_h.cc) # The string in this command should not be split -set_target_properties(foo bar baz - PROPERTIES COMPILE_FLAGS "-std=c++11 -Wall -Wextra") +set_target_properties(foo bar baz PROPERTIES COMPILE_FLAGS + "-std=c++11 -Wall -Wextra") # This command has a very long argument and can't be aligned with the command # end, so it should be moved to a new line with block indent + 1. @@ -43,11 +39,9 @@ set(CMAKE_CXX_FLAGS "-std=c++11 -Wall -Wno-sign-compare -Wno-unused-parameter -xx") set(HEADERS - header_a.h - header_b.h # This comment should be preserved, moreover it should be split - # across two lines. - header_c.h - header_d.h) + header_a.h header_b.h # This comment should be preserved, moreover it should + # be split across two lines. + header_c.h header_d.h) # This part of the comment should be formatted but... # cmake-format: off @@ -72,30 +66,32 @@ set(HEADERS if(foo) if(sbar) # This comment is in-scope. - add_library(foo_bar_baz - foo.cc - bar.cc # this is a comment for arg2 this is more comment for - # arg2, it should be joined with the first. - baz.cc) # This comment is part of add_library - - other_command(some_long_argument some_long_argument) # this comment is very - # long and gets split - # across some lines - - other_command(some_long_argument some_long_argument some_long_argument) - # this comment is even longer and wouldn't make sense to pack at the end of - # the command so it gets it's own lines + add_library( + foo_bar_baz + foo.cc bar.cc # this is a comment for arg2 this is more comment for arg2, + # it should be joined with the first. + baz.cc) # This comment is part of add_library + + other_command( + some_long_argument some_long_argument) # this comment is very long and + # gets split across some lines + + other_command( + some_long_argument some_long_argument some_long_argument) # this comment + # is even longer + # and wouldn't + # make sense to + # pack at the + # end of the + # command so it + # gets it's own + # lines endif() endif() # This very long command should be broken up along keyword arguments foo(nonkwarg_a nonkwarg_b - HEADERS a.h - b.h - c.h - d.h - e.h - f.h + HEADERS a.h b.h c.h d.h e.h f.h SOURCES a.cc b.cc d.cc DEPENDS foo bar baz) diff --git a/cmake_format/tests.py b/cmake_format/tests.py index b9c1700..511fe35 100644 --- a/cmake_format/tests.py +++ b/cmake_format/tests.py @@ -4,7 +4,6 @@ # pylint: disable=unused-wildcard-import # pylint: disable=unused-import -from cmake_format.format_tests import * from cmake_format.invocation_tests import * from cmake_format.layout_tests import * from cmake_format.lexer_tests import * @@ -19,14 +18,16 @@ import TestAddLibraryCommand from cmake_format.command_tests.conditional_tests \ import TestConditionalCommands -from cmake_format.command_tests.set_tests \ - import TestSetCommand +from cmake_format.command_tests.export_tests \ + import TestExportCommand from cmake_format.command_tests.file_tests \ import TestFileCommands from cmake_format.command_tests.install_tests \ import TestInstallCommands -from cmake_format.command_tests.export_tests \ - import TestExportCommand +from cmake_format.command_tests.misc_tests \ + import TestMiscFormatting +from cmake_format.command_tests.set_tests \ + import TestSetCommand if __name__ == '__main__': unittest.main() diff --git a/cmake_format/vscode_extension/CHANGELOG.md b/cmake_format/vscode_extension/CHANGELOG.md index f214729..20e4f6c 100644 --- a/cmake_format/vscode_extension/CHANGELOG.md +++ b/cmake_format/vscode_extension/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +Note that the vscode extension release version matches the +cmake-format release version on [pypi][2]. + +### 0.6.0 + +No functional changes, just documentation update. + +### 0.5.5 + +- Modify vscode extension cwd to better support subtree configuration files +- Fix vscode extension args type configuration + +### 0.4.2 + +Fixed bug with using workspace path as `cwd` when calling `cmake-foramt`. + ## 0.4.1 - Initial release -- Working callout to cmake-format with configuration options. \ No newline at end of file +- Working callout to cmake-format with configuration options. + +[2]: https://pypi.org/project/cmake_format/ diff --git a/cmake_format/vscode_extension/README.md b/cmake_format/vscode_extension/README.md index d0584c7..5301bf1 100644 --- a/cmake_format/vscode_extension/README.md +++ b/cmake_format/vscode_extension/README.md @@ -41,19 +41,5 @@ This extension contributes the following settings: You can find known issues with `cmake-format` on [github][3]. Feel free to file issues with the vscode extension there as well. -## Release Notes - -### 0.4.2 - -Fixed bug with using workspace path as `cwd` when calling `cmake-foramt`. - -### 0.4.1 - -Initial release. Note that the vscode extension release version matches the -cmake-format release version on [pypi][2]. - - - [1]: https://github.com/cheshirekow/cmake_format -[2]: https://pypi.org/project/cmake_format/ [3]: https://github.com/cheshirekow/cmake_format/issues diff --git a/cmake_format/vscode_extension/package-lock.json b/cmake_format/vscode_extension/package-lock.json index 11b89f6..b46a4ad 100644 --- a/cmake_format/vscode_extension/package-lock.json +++ b/cmake_format/vscode_extension/package-lock.json @@ -1,6 +1,6 @@ { "name": "cmake-format", - "version": "0.4.2", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -481,22 +481,6 @@ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", "dev": true }, - "event-stream": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.6.tgz", - "integrity": "sha512-dGXNg4F/FgVzlApjzItL+7naHutA3fDqbV/zAZqDDlXTjiMnQmZKu+prImWKszeBM5UQeGvAl3u1wBiKeDh61g==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "flatmap-stream": "^0.1.0", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" - } - }, "expand-brackets": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", diff --git a/cmake_format/vscode_extension/package.json b/cmake_format/vscode_extension/package.json index 6b538e8..555b12c 100644 --- a/cmake_format/vscode_extension/package.json +++ b/cmake_format/vscode_extension/package.json @@ -2,7 +2,7 @@ "name": "cmake-format", "displayName": "cmake-format", "description": "Format listfiles so they don't look like crap", - "version": "0.5.5", + "version": "0.6.0", "publisher": "cheshirekow", "repository": "https://github.com/cheshirekow/cmake_format", "icon": "images/cmake-format-logo.png", diff --git a/doc/find_rst.py b/doc/find_rst.py new file mode 100644 index 0000000..3b540b4 --- /dev/null +++ b/doc/find_rst.py @@ -0,0 +1,64 @@ +""" +Find all restructured text files and write them out to a manifest +""" + +import argparse +import os + +EXCLUDE_DIRS = [ + ".build", + ".git" +] + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-m", "--manifest-path", + help="Path to the manifest file to create/update") + parser.add_argument("-t", "--touch", action="store_true", + help="Touch the manifest if any rst files are newer") + parser.add_argument("rootdir") + args = parser.parse_args() + + rootdir = os.path.realpath(args.rootdir) + + latest_mtime = 0 + + fileset = set() + for parent, dirnames, filenames in os.walk(rootdir): + dirnames[:] = sorted(dirnames) + relpath_parent = os.path.relpath(parent, rootdir) + if relpath_parent in EXCLUDE_DIRS: + dirnames[:] = [] + continue + + for filename in filenames: + if filename.endswith(".rst"): + if relpath_parent == ".": + relpath_file = filename + else: + relpath_file = os.path.join(relpath_parent, filename) + file_mtime = os.path.getmtime(os.path.join(rootdir, relpath_file)) + latest_mtime = max(latest_mtime, file_mtime) + fileset.add(relpath_file) + + manifest_mtime = 0 + manifest_path = os.path.realpath(args.manifest_path) + manifest_set = set() + if os.path.exists(manifest_path): + manifest_mtime = os.path.getmtime(manifest_path) + with open(manifest_path, "r") as infile: + for line in infile: + manifest_set.add(line.strip()) + + if manifest_set != fileset: + print("RST manifest has changed") + with open(manifest_path, "w") as outfile: + for relpath_file in sorted(fileset): + outfile.write(relpath_file) + outfile.write("\n") + elif args.touch and latest_mtime > manifest_mtime: + print("An RST file has changed") + os.utime(manifest_path, None) + +if __name__ == "__main__": + main()