From 907c536bd95a0fbcf9e8dc9b1d9dc44fe218ffb4 Mon Sep 17 00:00:00 2001 From: Benjamin Gilbert Date: Thu, 4 Sep 2025 12:35:47 -0500 Subject: [PATCH] mformat: sort files() arguments naturally --- docs/markdown/Commands.md | 4 +++- docs/markdown/snippets/meson-format-sort.md | 5 +++++ mesonbuild/mformat.py | 7 +++---- mesonbuild/rewriter.py | 13 ++----------- mesonbuild/utils/universal.py | 15 +++++++++++++++ test cases/format/1 default/meson.build | 1 + test cases/format/1 default/sort_files.meson | 19 +++++++++++++++++++ 7 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 docs/markdown/snippets/meson-format-sort.md create mode 100644 test cases/format/1 default/sort_files.meson diff --git a/docs/markdown/Commands.md b/docs/markdown/Commands.md index 09d8fd95fcde..9b20a12e7771 100644 --- a/docs/markdown/Commands.md +++ b/docs/markdown/Commands.md @@ -473,7 +473,9 @@ The following options are recognized: - tab_width (int): Width of tab stops, used to compute line length when `indent_by` uses tab characters (default is 4). - sort_files (bool): When true, arguments of `files()` function are - sorted alphabetically (default is true). + sorted (default is true). *Since 1.10.0*, arguments are sorted + [naturally](Style-guide.md#sorting-source-paths) rather than + alphabetically. - group_arg_value (bool): When true, string argument with `--` prefix followed by string argument without `--` prefix are grouped on the same line, in multiline arguments (default is false). diff --git a/docs/markdown/snippets/meson-format-sort.md b/docs/markdown/snippets/meson-format-sort.md new file mode 100644 index 000000000000..00e292770240 --- /dev/null +++ b/docs/markdown/snippets/meson-format-sort.md @@ -0,0 +1,5 @@ +## `meson format` sorts `files()` arguments naturally + +If the `sort_files` option is enabled, as it is by default, `meson format` +now sorts `files()` arguments [naturally](Style-guide.md#sorting-source-paths) +rather than alphabetically. diff --git a/mesonbuild/mformat.py b/mesonbuild/mformat.py index 6b0429b2fa36..e59985ed49ea 100644 --- a/mesonbuild/mformat.py +++ b/mesonbuild/mformat.py @@ -12,7 +12,7 @@ import sys from . import mparser -from .mesonlib import MesonException +from .mesonlib import MesonException, pathname_sort_key from .ast.postprocess import AstConditionLevel from .ast.printer import RawPrinter from .ast.visitor import FullAstVisitor @@ -301,13 +301,12 @@ def dedent(self, value: str) -> str: return value def sort_arguments(self, node: mparser.ArgumentNode) -> None: - # TODO: natsort def sort_key(arg: mparser.BaseNode) -> str: if isinstance(arg, mparser.StringNode): return arg.raw_value return getattr(node, 'value', '') - node.arguments.sort(key=sort_key) + node.arguments.sort(key=lambda arg: pathname_sort_key(sort_key(arg))) def visit_EmptyNode(self, node: mparser.EmptyNode) -> None: self.enter_node(node) @@ -1064,5 +1063,5 @@ def run(options: argparse.Namespace) -> int: # - Option to simplify string literals # - Option to recognize and parse meson.build in subdirs # - Correctly compute line length when using tabs -# - By default, arguments in files() are sorted alphabetically +# - By default, arguments in files() are sorted naturally # - Option to group '--arg', 'value' on same line in multiline arguments diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py index 4c2fb11bc793..b632b98c0962 100644 --- a/mesonbuild/rewriter.py +++ b/mesonbuild/rewriter.py @@ -13,7 +13,7 @@ from .ast.interpreter import IntrospectionBuildTarget, IntrospectionDependency, _symbol from .interpreterbase import UnknownValue, TV_func from .interpreterbase.helpers import flatten -from mesonbuild.mesonlib import MesonException, setup_vsenv, relpath +from mesonbuild.mesonlib import MesonException, pathname_sort_key, relpath, setup_vsenv from . import mlog, environment from functools import wraps from .mparser import Token, ArrayNode, ArgumentNode, ArithmeticNode, AssignmentNode, BaseNode, StringNode, BooleanNode, ElementaryNode, IdNode, FunctionNode, PlusAssignmentNode @@ -951,15 +951,6 @@ def rel_source(src: str) -> str: # Sort files for i in to_sort_nodes: - def convert(text: str) -> T.Union[int, str]: - return int(text) if text.isdigit() else text.lower() - - def alphanum_key(key: str) -> T.List[T.Union[int, str]]: - return [convert(c) for c in re.split('([0-9]+)', key)] - - def path_sorter(key: str) -> T.List[T.Tuple[bool, T.List[T.Union[int, str]]]]: - return [(key.count('/') <= idx, alphanum_key(x)) for idx, x in enumerate(key.split('/'))] - if isinstance(i, FunctionNode) and i.func_name.value in BUILD_TARGET_FUNCTIONS: src_args = i.args.arguments[1:] target_name = [i.args.arguments[0]] @@ -968,7 +959,7 @@ def path_sorter(key: str) -> T.List[T.Tuple[bool, T.List[T.Union[int, str]]]]: target_name = [] unknown: T.List[BaseNode] = [x for x in src_args if not isinstance(x, StringNode)] sources: T.List[StringNode] = [x for x in src_args if isinstance(x, StringNode)] - sources = sorted(sources, key=lambda x: path_sorter(x.value)) + sources = sorted(sources, key=lambda x: pathname_sort_key(x.value)) i.args.arguments = target_name + unknown + T.cast(T.List[BaseNode], sources) def process(self, cmd: T.Dict[str, T.Any]) -> None: diff --git a/mesonbuild/utils/universal.py b/mesonbuild/utils/universal.py index ad652aab1bd4..f1a3cb5c79a8 100644 --- a/mesonbuild/utils/universal.py +++ b/mesonbuild/utils/universal.py @@ -138,6 +138,7 @@ class _VerPickleLoadable(Protocol): 'listify_array_value', 'partition', 'path_is_in_root', + 'pathname_sort_key', 'pickle_load', 'Popen_safe', 'Popen_safe_logged', @@ -2499,3 +2500,17 @@ def __get__(self, instance: object, cls: T.Type) -> _T: value = self.__func(instance) setattr(instance, self.__name, value) return value + + +def pathname_sort_key(key: str) -> tuple[tuple[bool, tuple[int | str, ...]], ...]: + '''Sort key for natural pathname sort, as defined in the Meson style guide. + Use as the key= argument to sort() or sorted().''' + + def convert(text: str) -> int | str: + return int(text) if text.isdigit() else text.lower() + + def alphanum_key(key: str) -> tuple[int | str, ...]: + return tuple(convert(c) for c in re.split('([0-9]+)', key)) + + return tuple((key.count('/') <= idx, alphanum_key(x)) + for idx, x in enumerate(key.split('/'))) diff --git a/test cases/format/1 default/meson.build b/test cases/format/1 default/meson.build index d3bb153eeb92..9f2d6260c9fe 100644 --- a/test cases/format/1 default/meson.build +++ b/test cases/format/1 default/meson.build @@ -8,6 +8,7 @@ meson_files = { 'comments': files('crazy_comments.meson'), 'indentation': files('indentation.meson'), 'gh13242': files('gh13242.meson'), + 'sort': files('sort_files.meson'), } # Ensure empty function are formatted correctly on long lines diff --git a/test cases/format/1 default/sort_files.meson b/test cases/format/1 default/sort_files.meson new file mode 100644 index 000000000000..9c5d03785885 --- /dev/null +++ b/test cases/format/1 default/sort_files.meson @@ -0,0 +1,19 @@ +# Based on the example in the Meson style guide + +sources = files( + 'aaa/a1.c', + 'aaa/A2.c', + 'aaa/a3.c', + 'bbb/subdir1/b1.c', + 'bbb/subdir2/b2.c', + 'bbb/subdir10/b3.c', + 'bbb/subdir20/b4.c', + 'bbb/b5.c', + 'bbb/b6.c', + 'c/a111.c', + 'c/ab0.c', + 'f1.c', + 'f2.c', + 'f10.c', + 'f20.c', +)