Repository: python-jsonschema/check-jsonschema

Issue:
AzurePipelinesDataError on valid expression syntax
Looking to validate my Azure pipelines and have run into a blocker.  I currently have a pipeline in production ( working and running ) with the following syntax

     steps:
          - bash: |         
              pipenv run pip install ansible-lint-junit --upgrade
              pipenv run ansible-lint -c ../.config/ansible-lint.yml -p --nocolor > ansible-lint.txt
              pipenv run ansible-lint-junit ansible-lint.txt -o AnsibleLintReport.xml
            displayName: "Lint"
            workingDirectory: ${{parameters.workdir}}
            env:
              ${{ each pair in parameters.env }}:
                ${{ pair.key }}: ${{ pair.value }}

when running with the pre-commit hook

            - repo: https://github.com/python-jsonschema/check-jsonschema
              rev: 0.18.3
              hooks:
                - id: check-azure-pipelines
                  files: '^.azure/.*\.yml'
it currently raises the error 

      raise AzurePipelinesDataError(
      check_jsonschema.transforms.azure_pipelines.AzurePipelinesDataError: azure-pipelines transform: found non-object data under an expression in an object, expression=${{ pair.key }}


Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 8f1912c..b752e3d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,8 @@ Unreleased
 
 - Update vendored schemas (2022-10-20)
 - Tweak format checker usage to avoid deprecation warning from ``jsonschema``
+- The Azure Pipelines data transform is now more permissive, which should allow
+  it to handle a wider variety of pipelines files (:issue:`162`)
 
 0.18.3
 ------
diff --git a/src/check_jsonschema/transforms/azure_pipelines.py b/src/check_jsonschema/transforms/azure_pipelines.py
index bdfd107..516ae6c 100644
--- a/src/check_jsonschema/transforms/azure_pipelines.py
+++ b/src/check_jsonschema/transforms/azure_pipelines.py
@@ -85,19 +85,39 @@ def traverse_dict(data: dict) -> dict:
         if is_expression(key):
             # WARNING -- correctness unclear
             #
-            # in the case that an expression in a dict does not map to a dict, the
-            # azure-pipelines-language server will drop it from the data:
-            # https://github.com/microsoft/azure-pipelines-language-server/blob/71b20f92874c02dfe82ad2cc2dcc7fa64996be91/language-service/src/parser/yamlParser.ts#L185
+            # "lift" any dict by moving its attributes up into the object being evaluated
+            #
+            # e.g.
+            #    parent:
+            #      ${{ each x in xs }}:
+            #        - k: v-${{ x }}
+            #
+            # becomes
             #
-            # instead, we'll raise an error
+            #    parent:
+            #      - k: v-${{ x }}
             if isinstance(newvalue, dict):
                 for add_k, add_v in newvalue.items():
                     newdata[add_k] = add_v
+            # In all other cases, drop the content from the data. This is based on the
+            # azure-pipelines-language server behavior:
+            # https://github.com/microsoft/azure-pipelines-language-server/blob/71b20f92874c02dfe82ad2cc2dcc7fa64996be91/language-service/src/parser/yamlParser.ts#L185
+            #
+            # earlier versions would raise an error here, but this caused issues with
+            # data in which expressions were mapped to simple strings
+            #
+            # e.g.
+            #
+            #    parent:
+            #      ${{ x }}: ${{ y }}
+            #
+            # which occurs naturally *after* a lifting operation, as in
+            #
+            #    parent:
+            #      ${{ each x, y in attrs }}:
+            #        ${{ x }}: ${{ y }}
             else:
-                raise AzurePipelinesDataError(
-                    "found non-object data under an expression in an object, "
-                    f"expression={key}"
-                )
+                continue
         else:
             newdata[key] = newvalue
     return newdata


Test patch:
diff --git a/tests/example-files/hooks/positive/azure-pipelines/object-defined-by-expression-map.yaml b/tests/example-files/hooks/positive/azure-pipelines/object-defined-by-expression-map.yaml
new file mode 100644
index 0000000..b789a27
--- /dev/null
+++ b/tests/example-files/hooks/positive/azure-pipelines/object-defined-by-expression-map.yaml
@@ -0,0 +1,15 @@
+parameters:
+  - name: env
+    default:
+      - key: FOO
+        value: foo
+      - key: BAR
+        value: bar
+
+jobs:
+  - job: echo-foo-bar
+    steps:
+      - bash: 'echo "$FOO-$BAR"'
+        env:
+          ${{ each pair in parameters.env }}:
+            ${{ pair.key }}: ${{ pair.value }}