Repository: sqlfluff/sqlfluff

Issue:
noqa is ignored for jinja templated lines
## Expected Behaviour
Line with `noqa: TMP` should be ignored (despite of evaluation error)

## Observed Behaviour
trying to lint airflow sql-template for AWS Athena query
setting up inline `-- noqa` or `--noqa: TMP` for jinja templated line not silenting templating error (typecasting error due to unable to pass datetime object while linting into template context):
```
== [transform/airflow/dags/queries/sfmc/player_balance.sql] FAIL
L:   0 | P:   0 |  TMP | Unrecoverable failure in Jinja templating: unsupported operand type(s) for -: 'int' and 'datetime.timedelta'. Have you configured your variables?
                       | https://docs.sqlfluff.com/en/latest/configuration.html
```

## Steps to Reproduce
templated file:
```sql
select *, row_number() over (partition by player_id order by balance_change_date desc)  as rnk
from raw
where
    balance_change_date >= cast(from_iso8601_timestamp('{{ execution_date - macros.timedelta(hours=2, minutes=10) }}') as timestamp)  and  --noqa: TMP
    balance_change_date < cast(from_iso8601_timestamp('{{ next_execution_date - macros.timedelta(minutes=10) }}') as timestamp) --noqa: TMP
```
run:
```bash
sqlfluff lint transform/airflow/dags/queries/sfmc/player_balance.sql
```

## Dialect
postgres (used for AWS Athena)

## Version
datalake % sqlfluff --version
sqlfluff, version 0.8.1
datalake % python3 --version
Python 3.9.8

## Configuration
```ini
# tox.ini
[sqlfluff]
templater = jinja
output_line_length = 180
exclude_rules = L011,L012,L022,L031,L034
dialect = postgres

[sqlfluff:rules]
max_line_length = 120

[sqlfluff:templater:jinja]
library_path = operation/deploy/lint
apply_dbt_builtins = false

[sqlfluff:templater:jinja:context]
ds = 2021-11-11
ds_nodash = 20211111
start_date = 2021-11-11
end_date = 2021-11-11
interval = 1
# passed as int due to inabliity to pass datetime obkject 
data_interval_start = 1636588800
data_interval_end = 1636588800
```

```python
# operation/deploy/lint/macro.py
from datetime import datetime, timedelta  # noqa: F401

import dateutil  # noqa: F401
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff) Report
> Merging [#2509](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff) (8687cb6) into [main](https://codecov.io/gh/sqlfluff/sqlfluff/commit/b0d26663908beecf4205fc182baff888faa8deba?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff) (b0d2666) will **decrease** coverage by `0.02%`.
> The diff coverage is `88.46%`.

[![Impacted file tree graph](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509/graphs/tree.svg?width=650&height=150&src=pr&token=maM8kLgCmn&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff)](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff)

```diff
@@             Coverage Diff             @@
##              main    #2509      +/-   ##
===========================================
- Coverage   100.00%   99.97%   -0.03%     
===========================================
  Files          162      162              
  Lines        11661    11684      +23     
===========================================
+ Hits         11661    11681      +20     
- Misses           0        3       +3     
```


| [Impacted Files](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff) | Coverage Δ | |
|---|---|---|
| [src/sqlfluff/core/linter/linter.py](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff#diff-c3JjL3NxbGZsdWZmL2NvcmUvbGludGVyL2xpbnRlci5weQ==) | `99.21% <87.50%> (-0.79%)` | :arrow_down: |
| [src/sqlfluff/core/linter/common.py](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff#diff-c3JjL3NxbGZsdWZmL2NvcmUvbGludGVyL2NvbW1vbi5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff). Last update [b0d2666...8687cb6](https://codecov.io/gh/sqlfluff/sqlfluff/pull/2509?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sqlfluff).

tunetheweb: Change looks good once you address those last two comments.

I also feel we should merge #2513 first to differentiate the black fix from this PR.
barrywhart: @tunetheweb: BTW, I removed the Black-related changes from this PR. Hope we get that figured out soon, or they release an upstream fix. What a mess...
tunetheweb: > @tunetheweb: BTW, I removed the Black-related changes from this PR. Hope we get that figured out soon, or they release an upstream fix. What a mess...

It's actually not black, it's flake8-black and they've released a fix.

But looks like our dbt tests hang on new black anyway. Not sure what that's about but might leave it for now and try again later next month once things settle down.
barrywhart: Slightly related, some of the challenges with Black make me wonder if we could drop support for older dbt versions (i.e. prior to dbt 1.0.0). IIRC, Alan said there's very little usage now, and supporting multiple versions of a large package like dbt makes **our** version considerations considerably more complex. I'll bring this up at the Tuesday meeting.
tunetheweb: > Slightly related, some of the challenges with Black make me wonder if we could drop support for older dbt versions (i.e. prior to dbt 1.0.0). IIRC, Alan said there's very little usage now, and supporting multiple versions of a large package like dbt makes **our** version considerations considerably more complex. I'll bring this up at the Tuesday meeting.

Maybe open an issue for this too so others can comment too? Would be interesting to see feedback before Tuesday if we get any.

Patch:
diff --git a/src/sqlfluff/core/linter/common.py b/src/sqlfluff/core/linter/common.py
index 9b51d7650..db2c7a38f 100644
--- a/src/sqlfluff/core/linter/common.py
+++ b/src/sqlfluff/core/linter/common.py
@@ -42,6 +42,7 @@ class RenderedFile(NamedTuple):
     time_dict: Dict[str, float]
     fname: str
     encoding: str
+    source_str: str
 
 
 class ParsedString(NamedTuple):
@@ -65,6 +66,7 @@ class ParsedString(NamedTuple):
     templated_file: TemplatedFile
     config: FluffConfig
     fname: str
+    source_str: str
 
 
 class EnrichedFixPatch(NamedTuple):
diff --git a/src/sqlfluff/core/linter/linter.py b/src/sqlfluff/core/linter/linter.py
index 73ec53ebe..f88e3abc1 100644
--- a/src/sqlfluff/core/linter/linter.py
+++ b/src/sqlfluff/core/linter/linter.py
@@ -26,7 +26,7 @@ from sqlfluff.core.errors import (
     SQLParseError,
     SQLTemplaterSkipFile,
 )
-from sqlfluff.core.parser import Lexer, Parser
+from sqlfluff.core.parser import Lexer, Parser, RegexLexer
 from sqlfluff.core.file_helpers import get_encoding
 from sqlfluff.core.templaters import TemplatedFile
 from sqlfluff.core.rules import get_ruleset
@@ -343,7 +343,7 @@ class Linter:
         cls,
         rendered: RenderedFile,
         recurse: bool = True,
-    ):
+    ) -> ParsedString:
         """Parse a rendered file."""
         t0 = time.monotonic()
         violations = cast(List[SQLBaseError], rendered.templater_violations)
@@ -382,6 +382,7 @@ class Linter:
             rendered.templated_file,
             rendered.config,
             rendered.fname,
+            rendered.source_str,
         )
 
     @classmethod
@@ -400,7 +401,7 @@ class Linter:
         return result
 
     @classmethod
-    def extract_ignore_mask(
+    def extract_ignore_mask_tree(
         cls,
         tree: BaseSegment,
         rule_codes: List[str],
@@ -419,6 +420,34 @@ class Linter:
             linter_logger.info("Parsed noqa directives from file: %r", ignore_buff)
         return ignore_buff, violations
 
+    @classmethod
+    def extract_ignore_mask_source(
+        cls,
+        source: str,
+        inline_comment_regex: RegexLexer,
+        rule_codes: List[str],
+    ) -> Tuple[List[NoQaDirective], List[SQLBaseError]]:
+        """Look for inline ignore comments and return NoQaDirectives.
+
+        Very similar to extract_ignore_mask_tree(), but can be run on raw source
+        (i.e. does not require the code to have parsed successfully).
+        """
+        ignore_buff: List[NoQaDirective] = []
+        violations: List[SQLBaseError] = []
+        for idx, line in enumerate(source.split("\n")):
+            match = inline_comment_regex.search(line) if line else None
+            if match:
+                ignore_entry = cls.parse_noqa(
+                    line[match[0] : match[1]], idx + 1, rule_codes
+                )
+                if isinstance(ignore_entry, SQLParseError):
+                    violations.append(ignore_entry)  # pragma: no cover
+                elif ignore_entry:
+                    ignore_buff.append(ignore_entry)
+        if ignore_buff:
+            linter_logger.info("Parsed noqa directives from file: %r", ignore_buff)
+        return ignore_buff, violations
+
     @classmethod
     def lint_fix_parsed(
         cls,
@@ -449,7 +478,7 @@ class Linter:
         # Look for comment segments which might indicate lines to ignore.
         if not config.get("disable_noqa"):
             rule_codes = [r.code for r in rule_set]
-            ignore_buff, ivs = cls.extract_ignore_mask(tree, rule_codes)
+            ignore_buff, ivs = cls.extract_ignore_mask_tree(tree, rule_codes)
             all_linting_errors += ivs
         else:
             ignore_buff = []
@@ -556,6 +585,21 @@ class Linter:
             # If no parsed tree, set to None
             tree = None
             ignore_buff = []
+            if not parsed.config.get("disable_noqa"):
+                # Templating and/or parsing have failed. Look for "noqa"
+                # comments (the normal path for identifying these comments
+                # requires access to the parse tree, and because of the failure,
+                # we don't have a parse tree).
+                ignore_buff, ignore_violations = cls.extract_ignore_mask_source(
+                    parsed.source_str,
+                    [
+                        lm
+                        for lm in parsed.config.get("dialect_obj").lexer_matchers
+                        if lm.name == "inline_comment"
+                    ][0],
+                    [r.code for r in rule_set],
+                )
+                violations += ignore_violations
 
         # We process the ignore config here if appropriate
         for violation in violations:
@@ -649,7 +693,13 @@ class Linter:
         time_dict = {"templating": time.monotonic() - t0}
 
         return RenderedFile(
-            templated_file, templater_violations, config, time_dict, fname, encoding
+            templated_file,
+            templater_violations,
+            config,
+            time_dict,
+            fname,
+            encoding,
+            in_str,
         )
 
     def render_file(self, fname: str, root_config: FluffConfig) -> RenderedFile:
diff --git a/src/sqlfluff/core/templaters/jinja.py b/src/sqlfluff/core/templaters/jinja.py
index 7b0337a61..12acff570 100644
--- a/src/sqlfluff/core/templaters/jinja.py
+++ b/src/sqlfluff/core/templaters/jinja.py
@@ -386,7 +386,13 @@ class JinjaTemplater(PythonTemplater):
                         "Unrecoverable failure in Jinja templating: {}. Have you "
                         "configured your variables? "
                         "https://docs.sqlfluff.com/en/latest/configuration.html"
-                    ).format(err)
+                    ).format(err),
+                    # We don't have actual line number information, but specify
+                    # line 1 so users can ignore with "noqa" if they want. (The
+                    # default is line 0, which can't be ignored because it's not
+                    # a valid line number.)
+                    line_no=1,
+                    line_pos=1,
                 )
             )
             return None, violations


Test patch:
diff --git a/test/core/linter_test.py b/test/core/linter_test.py
index 2a599fd43..efa4b288e 100644
--- a/test/core/linter_test.py
+++ b/test/core/linter_test.py
@@ -708,18 +708,42 @@ def test_linter_noqa_with_templating():
     assert not result.get_violations()
 
 
+def test_linter_noqa_template_errors():
+    """Similar to test_linter_noqa, but uses templating (Jinja)."""
+    lntr = Linter(
+        config=FluffConfig(
+            overrides={
+                "templater": "jinja",
+            }
+        )
+    )
+    sql = """select * --noqa: TMP
+from raw
+where
+    balance_date >= {{ execution_date - macros.timedelta() }}  --noqa: TMP
+"""
+    result = lntr.lint_string(sql)
+    assert not result.get_violations()
+
+
 def test_linter_noqa_prs():
     """Test "noqa" feature to ignore PRS at the higher "Linter" level."""
     lntr = Linter(
         config=FluffConfig(
             overrides={
+                "dialect": "bigquery",
                 "exclude_rules": "L050",
             }
         )
     )
     sql = """
-    SELECT col_a AS a
-    FROM foo;, -- noqa: PRS
+    CREATE TABLE IF NOT EXISTS
+    Test.events (userID STRING,
+    eventName STRING,
+    eventID INTEGER,
+    device STRUCT < mobileBrandName STRING, -- noqa: PRS
+    mobileModelName STRING>);
+    Insert into Test.events VALUES ("1","abc",123,STRUCT("htc","10"));
         """
     result = lntr.lint_string(sql)
     violations = result.get_violations()