Repository: mesonbuild/meson

Issue:
get_options crashes if the argument containts invalid characters
**Describe the bug**
I accidentally updated  `get_options()`  instead `find_program()` call with the argument `../scripts/build-docs`.
This resulted into a crash:

```shell
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/mesonbuild/mesonmain.py", line 194, in run
    return options.run_func(options)
  File "/usr/lib/python3.10/site-packages/mesonbuild/msetup.py", line 318, in run
    app.generate()
  File "/usr/lib/python3.10/site-packages/mesonbuild/msetup.py", line 194, in generate
    self._generate(env)
  File "/usr/lib/python3.10/site-packages/mesonbuild/msetup.py", line 239, in _generate
    intr.run()
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreter/interpreter.py", line 2971, in run
    super().run()
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 164, in run
    self.evaluate_codeblock(self.ast, start=1)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 190, in evaluate_codeblock
    raise e
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 182, in evaluate_codeblock
    self.evaluate_statement(cur)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 196, in evaluate_statement
    return self.function_call(cur)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 521, in function_call
    res = func(node, func_args, kwargs)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/decorators.py", line 277, in wrapper
    return f(*nargs, **wrapped_kwargs)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/decorators.py", line 596, in wrapper
    return f(*wrapped_args, **wrapped_kwargs)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreter/interpreter.py", line 2383, in func_subdir
    self.evaluate_codeblock(codeblock)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 190, in evaluate_codeblock
    raise e
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 182, in evaluate_codeblock
    self.evaluate_statement(cur)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 198, in evaluate_statement
    self.assignment(cur)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 635, in assignment
    value = self.evaluate_statement(node.value)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 196, in evaluate_statement
    return self.function_call(cur)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/interpreterbase.py", line 521, in function_call
    res = func(node, func_args, kwargs)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/decorators.py", line 277, in wrapper
    return f(*nargs, **wrapped_kwargs)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreterbase/decorators.py", line 73, in wrapped
    return f(*wrapped_args, **wrapped_kwargs)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreter/interpreter.py", line 1103, in func_get_option
    opt = self.get_option_internal(optname)
  File "/usr/lib/python3.10/site-packages/mesonbuild/interpreter/interpreter.py", line 1060, in get_option_internal
    key = OptionKey.from_string(optname).evolve(subproject=self.subproject)
  File "/usr/lib/python3.10/site-packages/mesonbuild/utils/universal.py", line 2291, in from_string
    assert '.' not in opt
AssertionError

doc/meson.build:51:18: ERROR: Unhandled python exception

    This is a Meson bug and should be reported!
```

**To Reproduce**
```meson
project('get_option_test', ['c'])

get_option('../scripts/build-doc')
```

**Expected behavior**
No crash instead a error message.

**system parameters**
* plain native build
* openSUSE Tumbleweed 20230613
* Python 3.10.11
* Meson 1.1.1
* Ninja 1.11.1



Discussion:

Resolving patch:
diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py
--- a/mesonbuild/ast/interpreter.py
+++ b/mesonbuild/ast/interpreter.py
@@ -29,6 +29,7 @@
     InvalidArguments,
     BreakRequest,
     ContinueRequest,
+    Disabler,
     default_resolve_key,
 )
 
@@ -53,6 +54,7 @@
     NotNode,
     PlusAssignmentNode,
     TernaryNode,
+    TestCaseClauseNode,
 )
 
 if T.TYPE_CHECKING:
@@ -443,3 +445,6 @@ def flatten_kwargs(self, kwargs: T.Dict[str, TYPE_nvar], include_unknown_args: b
             elif isinstance(val, (str, bool, int, float)) or include_unknown_args:
                 flattened_kwargs[key] = val
         return flattened_kwargs
+
+    def evaluate_testcase(self, node: TestCaseClauseNode) -> Disabler | None:
+        return Disabler(subproject=self.subproject)
diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py
--- a/mesonbuild/interpreter/interpreter.py
+++ b/mesonbuild/interpreter/interpreter.py
@@ -37,6 +37,7 @@
 from ..interpreterbase import ObjectHolder, ContextManagerObject
 from ..interpreterbase import stringifyUserArguments
 from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule
+from ..optinterpreter import optname_regex
 
 from . import interpreterobjects as OBJ
 from . import compiler as compilerOBJ
@@ -1090,6 +1091,10 @@ def func_get_option(self, nodes: mparser.BaseNode, args: T.Tuple[str],
             raise InterpreterException('Having a colon in option name is forbidden, '
                                        'projects are not allowed to directly access '
                                        'options of other subprojects.')
+
+        if optname_regex.search(optname.split('.', maxsplit=1)[-1]) is not None:
+            raise InterpreterException(f'Invalid option name {optname!r}')
+
         opt = self.get_option_internal(optname)
         if isinstance(opt, coredata.UserFeatureOption):
             opt.name = optname


Test patch:
diff --git a/test cases/common/40 options/meson.build b/test cases/common/40 options/meson.build
--- a/test cases/common/40 options/meson.build	
+++ b/test cases/common/40 options/meson.build	
@@ -46,3 +46,11 @@ assert(get_option('wrap_mode') == 'default', 'Wrap mode option is broken.')
 assert(get_option('boolean_string') == false)
 assert(get_option('boolean_string2') == true)
 assert(get_option('integer_string') == 42)
+
+testcase expect_error('Invalid option name \'..invalid\'')
+  get_option('..invalid')
+endtestcase
+
+testcase expect_error('Invalid option name \'this.is.also.invalid\'')
+  get_option('this.is.also.invalid')
+endtestcase