meson format command

pull/13040/merge
Charles Brunet 8 months ago committed by Dylan Baker
parent bd4fd90730
commit 2b37101998
  1. 79
      docs/markdown/Commands.md
  2. 4
      docs/markdown/snippets/meson_format_cmd.md
  3. 4
      mesonbuild/mesonmain.py
  4. 979
      mesonbuild/mformat.py
  5. 3
      mesonbuild/mparser.py
  6. 1
      run_format_tests.py
  7. 1
      run_mypy.py
  8. 4
      run_project_tests.py
  9. 47
      test cases/format/1 default/crazy_comments.meson
  10. 73
      test cases/format/1 default/indentation.meson
  11. 14
      test cases/format/1 default/meson.build
  12. 47
      test cases/format/2 muon/crazy_comments.meson
  13. 71
      test cases/format/2 muon/indentation.meson
  14. 14
      test cases/format/2 muon/meson.build
  15. 15
      test cases/format/2 muon/muon.ini
  16. 8
      test cases/format/3 editorconfig/.editorconfig
  17. 47
      test cases/format/3 editorconfig/crazy_comments.meson
  18. 73
      test cases/format/3 editorconfig/indentation.meson
  19. 31
      test cases/format/3 editorconfig/meson.build
  20. 1
      test cases/format/3 editorconfig/options.ini
  21. 47
      test cases/format/4 config/crazy_comments.meson
  22. 73
      test cases/format/4 config/indentation.meson
  23. 19
      test cases/format/4 config/meson.build
  24. 11
      test cases/format/4 config/meson.format
  25. 69
      test cases/format/5 transform/default.expected.meson
  26. 15
      test cases/format/5 transform/default.ini
  27. 7
      test cases/format/5 transform/file_compare.py
  28. 7
      test cases/format/5 transform/genexpected.cmd
  29. 29
      test cases/format/5 transform/meson.build
  30. 1
      test cases/format/5 transform/meson.options
  31. 69
      test cases/format/5 transform/muon.expected.meson
  32. 15
      test cases/format/5 transform/muon.ini
  33. 48
      test cases/format/5 transform/options.expected.meson
  34. 15
      test cases/format/5 transform/options.ini
  35. 37
      test cases/format/5 transform/source.meson
  36. 11
      test cases/format/5 transform/test.json
  37. 1
      unittests/allplatformstests.py
  38. 43
      unittests/platformagnostictests.py

@ -396,3 +396,82 @@ format should be used. There are currently 3 formats supported:
seems to be properly supported by vscode.
{{ devenv_arguments.inc }}
### format
*(since 1.5.0)*
{{ format_usage.inc }}
Format specified `meson.build` documents. For compatibility with `muon`, `fmt`
is an alias to `format`.
{{ format_arguments.inc }}
The configuration file is a `.ini` file. If a `meson.format` file exists
beside the provided build file to analyze, and no configuration file is
provided on the command line, the `meson.format` file is automatically used.
If no build file is provided on the command line, the `meson.build` file in
current directory is analyzed.
The following options are recognized:
- max_line_length (int): When an array, a dict, a function or a method
would be longer that this, it is formatted one argument per line
(Default is 80).
- indent_by (str): Indentation to use (Default is four spaces `' '`).
- space_array (bool): Whether to add spaces between `[]` and array
arguments (default is false).
- kwargs_force_multiline (bool): If true, arguments are formatted one per
line as soon as there is a keyword argument (default is false).
- wide_colon (bool): If true, a space is placed before colon in dict
and in keyword arguments (default is false).
- no_single_comma_function (bool): If true, a comma is never appended
to function arguments if there is only one argument, even if
using multiline arguments (default is false).
- end_of_line ('cr', 'lf', 'crlf', 'native'): Line ending to use
(applied when using `--output` or `--inline` argument) (default
is 'native).
- indent_before_comments (str): Indentation to use before inline comments
(default is two spaces `' '`).
- simplify_string_literals (bool): When true, multiline strings are
converted to single line strings if they don't contain newlines.
Formatted strings are converted to normal strings if they don't
contain substitutions (default is true).
- insert_final_newline (bool): If true, force the `meson.build` file
to end with a newline character (default is true).
- 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 false).
- 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).
- use_editor_config (bool): When true, also uses config from .editorconfig .
The first six options are the same than for the `muon fmt` command.
It is also possible to use a `.editorconfig` file, by providing
the `--editor-config` option on the command line, or with the
`use_editor_config` option in the config file.
When `--recursive` option is specified, `meson.build` files from
`subdir` are also analyzed (must be used in conjunction with `--inplace`
or `--check-only` option).
#### Differences with `muon fmt`
The `meson format` command should be compatible with the `muon fmt` command.
However, it has more features, and some differences:
- By default, `meson format` put two spaces before inline comments,
while `muon fmt` only puts one.
- `muon fmt` can potentially mix crlf and lf end-of-lines, as it is not aware
of them. `meson format` will always be consistent in the output it produces.
- `muon fmt` only recognize the `indent_by` option from .editorconfig files.
`meson format` also recognizes `max_line_length`, `end_of_line`,
`insert_final_newline` and `tab_width` options.
- `meson format` has many additional format rules (see option list above).

@ -0,0 +1,4 @@
## New meson format command
This command is similar to `muon fmt` and allows to format a `meson.build`
document.

@ -61,7 +61,7 @@ def errorhandler(e: Exception, command: str) -> int:
class CommandLineParser:
def __init__(self) -> None:
# only import these once we do full argparse processing
from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile, mdevenv
from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile, mdevenv, mformat
from .scripts import env2mfile
from .wrap import wraptool
import shutil
@ -100,6 +100,8 @@ class CommandLineParser:
help_msg='Run commands in developer environment')
self.add_command('env2mfile', env2mfile.add_arguments, env2mfile.run,
help_msg='Convert current environment to a cross or native file')
self.add_command('format', mformat.add_arguments, mformat.run, aliases=['fmt'],
help_msg='Format meson source file')
# Add new commands above this line to list them in help command
self.add_command('help', self.add_help_arguments, self.run_help_command,
help_msg='Print help of a subcommand')

@ -0,0 +1,979 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2024 The Meson development team
from __future__ import annotations
import argparse
import re
import typing as T
from configparser import ConfigParser, MissingSectionHeaderError
from copy import deepcopy
from dataclasses import dataclass, field, fields, asdict
from pathlib import Path
from . import mparser
from .mesonlib import MesonException
from .ast.postprocess import AstConditionLevel
from .ast.printer import RawPrinter
from .ast.visitor import FullAstVisitor
from .environment import build_filename
if T.TYPE_CHECKING:
from typing_extensions import Literal
class DefaultConfigParser(ConfigParser):
def __init__(self, delimiters: T.Tuple[str, ...] = ('=', ':')):
super().__init__(delimiters=delimiters, interpolation=None)
def read_default(self, filename: Path) -> None:
if not filename.exists():
raise MesonException(f'Configuration file {filename} not found')
try:
super().read(filename, encoding='utf-8')
except MissingSectionHeaderError:
self.read_string(f'[{self.default_section}]\n' + filename.read_text(encoding='utf-8'))
def getstr(self, section: str, key: str, fallback: T.Optional[str] = None) -> T.Optional[str]:
value: T.Optional[str] = self.get(section, key, fallback=fallback)
if value:
value = value.strip('"').strip("'")
return value
def match_path(filename: str, pattern: str) -> bool:
'''recursive glob match for editorconfig sections'''
index = 0
num_ranges: T.List[T.Tuple[int, int]] = []
def curl_replace(m: re.Match) -> str:
nonlocal index
if '\\.\\.' in m[1]:
index += 1
low, high = m[1].split('\\.\\.')
num_ranges.append((int(low), int(high)))
return f'(?P<num{index}>-?[0-9]+)'
else:
return T.cast(str, m[1].replace(',', '|'))
pattern_re = pattern.replace('.', '\\.')
pattern_re = re.sub(r'(?<!\\)\?', '.', pattern_re) # ? -> .
pattern_re = re.sub(r'(?<![\\\*])\*(?!\*)', '([^/]*)', pattern_re) # * -> ([^/]*)
pattern_re = re.sub(r'(?<!\\)\*\*', '(.*)', pattern_re) # ** -> (.*)
pattern_re = re.sub(r'(?<!\\)\[!(.*?[^\\])\]', r'([^\1])', pattern_re) # [!name] -> [^name]
pattern_re = re.sub(r'(?<!\\)\{(.*?[^\\])}', curl_replace, pattern_re) # {}
if pattern.startswith('/'):
pattern_re = '^' + pattern_re
pattern_re += '$'
m = re.search(pattern_re, filename)
if m is None:
return False
for i in range(index):
try:
val = int(m[f'num{i+1}'])
if not num_ranges[i][0] <= val <= num_ranges[i][1]:
return False
except ValueError:
return False
return True
@dataclass
class EditorConfig:
indent_style: T.Optional[Literal['space', 'tab']] = field(default=None, metadata={'getter': DefaultConfigParser.get})
indent_size: T.Optional[int] = field(default=None, metadata={'getter': DefaultConfigParser.getint})
tab_width: T.Optional[int] = field(default=None, metadata={'getter': DefaultConfigParser.getint})
end_of_line: T.Optional[Literal['lf', 'cr', 'crlf']] = field(default=None, metadata={'getter': DefaultConfigParser.get})
charset: T.Optional[Literal['latin1', 'utf-8', 'utf-8-bom', 'utf-16be', 'utf-16le']] = field(default=None, metadata={'getter': DefaultConfigParser.get})
trim_trailing_whitespace: T.Optional[bool] = field(default=None, metadata={'getter': DefaultConfigParser.getboolean})
insert_final_newline: T.Optional[bool] = field(default=None, metadata={'getter': DefaultConfigParser.getboolean})
max_line_length: T.Optional[T.Union[Literal['off'], int]] = field(default=None, metadata={'getter': DefaultConfigParser.get})
@dataclass
class FormatterConfig:
# Config keys compatible with muon
max_line_length: T.Optional[int] = field(
default=None,
metadata={'getter': DefaultConfigParser.getint,
'default': 80,
})
indent_by: T.Optional[str] = field(
default=None,
metadata={'getter': DefaultConfigParser.getstr,
'default': ' ',
})
space_array: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': False,
})
kwargs_force_multiline: T.Optional[bool] = field(
default=None, # kwa_ml
metadata={'getter': DefaultConfigParser.getboolean,
'default': False,
})
wide_colon: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': False,
})
no_single_comma_function: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': False,
})
# Additional config keys
end_of_line: T.Optional[Literal['cr', 'lf', 'crlf', 'native']] = field(
default=None,
metadata={'getter': DefaultConfigParser.getstr,
'default': 'native',
})
indent_before_comments: T.Optional[str] = field(
default=None,
metadata={'getter': DefaultConfigParser.getstr,
'default': ' ',
})
simplify_string_literals: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': True,
})
insert_final_newline: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': True,
})
tab_width: T.Optional[int] = field(
default=None,
metadata={'getter': DefaultConfigParser.getint,
'default': 4,
}
)
sort_files: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': True,
})
group_arg_value: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': False,
})
use_editor_config: T.Optional[bool] = field(
default=None,
metadata={'getter': DefaultConfigParser.getboolean,
'default': False,
})
@classmethod
def default(cls) -> FormatterConfig:
defaults = {f.name: f.metadata['default'] for f in fields(cls)}
return cls(**defaults)
def update(self, config: FormatterConfig) -> FormatterConfig:
"""Returns copy of self updated with other config"""
new_config = deepcopy(self)
for key, value in asdict(config).items():
if value is not None:
setattr(new_config, key, value)
return new_config
def with_editorconfig(self, editorconfig: EditorConfig) -> FormatterConfig:
"""Returns copy of self updated with editorconfig"""
config = deepcopy(self)
if editorconfig.indent_style == 'space':
indent_size = editorconfig.indent_size or 4
config.indent_by = indent_size * ' '
elif editorconfig.indent_style == 'tab':
config.indent_by = '\t'
elif editorconfig.indent_size:
config.indent_by = editorconfig.indent_size * ' '
if editorconfig.max_line_length == 'off':
config.max_line_length = 0
elif editorconfig.max_line_length:
config.max_line_length = int(editorconfig.max_line_length)
if editorconfig.end_of_line:
config.end_of_line = editorconfig.end_of_line
if editorconfig.insert_final_newline:
config.insert_final_newline = editorconfig.insert_final_newline
if editorconfig.tab_width:
config.tab_width = editorconfig.tab_width
return config
@property
def newline(self) -> T.Optional[str]:
if self.end_of_line == 'crlf':
return '\r\n'
if self.end_of_line == 'lf':
return '\n'
if self.end_of_line == 'cr':
return '\r'
return None
class MultilineArgumentDetector(FullAstVisitor):
def __init__(self, config: FormatterConfig):
self.config = config
self.is_multiline = False
def enter_node(self, node: mparser.BaseNode) -> None:
if node.whitespaces and '#' in node.whitespaces.value:
self.is_multiline = True
elif isinstance(node, mparser.StringNode) and node.is_multiline:
self.is_multiline = True
def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
if node.is_multiline:
self.is_multiline = True
if self.is_multiline:
return
if self.config.kwargs_force_multiline and node.kwargs:
self.is_multiline = True
super().visit_ArgumentNode(node)
class TrimWhitespaces(FullAstVisitor):
def __init__(self, config: FormatterConfig):
self.config = config
self.in_block_comments = False
self.in_arguments = 0
self.indent_comments = ''
def visit_default_func(self, node: mparser.BaseNode) -> None:
self.enter_node(node)
node.whitespaces.accept(self)
def enter_node(self, node: mparser.BaseNode) -> None:
if isinstance(node, mparser.WhitespaceNode):
return
if not node.whitespaces:
# Ensure every node has a whitespace node
node.whitespaces = mparser.WhitespaceNode(mparser.Token('whitespace', node.filename, 0, 0, 0, (0, 0), ''))
node.whitespaces.condition_level = node.condition_level
def exit_node(self, node: mparser.BaseNode) -> None:
pass
def move_whitespaces(self, from_node: mparser.BaseNode, to_node: mparser.BaseNode) -> None:
to_node.whitespaces.value = from_node.whitespaces.value + to_node.whitespaces.value
from_node.whitespaces = None
to_node.whitespaces.accept(self)
def add_space_after(self, node: mparser.BaseNode) -> None:
if not node.whitespaces.value:
node.whitespaces.value = ' '
def add_nl_after(self, node: mparser.BaseNode, force: bool = False) -> None:
if not node.whitespaces.value:
node.whitespaces.value = '\n'
elif force and not node.whitespaces.value.endswith('\n'):
node.whitespaces.value += '\n'
def dedent(self, value: str) -> str:
if value.endswith(self.config.indent_by):
value = value[:-len(self.config.indent_by)]
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)
def visit_EmptyNode(self, node: mparser.EmptyNode) -> None:
self.enter_node(node)
self.in_block_comments = True
node.whitespaces.accept(self)
self.in_block_comments = False
def visit_WhitespaceNode(self, node: mparser.WhitespaceNode) -> None:
lines = node.value.splitlines(keepends=True)
node.value = ''
in_block_comments = self.in_block_comments
with_comments = ['#' in line for line in lines] + [False]
for i, line in enumerate(lines):
has_nl = line.endswith('\n')
line = line.strip()
if line.startswith('#'):
if not in_block_comments:
node.value += self.config.indent_before_comments
else:
node.value += self.indent_comments
node.value += line
if has_nl and (line or with_comments[i+1] or not self.in_arguments):
node.value += '\n'
in_block_comments = True
if node.value.endswith('\n'):
node.value += self.indent_comments
def visit_SymbolNode(self, node: mparser.SymbolNode) -> None:
super().visit_SymbolNode(node)
if node.value in "([{" and node.whitespaces.value == '\n':
node.whitespaces.value = ''
def visit_StringNode(self, node: mparser.StringNode) -> None:
self.enter_node(node)
if self.config.simplify_string_literals:
if node.is_multiline and '\n' not in node.value:
node.is_multiline = False
node.value = node.escape()
if node.is_fstring and '@' not in node.value:
node.is_fstring = False
self.exit_node(node)
def visit_UnaryOperatorNode(self, node: mparser.UnaryOperatorNode) -> None:
super().visit_UnaryOperatorNode(node)
self.move_whitespaces(node.value, node)
def visit_NotNode(self, node: mparser.NotNode) -> None:
super().visit_UnaryOperatorNode(node)
if not node.operator.whitespaces.value:
node.operator.whitespaces.value = ' '
self.move_whitespaces(node.value, node)
def visit_BinaryOperatorNode(self, node: mparser.BinaryOperatorNode) -> None:
super().visit_BinaryOperatorNode(node)
self.add_space_after(node.left)
self.add_space_after(node.operator)
self.move_whitespaces(node.right, node)
def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
super().visit_ArrayNode(node)
self.move_whitespaces(node.rbracket, node)
if node.lbracket.whitespaces.value:
node.args.is_multiline = True
if node.args.arguments and not node.args.is_multiline and self.config.space_array:
self.add_space_after(node.lbracket)
self.add_space_after(node.args)
def visit_DictNode(self, node: mparser.DictNode) -> None:
super().visit_DictNode(node)
self.move_whitespaces(node.rcurl, node)
if node.lcurl.whitespaces.value:
node.args.is_multiline = True
def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None:
self.enter_node(node)
if node.pre_whitespaces:
self.in_block_comments = True
node.pre_whitespaces.accept(self)
self.in_block_comments = False
else:
node.pre_whitespaces = mparser.WhitespaceNode(mparser.Token('whitespace', node.filename, 0, 0, 0, (0, 0), ''))
for i in node.lines:
i.accept(self)
self.exit_node(node)
if node.lines:
self.move_whitespaces(node.lines[-1], node)
else:
node.whitespaces.accept(self)
if node.condition_level == 0 and self.config.insert_final_newline:
self.add_nl_after(node, force=True)
indent = node.condition_level * self.config.indent_by
if indent and node.lines:
node.pre_whitespaces.value += indent
for line in node.lines[:-1]:
line.whitespaces.value += indent
def visit_IndexNode(self, node: mparser.IndexNode) -> None:
super().visit_IndexNode(node)
self.move_whitespaces(node.rbracket, node)
def visit_MethodNode(self, node: mparser.MethodNode) -> None:
super().visit_MethodNode(node)
self.move_whitespaces(node.rpar, node)
if node.lpar.whitespaces.value:
node.args.is_multiline = True
def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
if node.func_name.value == 'files':
if self.config.sort_files:
self.sort_arguments(node.args)
if len(node.args.arguments) == 1 and not node.args.kwargs:
arg = node.args.arguments[0]
if isinstance(arg, mparser.ArrayNode):
if not arg.lbracket.whitespaces or not arg.lbracket.whitespaces.value.strip():
# files([...]) -> files(...)
node.args = arg.args
super().visit_FunctionNode(node)
self.move_whitespaces(node.rpar, node)
if node.lpar.whitespaces.value:
node.args.is_multiline = True
def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None:
super().visit_AssignmentNode(node)
self.add_space_after(node.var_name)
self.add_space_after(node.operator)
self.move_whitespaces(node.value, node)
def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None:
super().visit_ForeachClauseNode(node)
self.add_space_after(node.foreach_)
self.add_space_after(node.varnames[-1])
for comma in node.commas:
self.add_space_after(comma)
self.add_space_after(node.colon)
node.block.whitespaces.value += node.condition_level * self.config.indent_by
self.move_whitespaces(node.endforeach, node)
def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None:
super().visit_IfClauseNode(node)
self.move_whitespaces(node.endif, node)
if isinstance(node.elseblock, mparser.ElseNode):
node.elseblock.whitespaces.value += node.condition_level * self.config.indent_by
else:
node.ifs[-1].whitespaces.value += node.condition_level * self.config.indent_by
def visit_IfNode(self, node: mparser.IfNode) -> None:
super().visit_IfNode(node)
self.add_space_after(node.if_)
self.move_whitespaces(node.block, node)
def visit_ElseNode(self, node: mparser.ElseNode) -> None:
super().visit_ElseNode(node)
self.move_whitespaces(node.block, node)
def visit_TernaryNode(self, node: mparser.TernaryNode) -> None:
super().visit_TernaryNode(node)
self.add_space_after(node.condition)
self.add_space_after(node.questionmark)
self.add_space_after(node.trueblock)
self.add_space_after(node.colon)
self.move_whitespaces(node.falseblock, node)
def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
if not node.is_multiline:
ml_detector = MultilineArgumentDetector(self.config)
node.accept(ml_detector)
if ml_detector.is_multiline:
node.is_multiline = True
self.in_arguments += 1
super().visit_ArgumentNode(node)
self.in_arguments -= 1
if not node.arguments and not node.kwargs:
node.whitespaces.accept(self)
return
last_node: mparser.BaseNode
has_trailing_comma = len(node.commas) == len(node.arguments) + len(node.kwargs)
if has_trailing_comma:
last_node = node.commas[-1]
elif node.kwargs:
for last_node in node.kwargs.values():
pass
else:
last_node = node.arguments[-1]
self.move_whitespaces(last_node, node)
if not node.is_multiline and '#' not in node.whitespaces.value:
node.whitespaces.value = ''
def visit_ParenthesizedNode(self, node: mparser.ParenthesizedNode) -> None:
self.enter_node(node)
is_multiline = node.lpar.whitespaces and '#' in node.lpar.whitespaces.value
if is_multiline:
self.indent_comments += self.config.indent_by
node.lpar.accept(self)
node.inner.accept(self)
if is_multiline:
node.inner.whitespaces.value = self.dedent(node.inner.whitespaces.value)
self.indent_comments = self.dedent(self.indent_comments)
self.add_nl_after(node.inner)
node.rpar.accept(self)
self.move_whitespaces(node.rpar, node)
class ArgumentFormatter(FullAstVisitor):
def __init__(self, config: FormatterConfig):
self.config = config
self.level = 0
self.indent_after = False
self.is_function_arguments = False
def add_space_after(self, node: mparser.BaseNode) -> None:
if not node.whitespaces.value:
node.whitespaces.value = ' '
def add_nl_after(self, node: mparser.BaseNode, indent: int) -> None:
if not node.whitespaces.value or node.whitespaces.value == ' ':
node.whitespaces.value = '\n'
indent_by = (node.condition_level + indent) * self.config.indent_by
if indent_by:
node.whitespaces.value += indent_by
def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
self.enter_node(node)
if node.args.is_multiline:
self.level += 1
self.add_nl_after(node.lbracket, indent=self.level)
self.is_function_arguments = False
node.args.accept(self)
if node.args.is_multiline:
self.level -= 1
self.exit_node(node)
def visit_DictNode(self, node: mparser.DictNode) -> None:
self.enter_node(node)
if node.args.is_multiline:
self.level += 1
self.add_nl_after(node.lcurl, indent=self.level)
self.is_function_arguments = False
node.args.accept(self)
if node.args.is_multiline:
self.level -= 1
self.exit_node(node)
def visit_MethodNode(self, node: mparser.MethodNode) -> None:
self.enter_node(node)
node.source_object.accept(self)
if node.args.is_multiline:
self.level += 1
self.add_nl_after(node.lpar, indent=self.level)
self.is_function_arguments = True
node.args.accept(self)
if node.args.is_multiline:
self.level -= 1
self.exit_node(node)
def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
self.enter_node(node)
if node.args.is_multiline:
self.level += 1
self.add_nl_after(node.lpar, indent=self.level)
self.is_function_arguments = True
node.args.accept(self)
if node.args.is_multiline:
self.level -= 1
self.exit_node(node)
def visit_WhitespaceNode(self, node: mparser.WhitespaceNode) -> None:
lines = node.value.splitlines(keepends=True)
if lines:
indent = (node.condition_level + self.level) * self.config.indent_by
node.value = lines[0]
for line in lines[1:]:
if '#' in line and not line.startswith(indent):
node.value += indent
node.value += line
if self.indent_after and node.value.endswith(('\n', self.config.indent_by)):
node.value += indent
def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
is_function_arguments = self.is_function_arguments # record it, because it may change when visiting children
super().visit_ArgumentNode(node)
for colon in node.colons:
self.add_space_after(colon)
if self.config.wide_colon:
for key in node.kwargs:
self.add_space_after(key)
arguments_count = len(node.arguments) + len(node.kwargs)
has_trailing_comma = node.commas and len(node.commas) == arguments_count
if node.is_multiline:
need_comma = True
if arguments_count == 1 and is_function_arguments:
need_comma = not self.config.no_single_comma_function
if need_comma and not has_trailing_comma:
comma = mparser.SymbolNode(mparser.Token('comma', node.filename, 0, 0, 0, (0, 0), ','))
comma.condition_level = node.condition_level
node.commas.append(comma)
elif has_trailing_comma and not need_comma:
node.commas.pop(-1)
arg_index = 0
if self.config.group_arg_value:
for arg in node.arguments[:-1]:
group_args = False
if isinstance(arg, mparser.StringNode) and arg.value.startswith('--'):
next_arg = node.arguments[arg_index + 1]
if isinstance(next_arg, mparser.StringNode) and not next_arg.value.startswith('--'):
group_args = True
if group_args:
# keep '--arg', 'value' on same line
self.add_space_after(node.commas[arg_index])
elif arg_index < len(node.commas):
self.add_nl_after(node.commas[arg_index], self.level)
arg_index += 1
for comma in node.commas[arg_index:-1]:
self.add_nl_after(comma, self.level)
self.add_nl_after(node, self.level - 1)
else:
if has_trailing_comma and not (node.commas[-1].whitespaces and node.commas[-1].whitespaces.value):
node.commas.pop(-1)
for comma in node.commas:
self.add_space_after(comma)
self.exit_node(node)
def visit_ParenthesizedNode(self, node: mparser.ParenthesizedNode) -> None:
self.enter_node(node)
is_multiline = '\n' in node.lpar.whitespaces.value
if is_multiline:
current_indent_after = self.indent_after
self.indent_after = True
node.lpar.accept(self)
node.inner.accept(self)
if is_multiline:
self.indent_after = current_indent_after
node.rpar.accept(self)
self.exit_node(node)
class ComputeLineLengths(FullAstVisitor):
def __init__(self, config: FormatterConfig, level: int):
self.config = config
self.lengths: T.List[int] = []
self.length = 0
self.argument_stack: T.List[mparser.ArgumentNode] = []
self.level = level
self.need_regenerate = False
def visit_default_func(self, node: mparser.BaseNode) -> None:
self.enter_node(node)
assert hasattr(node, 'value')
self.length += len(str(node.value))
self.exit_node(node)
def len(self, line: str) -> int:
'''Compute line length, including tab stops'''
parts = line.split('\t')
line_length = len(parts[0])
for p in parts[1:]:
tab_length = ((self.length + line_length) % self.config.tab_width) or self.config.tab_width
line_length += tab_length + len(p)
return line_length
def count_multiline(self, value: str) -> None:
lines = value.splitlines(keepends=True)
for line in lines:
if line.endswith('\n'):
self.lengths.append(self.length + self.len(line) - 1)
self.length = 0
else:
self.length += self.len(line)
def visit_WhitespaceNode(self, node: mparser.WhitespaceNode) -> None:
self.count_multiline(node.value)
def visit_EmptyNode(self, node: mparser.EmptyNode) -> None:
self.enter_node(node)
self.exit_node(node)
def visit_NumberNode(self, node: mparser.NumberNode) -> None:
self.enter_node(node)
self.length += len(node.raw_value)
self.exit_node(node)
def visit_StringNode(self, node: mparser.StringNode) -> None:
self.enter_node(node)
if node.is_fstring:
self.length += 1
if node.is_multiline:
self.length += 3
self.count_multiline(node.value)
self.length += 3
else:
self.length += self.len(node.raw_value) + 2
self.exit_node(node)
def visit_ContinueNode(self, node: mparser.ContinueNode) -> None:
self.enter_node(node)
self.length += len('continue')
self.exit_node(node)
def visit_BreakNode(self, node: mparser.BreakNode) -> None:
self.enter_node(node)
self.length += len('break')
self.exit_node(node)
def split_if_needed(self, node: mparser.ArgumentNode) -> None:
if not node.is_multiline and self.length > self.config.max_line_length:
arg = self.argument_stack[self.level] if len(self.argument_stack) > self.level else node
if not arg.is_multiline:
arg.is_multiline = True
self.need_regenerate = True
def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None:
self.argument_stack.append(node)
super().visit_ArgumentNode(node)
self.split_if_needed(node)
self.argument_stack.pop(-1)
def visit_ArrayNode(self, node: mparser.ArrayNode) -> None:
self.enter_node(node)
node.lbracket.accept(self)
node.args.accept(self)
node.rbracket.accept(self)
self.split_if_needed(node.args) # split if closing bracket is too far
self.exit_node(node)
def visit_DictNode(self, node: mparser.DictNode) -> None:
self.enter_node(node)
node.lcurl.accept(self)
node.args.accept(self)
node.rcurl.accept(self)
self.split_if_needed(node.args) # split if closing bracket is too far
self.exit_node(node)
class SubdirFetcher(FullAstVisitor):
def __init__(self, current_dir: Path):
self.current_dir = current_dir
self.subdirs: T.List[Path] = []
def visit_FunctionNode(self, node: mparser.FunctionNode) -> None:
if node.func_name.value == 'subdir':
if node.args.arguments and isinstance(node.args.arguments[0], mparser.StringNode):
subdir = node.args.arguments[0].value
self.subdirs.append(self.current_dir / subdir)
super().visit_FunctionNode(node)
class Formatter:
def __init__(self, configuration_file: T.Optional[Path], use_editor_config: bool, fetch_subdirs: bool):
self.fetch_subdirs = fetch_subdirs
self.use_editor_config = use_editor_config
self.config = self.load_configuration(configuration_file)
self.current_config = self.config
self.current_dir = Path()
self.subdirs: T.List[Path] = []
def load_editor_config(self, source_file: Path) -> EditorConfig:
# See https://editorconfig.org/
config = EditorConfig()
for p in source_file.parents:
editorconfig_file = p / '.editorconfig'
if not editorconfig_file.exists():
continue
cp = DefaultConfigParser(delimiters=('=',))
cp.read_default(editorconfig_file)
sections = [section for section in cp.sections() if match_path(source_file.as_posix(), section)]
for f in fields(config):
if getattr(cp, f.name, None) is not None:
continue # value already set from higher file
getter = f.metadata['getter']
for section in sections:
value = getter(cp, section, f.name, fallback=None)
if value is not None:
setattr(config, f.name, value)
if cp.getboolean(cp.default_section, 'root'):
break
return config
def load_configuration(self, configuration_file: T.Optional[Path]) -> FormatterConfig:
config = FormatterConfig()
if configuration_file:
cp = DefaultConfigParser()
cp.read_default(configuration_file)
for f in fields(config):
getter = f.metadata['getter']
value = getter(cp, cp.default_section, f.name, fallback=None)
if value is not None:
setattr(config, f.name, value)
if config.use_editor_config:
self.use_editor_config = True
return config
def format(self, code: str, source_file: Path) -> str:
self.current_dir = source_file.parent
self.current_config = FormatterConfig.default()
if self.use_editor_config:
self.current_config = self.current_config.with_editorconfig(self.load_editor_config(source_file))
self.current_config = self.current_config.update(self.config)
ast = mparser.Parser(code, source_file.as_posix()).parse()
if self.fetch_subdirs:
subdir_fetcher = SubdirFetcher(self.current_dir)
ast.accept(subdir_fetcher)
self.subdirs = subdir_fetcher.subdirs
ast.accept(AstConditionLevel())
for level in range(5):
ast.accept(TrimWhitespaces(self.current_config))
ast.accept(ArgumentFormatter(self.current_config))
cll = ComputeLineLengths(self.current_config, level)
ast.accept(cll)
if not cll.need_regenerate:
break
printer = RawPrinter()
ast.accept(printer)
return printer.result
def add_arguments(parser: argparse.ArgumentParser) -> None:
inplace_group = parser.add_mutually_exclusive_group()
inplace_group.add_argument(
'-q', '--check-only',
action='store_true',
help='exit with 1 if files would be modified by meson format'
)
inplace_group.add_argument(
'-i', '--inplace',
action='store_true',
help='format files in-place'
)
parser.add_argument(
'-r', '--recursive',
action='store_true',
help='recurse subdirs (requires --check-only or --inplace option)',
)
parser.add_argument(
'-c', '--configuration',
metavar='meson.format',
type=Path,
help='read configuration from meson.format'
)
parser.add_argument(
'-e', '--editor-config',
action='store_true',
default=False,
help='try to read configuration from .editorconfig'
)
parser.add_argument(
'-o', '--output',
type=Path,
help='output file (implies having exactly one input)'
)
parser.add_argument(
'sources',
nargs='*',
type=Path,
help='meson source files'
)
def run(options: argparse.Namespace) -> int:
if options.output and len(options.sources) != 1:
raise MesonException('--output argument implies having exactly one source file')
if options.recursive and not (options.inplace or options.check_only):
raise MesonException('--recursive argument requires either --inplace or --check-only option')
sources: T.List[Path] = options.sources.copy() or [Path(build_filename)]
if not options.configuration:
default_config_path = sources[0].parent / 'meson.format'
if default_config_path.exists():
options.configuration = default_config_path
formatter = Formatter(options.configuration, options.editor_config, options.recursive)
while sources:
src_file = sources.pop(0)
if src_file.is_dir():
src_file = src_file / build_filename
try:
code = src_file.read_text(encoding='utf-8')
except IOError as e:
raise MesonException(f'Unable to read from {src_file}') from e
formatted = formatter.format(code, src_file)
if options.recursive:
sources.extend(formatter.subdirs)
if options.inplace:
try:
with src_file.open('w', encoding='utf-8', newline=formatter.current_config.newline) as sf:
sf.write(formatted)
except IOError as e:
raise MesonException(f'Unable to write to {src_file}') from e
elif options.check_only:
# TODO: add verbose output showing diffs
if code != formatted:
return 1
elif options.output:
try:
with options.output.open('w', encoding='utf-8', newline=formatter.current_config.newline) as of:
of.write(formatted)
except IOError as e:
raise MesonException(f'Unable to write to {src_file}') from e
else:
print(formatted, end='')
return 0
# TODO: remove empty newlines when more than N (2...)
# TODO: magic comment to prevent formatting
# TODO: handle meson.options ?
# TODO: split long lines on binary operators
# TODO: align series of assignements
# TODO: align comments
# TODO: move comments on long lines
# Differences from muon format:
# - By default, uses two spaces before comment, and added option for that
# - Muon will mix CRLF and LF on Windows files...
# - Support for end_of_line char
# - Support for max_line_length, end_of_line, insert_final_newline, tab_width in .editorconfig
# - 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
# - Option to group '--arg', 'value' on same line in multiline arguments

@ -343,6 +343,9 @@ class ArgumentNode(BaseNode):
self.kwargs = {}
self.order_error = False
# Attributes for the visitors
self.is_multiline = False
def prepend(self, statement: BaseNode) -> None:
if self.num_kwargs() > 0:
self.order_error = True

@ -52,6 +52,7 @@ def check_format() -> None:
'.eggs', '_cache', # e.g. .mypy_cache
'venv', # virtualenvs have DOS line endings
'120 rewrite', # we explicitly test for tab in meson.build file
'3 editorconfig',
}
for (root, _, filenames) in os.walk('.'):
if any([x in root for x in skip_dirs]):

@ -45,6 +45,7 @@ modules = [
'mesonbuild/utils/universal.py',
'mesonbuild/mconf.py',
'mesonbuild/mdist.py',
'mesonbuild/mformat.py',
'mesonbuild/minit.py',
'mesonbuild/minstall.py',
'mesonbuild/mintro.py',

@ -76,7 +76,8 @@ if T.TYPE_CHECKING:
ALL_TESTS = ['cmake', 'common', 'native', 'warning-meson', 'failing-meson', 'failing-build', 'failing-test',
'keyval', 'platform-osx', 'platform-windows', 'platform-linux',
'java', 'C#', 'vala', 'cython', 'rust', 'd', 'objective c', 'objective c++',
'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm', 'wayland'
'fortran', 'swift', 'cuda', 'python3', 'python', 'fpga', 'frameworks', 'nasm', 'wasm', 'wayland',
'format',
]
@ -1125,6 +1126,7 @@ def detect_tests_to_run(only: T.Dict[str, T.List[str]], use_tmp: bool) -> T.List
TestCategory('nasm', 'nasm'),
TestCategory('wasm', 'wasm', shutil.which('emcc') is None or backend is not Backend.ninja),
TestCategory('wayland', 'wayland', should_skip_wayland()),
TestCategory('format', 'format'),
]
categories = [t.category for t in all_tests]

@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: Stone Tickle <lattis@mochiro.moe>
# SPDX-License-Identifier: GPL-3.0-only
project('a')
if (
# comment
false # in a weird
) # place! # test
else
endif # test2
foreach a : (
# test 7
b # test 4
) # test 6 # test 3
endforeach
# test 5
a = [
1,
# inner
2, # between comma
# between comma 2
] # trailing
(
# hello
a()
)
(
# comment 1
# comment 2
# comment 3
a # comment 4
# comment 5
# comment 6
= (
# comment 7
1 # comment 8
# comment 9
+ 2 # comment 10
) # comment 11
# comment 12
) # comment 13
# trailing comment

@ -0,0 +1,73 @@
project(
'indentation',
default_options: {
'buildtype': 'release',
'default_library': 'shared',
'prefer_static': false,
'unity': 'off',
},
meson_version: '>= 1.5.0',
version: '1.2.3',
)
a = [
1,
2,
3,
[
4,
5,
6,
[
7,
8,
9,
[
10, # 10
11, # 11
12, # 12
],
13,
14,
15,
],
],
]
d = {}
if meson.project_version().version_compare('>1.2')
if meson.version().version_compare('>1.0')
foreach i : a
e = {
'a': 'a',
'b': 'b',
'c': 'c',
'd': [
1,
2,
3,
{
'e': 'e',
'f': 'f',
'g': 'g',
'h': {
'i': (
# a
1
# b
+
# c
2
),
'j': [
1, # 1
2, # 2
3, # 3
],
},
},
],
}
endforeach
endif
endif

@ -0,0 +1,14 @@
# This file is for testing meson format with default options
project('default format')
meson_cmd = find_program('meson')
meson_files = {
'self': files('meson.build'),
'comments': files('crazy_comments.meson'),
'indentation': files('indentation.meson'),
}
foreach name, f : meson_files
test(name, meson_cmd, args: ['format', '--check-only', f])
endforeach

@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: Stone Tickle <lattis@mochiro.moe>
# SPDX-License-Identifier: GPL-3.0-only
project('a')
if (
# comment
false # in a weird
) # place! # test
else
endif # test2
foreach a : (
# test 7
b # test 4
) # test 6 # test 3
endforeach
# test 5
a = [
1,
# inner
2, # between comma
# between comma 2
] # trailing
(
# hello
a()
)
(
# comment 1
# comment 2
# comment 3
a # comment 4
# comment 5
# comment 6
= (
# comment 7
1 # comment 8
# comment 9
+ 2 # comment 10
) # comment 11
# comment 12
) # comment 13
# trailing comment

@ -0,0 +1,71 @@
project(
'indentation',
default_options: {
'buildtype': 'release',
'default_library': 'shared',
'prefer_static': false,
'unity': 'off',
},
meson_version: '>= 1.5.0',
version: '1.2.3',
)
a = [
1,
2,
3,
[
4,
5,
6,
[
7,
8,
9,
[
10, # 10
11, # 11
12, # 12
],
13,
14,
15,
],
],
]
d = {}
if meson.project_version().version_compare('>1.2')
if meson.version().version_compare('>1.0')
foreach i : a
e = {
'a': 'a',
'b': 'b',
'c': 'c',
'd': [
1,
2,
3,
{
'e': 'e',
'f': 'f',
'g': 'g',
'h': {
'i': (
# a
1 # b
# c
+ 2
),
'j': [
1, # 1
2, # 2
3, # 3
],
},
},
],
}
endforeach
endif
endif

@ -0,0 +1,14 @@
# This file is for testing meson format is compatible with muon format
project('default format')
meson_cmd = find_program('meson')
meson_files = {
'self': files('meson.build'),
'comments': files('crazy_comments.meson'),
'indentation': files('indentation.meson'),
}
foreach name, f : meson_files
test(name, meson_cmd, args: ['fmt', '-q', '-c', files('muon.ini'), f])
endforeach

@ -0,0 +1,15 @@
; This config should behave like muon default config
; max_line_length = 80
; indent_by = ' '
; space_array = false
; kwargs_force_multiline = false
; wide_colon = false
; no_single_comma_function = false
indent_before_comments = ' '
end_of_line = lf
simplify_string_literals = false
; insert_final_newline = true
sort_files = false
; group_arg_value = false

@ -0,0 +1,8 @@
root = true
[*]
indent_style = tab
indent_size = 1
tab_width = 4
max_line_length = 60

@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: Stone Tickle <lattis@mochiro.moe>
# SPDX-License-Identifier: GPL-3.0-only
project('a')
if (
# comment
false # in a weird
) # place! # test
else
endif # test2
foreach a : (
# test 7
b # test 4
) # test 6 # test 3
endforeach
# test 5
a = [
1,
# inner
2, # between comma
# between comma 2
] # trailing
(
# hello
a()
)
(
# comment 1
# comment 2
# comment 3
a # comment 4
# comment 5
# comment 6
= (
# comment 7
1 # comment 8
# comment 9
+ 2 # comment 10
) # comment 11
# comment 12
) # comment 13
# trailing comment

@ -0,0 +1,73 @@
project(
'indentation',
default_options: {
'buildtype': 'release',
'default_library': 'shared',
'prefer_static': false,
'unity': 'off',
},
meson_version: '>= 1.5.0',
version: '1.2.3',
)
a = [
1,
2,
3,
[
4,
5,
6,
[
7,
8,
9,
[
10, # 10
11, # 11
12, # 12
],
13,
14,
15,
],
],
]
d = {}
if meson.project_version().version_compare('>1.2')
if meson.version().version_compare('>1.0')
foreach i : a
e = {
'a': 'a',
'b': 'b',
'c': 'c',
'd': [
1,
2,
3,
{
'e': 'e',
'f': 'f',
'g': 'g',
'h': {
'i': (
# a
1
# b
+
# c
2
),
'j': [
1, # 1
2, # 2
3, # 3
],
},
},
],
}
endforeach
endif
endif

@ -0,0 +1,31 @@
# This file is for testing meson format with editor config
project('default format')
meson_cmd = find_program('meson')
meson_files = {
'self': files('meson.build'),
'comments': files('crazy_comments.meson'),
'indentation': files('indentation.meson'),
}
foreach name, f : meson_files
test(
name,
meson_cmd,
args: ['format', '-e', '--check-only', f],
)
# Test that .editorconfig can also be loaded from options file
test(
name + '-fromconfig',
meson_cmd,
args: [
'format',
'-c',
files('options.ini'),
'--check-only',
f,
],
)
endforeach

@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: Stone Tickle <lattis@mochiro.moe>
# SPDX-License-Identifier: GPL-3.0-only
project('a')
if (
# comment
false # in a weird
) # place! # test
else
endif # test2
foreach a : (
# test 7
b # test 4
) # test 6 # test 3
endforeach
# test 5
a = [
1,
# inner
2, # between comma
# between comma 2
] # trailing
(
# hello
a()
)
(
# comment 1
# comment 2
# comment 3
a # comment 4
# comment 5
# comment 6
= (
# comment 7
1 # comment 8
# comment 9
+ 2 # comment 10
) # comment 11
# comment 12
) # comment 13
# trailing comment

@ -0,0 +1,73 @@
project(
'indentation',
default_options : {
'buildtype' : 'release',
'default_library' : 'shared',
'prefer_static' : false,
'unity' : 'off',
},
meson_version : '>= 1.5.0',
version : '1.2.3',
)
a = [
1,
2,
3,
[
4,
5,
6,
[
7,
8,
9,
[
10, # 10
11, # 11
12, # 12
],
13,
14,
15,
],
],
]
d = {}
if meson.project_version().version_compare('>1.2')
if meson.version().version_compare('>1.0')
foreach i : a
e = {
'a' : 'a',
'b' : 'b',
'c' : 'c',
'd' : [
1,
2,
3,
{
'e' : 'e',
'f' : 'f',
'g' : 'g',
'h' : {
'i' : (
# a
1
# b
+
# c
2
),
'j' : [
1, # 1
2, # 2
3, # 3
],
},
},
],
}
endforeach
endif
endif

@ -0,0 +1,19 @@
# This file is for testing meson format with custom options.
# It ensures 'meson.format' file is automatically loaded.
project('default format')
meson_cmd = find_program('meson')
meson_files = {
'self' : files('meson.build'),
'comments' : files('crazy_comments.meson'),
'indentation' : files('indentation.meson'),
}
foreach name, f : meson_files
test(
name,
meson_cmd,
args : [ 'format', '--check-only', f ],
)
endforeach

@ -0,0 +1,11 @@
; Different options for config
max_line_length = 120
indent_by = ' '
space_array = true
kwargs_force_multiline = true
wide_colon = true
no_single_comma_function = true
indent_before_comments = ' '
; end_of_line = 'native'

@ -0,0 +1,69 @@
project('a') # should be on one line
# List should be removed, and should be on one line
options_ini = 'options.ini'
f = files(options_ini, 'expected.meson', 'source.meson')
# This array should fit on one line
a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
# This array is too long and should be splitted
a2 = [
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
]
# space array
a3 = [1, 2, 3]
# no single comma function
fct = files(
'meson.build', # force multiline
)
# wide colon
d = {'a': 1, 'b': 2, 'c': 3}
# string conversion
'This is not a multiline'
'This is not a fstring'
# group arg value
arguments = [
'a',
'--opt_a',
'opt_a_value',
'b',
'c',
'--opt_d',
'--opt_e',
'opt_e_value',
'--opt_f',
'--opt_g',
'opt_g_value',
'other_value',
'again',
'--x',
]
# no final endline

@ -0,0 +1,15 @@
; Use default values for config
; max_line_length = 80
; indent_by = ' '
; space_array = false
; kwargs_force_multiline = false
; wide_colon = false
; no_single_comma_function = false
; indent_before_comments = ' '
; end_of_line = 'native'
; simplify_string_literals = true
; insert_final_newline = true
; sort_files = true
; group_arg_value = false

@ -0,0 +1,7 @@
#!/usr/bin/env python3
import sys
with open(sys.argv[1], 'r', encoding='utf-8') as f, open(sys.argv[2], 'r', encoding='utf-8') as g:
if f.read() != g.read():
sys.exit('contents are not equal')

@ -0,0 +1,7 @@
@echo off
REM This script generates the expected files
REM Please double-check the contents of those files before commiting them!!!
python ../../../meson.py format -o default.expected.meson source.meson
python ../../../meson.py format -c muon.ini -o muon.expected.meson source.meson
python ../../../meson.py format -c options.ini -o options.expected.meson source.meson

@ -0,0 +1,29 @@
project('format')
fs = import('fs')
meson_cmd = find_program('meson')
file_compare = find_program(files('file_compare.py'))
config = get_option('fmt_config')
source = files('source.meson')
config_file = files(config + '.ini')
expected = files(config + '.expected.meson')
transform = custom_target(
input: [config_file, source],
output: 'transformed.meson',
command: [
meson_cmd,
'format',
'--output', '@OUTPUT@',
'--configuration', '@INPUT@',
],
)
test(
'transform',
file_compare,
args: [transform, expected],
)

@ -0,0 +1 @@
option('fmt_config', type: 'string', value: 'default')

@ -0,0 +1,69 @@
project('a') # should be on one line
# List should be removed, and should be on one line
options_ini = 'options.ini'
f = files('expected.meson', 'source.meson', options_ini)
# This array should fit on one line
a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
# This array is too long and should be splitted
a2 = [
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
]
# space array
a3 = [1, 2, 3]
# no single comma function
fct = files(
'meson.build', # force multiline
)
# wide colon
d = {'a': 1, 'b': 2, 'c': 3}
# string conversion
'''This is not a multiline'''
f'This is not a fstring'
# group arg value
arguments = [
'a',
'--opt_a',
'opt_a_value',
'b',
'c',
'--opt_d',
'--opt_e',
'opt_e_value',
'--opt_f',
'--opt_g',
'opt_g_value',
'other_value',
'again',
'--x',
]
# no final endline

@ -0,0 +1,15 @@
; This config should behave like muon default config
; max_line_length = 80
; indent_by = ' '
; space_array = false
; kwargs_force_multiline = false
; wide_colon = false
; no_single_comma_function = false
indent_before_comments = ' '
end_of_line = lf
simplify_string_literals = false
; insert_final_newline = true
sort_files = false
; group_arg_value = false

@ -0,0 +1,48 @@
project('a') # should be on one line
# List should be removed, and should be on one line
options_ini = 'options.ini'
f = files(options_ini, 'expected.meson', 'source.meson')
# This array should fit on one line
a1 = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 ]
# This array is too long and should be splitted
a2 = [ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22 ]
# space array
a3 = [ 1, 2, 3 ]
# no single comma function
fct = files(
'meson.build' # force multiline
)
# wide colon
d = {
'a' : 1,
'b' : 2,
'c' : 3,
}
# string conversion
'This is not a multiline'
'This is not a fstring'
# group arg value
arguments = [
'a',
'--opt_a', 'opt_a_value',
'b',
'c',
'--opt_d',
'--opt_e', 'opt_e_value',
'--opt_f',
'--opt_g', 'opt_g_value',
'other_value',
'again',
'--x',
]
# no final endline

@ -0,0 +1,15 @@
; Different options for config
max_line_length = 120
indent_by = ' '
space_array = true
kwargs_force_multiline = true
wide_colon = true
no_single_comma_function = true
indent_before_comments = ' '
; end_of_line = 'native'
; simplify_string_literals = true
insert_final_newline = false
; sort_files = true
group_arg_value = true

@ -0,0 +1,37 @@
project(
'a'
) # should be on one line
# List should be removed, and should be on one line
options_ini = 'options.ini'
f = files(
[
'expected.meson', 'source.meson', options_ini])
# This array should fit on one line
a1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
# This array is too long and should be splitted
a2 = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
# space array
a3 = [ 1, 2, 3 ]
# no single comma function
fct = files(
'meson.build', # force multiline
)
# wide colon
d = {'a': 1, 'b': 2, 'c': 3}
# string conversion
'''This is not a multiline'''
f'This is not a fstring'
# group arg value
arguments = ['a', '--opt_a', 'opt_a_value', 'b', 'c', '--opt_d', '--opt_e', 'opt_e_value',
'--opt_f', '--opt_g', 'opt_g_value', 'other_value', 'again', '--x']
# no final endline

@ -0,0 +1,11 @@
{
"matrix": {
"options": {
"fmt_config": [
{ "val": "default"},
{ "val": "muon"},
{ "val": "options"}
]
}
}
}

@ -3945,6 +3945,7 @@ class AllPlatformTests(BasePlatformTests):
cmndstr = cmndline.split('{')[1]
self.assertIn('}', cmndstr)
help_commands = set(cmndstr.split('}')[0].split(','))
help_commands.remove('fmt') # Remove the alias
self.assertTrue(len(help_commands) > 0, 'Must detect some command names.')
self.assertEqual(md_commands | {'help'}, help_commands, f'Doc file: `{doc_path}`')

@ -16,6 +16,7 @@ from pathlib import Path
from .baseplatformtests import BasePlatformTests
from .helpers import is_ci
from mesonbuild.mesonlib import EnvironmentVariables, ExecutableSerialisation, MesonException, is_linux, python_command
from mesonbuild.mformat import match_path
from mesonbuild.optinterpreter import OptionInterpreter, OptionException
from run_tests import Backend
@ -291,6 +292,48 @@ class PlatformAgnosticTests(BasePlatformTests):
out = self.init(testdir, allow_fail=True)
self.assertNotIn('Unhandled python exception', out)
def test_editorconfig_match_path(self):
'''match_path function used to parse editorconfig in meson format'''
cases = [
('a.txt', '*.txt', True),
('a.txt', '?.txt', True),
('a.txt', 'a.t?t', True),
('a.txt', '*.build', False),
('/a.txt', '*.txt', True),
('/a.txt', '/*.txt', True),
('a.txt', '/*.txt', False),
('a/b/c.txt', 'a/b/*.txt', True),
('a/b/c.txt', 'a/*/*.txt', True),
('a/b/c.txt', '*/*.txt', True),
('a/b/c.txt', 'b/*.txt', True),
('a/b/c.txt', 'a/*.txt', False),
('a/b/c/d.txt', 'a/**/*.txt', True),
('a/b/c/d.txt', 'a/*', False),
('a/b/c/d.txt', 'a/**', True),
('a.txt', '[abc].txt', True),
('a.txt', '[!xyz].txt', True),
('a.txt', '[xyz].txt', False),
('a.txt', '[!abc].txt', False),
('a.txt', '{a,b,c}.txt', True),
('a.txt', '*.{txt,tex,cpp}', True),
('a.hpp', '*.{txt,tex,cpp}', False),
('a1.txt', 'a{0..9}.txt', True),
('a001.txt', 'a{0..9}.txt', True),
('a-1.txt', 'a{-10..10}.txt', True),
('a99.txt', 'a{0..9}.txt', False),
('a099.txt', 'a{0..9}.txt', False),
('a-1.txt', 'a{0..10}.txt', False),
]
for filename, pattern, expected in cases:
self.assertTrue(match_path(filename, pattern) is expected, f'{filename} -> {pattern}')
def test_error_configuring_subdir(self):
testdir = os.path.join(self.common_test_dir, '152 index customtarget')
out = self.init(os.path.join(testdir, 'subdir'), allow_fail=True)

Loading…
Cancel
Save