Repository: sqlfluff/sqlfluff

Issue:
Catch uncaught exception jinja2.exceptions.TemplateSyntaxError
### Search before asking

- [X] I searched the [issues](https://github.com/sqlfluff/sqlfluff/issues) and found no similar issues.


### What Happened

Largely this is similar to https://github.com/sqlfluff/sqlfluff/issues/3199 where there is an uncaught exception which is crashing sqlfluff. Because sqlfluff crashed - instead of say reporting a templating error instead - the dbt Cloud IDE does not return any errors to the end user (dbt Cloud does not return Python stack traces).

### Expected Behaviour

Have such errors be caught and reported as templating errors instead so that sqlfluff doesn't crash :)

### Observed Behaviour

sqlfluff runs into an uncaught exception.

### How to reproduce

Add the following files to a folder:

```yaml
# dbt_project.yml
name: my_dbt_project
profile: all
config-version: 2
version: 1.0

models:
  my_dbt_project:
    +materialized: table
```

```sql
-- models/foo.sql
select 1 id
```

Then lint:

```sh
$ sqlfluff lint models/foo.sql

Traceback (most recent call last):
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/bin/sqlfluff", line 8, in <module>
    sys.exit(cli())
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/cli/commands.py", line 635, in lint
    result = lnt.lint_paths(
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/linter/linter.py", line 1070, in lint_paths
    for i, linted_file in enumerate(runner.run(expanded_paths, fix), start=1):
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/linter/runner.py", line 107, in run
    for fname, partial in self.iter_partials(fnames, fix=fix):
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/linter/runner.py", line 60, in iter_partials
    for fname, rendered in self.iter_rendered(fnames):
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/linter/runner.py", line 47, in iter_rendered
    yield fname, self.linter.render_file(fname, self.config)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/linter/linter.py", line 739, in render_file
    return self.render_string(raw_file, fname, config, encoding)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/linter/linter.py", line 710, in render_string
    templated_file, templater_violations = self.templater.process(
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/templaters/base.py", line 54, in _wrapped
    return func(self, in_str=in_str, fname=fname, config=config, **kwargs)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/templaters/jinja.py", line 410, in process
    env, live_context, render_func = self.construct_render_func(
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/templaters/jinja.py", line 352, in construct_render_func
    live_context = self.get_context(fname=fname, config=config, env=env)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/templaters/jinja.py", line 339, in get_context
    self._extract_macros_from_config(
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/templaters/jinja.py", line 129, in _extract_macros_from_config
    self._extract_macros_from_template(value, env=env, ctx=ctx)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/sqlfluff/core/templaters/jinja.py", line 60, in _extract_macros_from_template
    macro_template = env.from_string(template, globals=ctx)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/jinja2/environment.py", line 1105, in from_string
    return cls.from_code(self, self.compile(source), gs, None)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/jinja2/environment.py", line 768, in compile
    self.handle_exception(source=source_hint)
  File "/Users/jeremy/src/dbt-basic/venv_dbt_1.6.latest/lib/python3.9/site-packages/jinja2/environment.py", line 936, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "<unknown>", line 1, in template
jinja2.exceptions.TemplateSyntaxError: expected token '(', got '.'
```

### Dialect

snowflake

### Version

```
sqlfluff, version 2.3.2
```

### Configuration

```
# .sqlfluff
[sqlfluff]
templater = jinja
dialect = snowflake

[sqlfluff:templater:jinja:macros]
a_macro_def = {% macro pkg.my_macro() %}pass{% endmacro %}
```

### Are you willing to work on and submit a PR to address the issue?

- [ ] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/sqlfluff/sqlfluff/blob/main/CODE_OF_CONDUCT.md)


Discussion:
github-actions[bot]: # Coverage Results ⚠️
```
Name                                    Stmts   Miss  Cover   Missing
---------------------------------------------------------------------
src/sqlfluff/core/templaters/jinja.py     274      2    99%   116, 529
---------------------------------------------------------------------
TOTAL                                   16733      2    99%

218 files skipped due to complete coverage.
Coverage failure: total of 99 is less than fail-under=100

```

coveralls: 
[![Coverage Status](https://coveralls.io/builds/63272410/badge)](https://coveralls.io/builds/63272410)

coverage: 99.989% (-0.01%) from 100.0% when pulling **009f32ccb03fc8905e3f819f2346c103d9bce268 on ac/jinja** into **b36755d2a3ef62261acaed0012b366bffa08cb42 on main**.


Patch:
diff --git a/src/sqlfluff/core/templaters/jinja.py b/src/sqlfluff/core/templaters/jinja.py
index c291c9a22..eb37dd0d4 100644
--- a/src/sqlfluff/core/templaters/jinja.py
+++ b/src/sqlfluff/core/templaters/jinja.py
@@ -20,7 +20,7 @@ from jinja2.ext import Extension
 from jinja2.sandbox import SandboxedEnvironment
 
 from sqlfluff.core.config import FluffConfig
-from sqlfluff.core.errors import SQLBaseError, SQLTemplaterError
+from sqlfluff.core.errors import SQLBaseError, SQLFluffUserError, SQLTemplaterError
 from sqlfluff.core.templaters.base import (
     RawFileSlice,
     TemplatedFile,
@@ -52,12 +52,20 @@ class JinjaTemplater(PythonTemplater):
         """Take a template string and extract any macros from it.
 
         Lovingly inspired by http://codyaray.com/2015/05/auto-load-jinja2-macros
+
+        Raises:
+            TemplateSyntaxError: If the macro we try to load has invalid
+                syntax. We assume that outer functions will catch this
+                exception and handle it appropriately.
         """
         from jinja2.runtime import Macro  # noqa
 
         # Iterate through keys exported from the loaded template string
         context = {}
+        # NOTE: `env.from_string()` will raise TemplateSyntaxError if `template`
+        # is invalid.
         macro_template = env.from_string(template, globals=ctx)
+
         # This is kind of low level and hacky but it works
         try:
             for k in macro_template.module.__dict__:
@@ -147,9 +155,14 @@ class JinjaTemplater(PythonTemplater):
         # Iterate to load macros
         macro_ctx = {}
         for value in loaded_context.values():
-            macro_ctx.update(
-                self._extract_macros_from_template(value, env=env, ctx=ctx)
-            )
+            try:
+                macro_ctx.update(
+                    self._extract_macros_from_template(value, env=env, ctx=ctx)
+                )
+            except TemplateSyntaxError as err:
+                raise SQLFluffUserError(
+                    f"Error loading user provided macro:\n`{value}`\n> {err}."
+                )
         return macro_ctx
 
     def _extract_libraries_from_config(self, config):


Test patch:
diff --git a/test/core/templaters/jinja_test.py b/test/core/templaters/jinja_test.py
index ee3f10c89..fff39c0f4 100644
--- a/test/core/templaters/jinja_test.py
+++ b/test/core/templaters/jinja_test.py
@@ -14,7 +14,7 @@ import pytest
 from jinja2.exceptions import UndefinedError
 
 from sqlfluff.core import FluffConfig, Linter
-from sqlfluff.core.errors import SQLFluffSkipFile, SQLTemplaterError
+from sqlfluff.core.errors import SQLFluffSkipFile, SQLFluffUserError, SQLTemplaterError
 from sqlfluff.core.templaters import JinjaTemplater
 from sqlfluff.core.templaters.base import RawFileSlice, TemplatedFile
 from sqlfluff.core.templaters.jinja import DummyUndefined, JinjaAnalyzer
@@ -621,6 +621,23 @@ def test__templater_jinja_error_macro_path_does_not_exist():
     assert str(e.value).startswith("Path does not exist")
 
 
+def test__templater_jinja_error_macro_invalid():
+    """Tests that an error is raised if a macro is invalid."""
+    invalid_macro_config_string = (
+        "[sqlfluff]\n"
+        "templater = jinja\n"
+        "dialect = ansi\n"
+        "[sqlfluff:templater:jinja:macros]\n"
+        "a_macro_def = {% macro pkg.my_macro() %}pass{% endmacro %}\n"
+    )
+    config = FluffConfig.from_string(invalid_macro_config_string)
+    with pytest.raises(SQLFluffUserError) as e:
+        JinjaTemplater().construct_render_func(config=config)
+    error_string = str(e.value)
+    assert error_string.startswith("Error loading user provided macro")
+    assert "{% macro pkg.my_macro() %}pass{% endmacro %}" in error_string
+
+
 def test__templater_jinja_lint_empty():
     """Check that parsing a file which renders to an empty string.