Repository: aws-cloudformation/cfn-lint

Issue:
LanguageExtensions mixing values from mapping
### CloudFormation Lint Version

cfn-lint 1.35.4

### What operating system are you using?

Mac,Linux

### Describe the bug

When evaluating a CloudFormation template using cfn-lint with the LanguageExtensions transform enabled, we encounter the following error:

```
E3690; Resources.DBCluster.Properties.EngineVersion; '8.0.mysql_aurora.3.07.0' is not one of ['11', '11.21', '11.9', '12', '12.22', '12.9', '13', '13.11', 
'13.12', '13.13', '13.14', '13.15', '13.16', '13.18', '13.20', '13.9', '14', '14.10', '14.11', '14.12', '14.13', '14.15', '14.17', '14.6', '14.8', '14.9', 
'15', '15.10', '15.12', '15.3', '15.4', '15.5', '15.6', '15.7', '15.8', '16', '16.1', '16.2', '16.3', '16.4', '16.4-limitless', '16.6', '16.6-limitless', 
'16.8', '16.8-limitless', '17', '17.4']
````

It seems that in the evaluation it is picking the mysql_aurora option as the EngineVersion, but it is comparing it to the versions available for the aurora-postgresql Engine. The error occurs only when the EngineMap includes both aurora-mysql and aurora-postgresql options, and the LanguageExtensions transform is enabled. When removing one of the options from the map or disabling the LanguageExtensions transform, the template passes validation correctly.

```yaml
        EngineMap:
          8.0.mysql-aurora.3.07.0:
            Engine: 'aurora-mysql'
            EngineVersion: '8.0.mysql_aurora.3.07.0'
          aurora-postgresql-15.10:
            Engine: 'aurora-postgresql'
            EngineVersion: '15.10'
```

### Expected behavior

Given the Engine aurora-mysql value, 8.0.mysql_aurora.3.07.0 should be a valid value for the EngineVersion.

```yaml
      Engine: !FindInMap [ EngineMap, !Ref Engine, Engine ]
      EngineVersion: !FindInMap [ EngineMap, !Ref Engine, EngineVersion ]
```

### Reproduction template

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: test-db.yml

Parameters:
  Engine:
    Description: 'DB Engine version.'
    Type: String
    Default: '8.0.mysql-aurora.3.07.0'
    AllowedValues:
      - "8.0.mysql-aurora.3.07.0"
      - "aurora-postgresql-15.10"

Mappings:
  EngineMap:
    8.0.mysql-aurora.3.07.0:
      Engine: 'aurora-mysql'
      EngineVersion: '8.0.mysql_aurora.3.07.0'
    aurora-postgresql-15.10:
      Engine: 'aurora-postgresql'
      EngineVersion: '15.10'

Transform: AWS::LanguageExtensions

Resources:
  DBCluster:
    Type: "AWS::RDS::DBCluster"
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      DBClusterIdentifier: database
      DeletionProtection : true
      Engine: !FindInMap [ EngineMap, !Ref Engine, Engine ]
      DatabaseName: "master_test_dbname"
      EngineVersion: !FindInMap [ EngineMap, !Ref Engine, EngineVersion ]
      MasterUsername: "master_test_user"
      MasterUserPassword: '{{resolve:secretsmanager:test-secret:SecretString:database_master_password}}'
```


Discussion:
ACK.  I found the issue and working on a fix.  I think this is pointing to needing some bigger re-work of the language extension stuff.  There are a lot caveats and use cases people have found that make things trickier.  This one is a result of covering a different use case so making those checks more valid.



Patch:
diff --git a/src/cfnlint/template/transforms/_language_extensions.py b/src/cfnlint/template/transforms/_language_extensions.py
index 8e2f32b383..607d939cc7 100644
--- a/src/cfnlint/template/transforms/_language_extensions.py
+++ b/src/cfnlint/template/transforms/_language_extensions.py
@@ -368,14 +368,14 @@ def value(
                 t_map[1].value(cfn, params, only_params)
             except _ResolveError:
                 try:
-                    t_map[2].value(cfn, params)
+                    t_map_2_value = t_map[2].value(cfn, params, only_params)
                     max_length = -1
                     for k, v in mapping.items():
                         if isinstance(v, dict):
-                            if t_map[2].value(cfn, params, only_params) in v:
+                            if t_map_2_value in v:
                                 if (
-                                    len(v[t_map[2].value(cfn, params, only_params)])
-                                    <= max_length
+                                    isinstance(v[t_map_2_value], list)
+                                    and len(v[t_map_2_value]) <= max_length
                                 ):
                                     continue
                                 if isinstance(t_map[1], _ForEachValueRef):
@@ -383,9 +383,8 @@ def value(
                                         global _ACCOUNT_ID
                                         _ACCOUNT_ID = k
                                 t_map[1] = _ForEachValue.create(k)
-                                max_length = len(
-                                    v[t_map[2].value(cfn, params, only_params)]
-                                )
+                                if isinstance(v[t_map_2_value], list):
+                                    max_length = len(v[t_map_2_value])
                 except _ResolveError:
                     pass
 
@@ -444,7 +443,7 @@ class _ForEachValueRef(_ForEachValue):
     def __init__(self, _hash: str, obj: Any) -> None:
         super().__init__(_hash)
         if not isinstance(obj, (str, dict)):
-            raise _TypeError("Fn::FindInMap should be a list", obj)
+            raise _TypeError("Fn::Ref should be a list", obj)
 
         self._ref = _ForEachValue.create(obj)
         self._obj = obj


Test patch:
diff --git a/test/unit/module/template/transforms/test_language_extensions.py b/test/unit/module/template/transforms/test_language_extensions.py
index 5235ae1924..20190b8bcd 100644
--- a/test/unit/module/template/transforms/test_language_extensions.py
+++ b/test/unit/module/template/transforms/test_language_extensions.py
@@ -1222,3 +1222,86 @@ def test_transform(self):
                 self.result,
                 template,
             )
+
+
+class TestTransformValueIsStringInMap(TestCase):
+    def setUp(self) -> None:
+        self.template_obj = convert_dict(
+            {
+                "Transform": ["AWS::LanguageExtensions"],
+                "Mappings": {
+                    "EngineMap": {
+                        "8.0.mysql-aurora.3.07.0": {
+                            "Engine": "aurora-mysql",
+                            "EngineVersion": "8.0.mysql_aurora.3.07.0",
+                        },
+                        "aurora-postgresql-15.10": {
+                            "Engine": "aurora-postgresql",
+                            "EngineVersion": "15.10",
+                        },
+                    }
+                },
+                "Resources": {
+                    "DBCluster": {
+                        "Type": "AWS::RDS::DBCluster",
+                        "Properties": {
+                            "Engine": {
+                                "Fn::FindInMap": [
+                                    "EngineMap",
+                                    {"Ref": "Engine"},
+                                    "Engine",
+                                ]
+                            },
+                            "EngineVersion": {
+                                "Fn::FindInMap": [
+                                    "EngineMap",
+                                    {"Ref": "Engine"},
+                                    "EngineVersion",
+                                ]
+                            },
+                        },
+                    }
+                },
+            }
+        )
+
+        self.result = {
+            "Mappings": {
+                "EngineMap": {
+                    "8.0.mysql-aurora.3.07.0": {
+                        "Engine": "aurora-mysql",
+                        "EngineVersion": "8.0.mysql_aurora.3.07.0",
+                    },
+                    "aurora-postgresql-15.10": {
+                        "Engine": "aurora-postgresql",
+                        "EngineVersion": "15.10",
+                    },
+                }
+            },
+            "Resources": {
+                "DBCluster": {
+                    "Type": "AWS::RDS::DBCluster",
+                    "Properties": {
+                        "Engine": "aurora-postgresql",
+                        "EngineVersion": "15.10",
+                    },
+                }
+            },
+            "Transform": ["AWS::LanguageExtensions"],
+        }
+
+    def test_transform(self):
+        self.maxDiff = None
+        with mock.patch(
+            "cfnlint.template.transforms._language_extensions._ACCOUNT_ID", None
+        ):
+            cfn = Template(
+                filename="", template=self.template_obj, regions=["us-east-1"]
+            )
+            matches, template = language_extension(cfn)
+            self.assertListEqual(matches, [])
+            self.assertDictEqual(
+                template,
+                self.result,
+                template,
+            )