• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 21737074253

05 Feb 2026 11:58PM UTC coverage: 86.883% (+0.01%) from 86.873%
21737074253

push

github

web-flow
CFn: implement DeletionPolicy and UpdateReplacePolicy (#13535)

40 of 40 new or added lines in 2 files covered. (100.0%)

8 existing lines in 2 files now uncovered.

69970 of 80534 relevant lines covered (86.88%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.47
/localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_transform.py
1
import copy
1✔
2
import json
1✔
3
import logging
1✔
4
import os
1✔
5
import re
1✔
6
from typing import Any, Final, TypedDict
1✔
7

8
import boto3
1✔
9
import jsonpath_ng
1✔
10
from botocore.exceptions import ClientError, ParamValidationError
1✔
11
from samtranslator.translator.transform import transform as transform_sam
1✔
12

13
from localstack.aws.connect import connect_to
1✔
14
from localstack.services.cloudformation.engine.parameters import StackParameter
1✔
15
from localstack.services.cloudformation.engine.policy_loader import create_policy_loader
1✔
16
from localstack.services.cloudformation.engine.template_preparer import parse_template
1✔
17
from localstack.services.cloudformation.engine.transformers import (
1✔
18
    FailedTransformationException,
19
    ResolveRefsRecursivelyContext,
20
    apply_language_extensions_transform,
21
)
22
from localstack.services.cloudformation.engine.v2.change_set_model import (
1✔
23
    ChangeType,
24
    FnTransform,
25
    Maybe,
26
    NodeForEach,
27
    NodeGlobalTransform,
28
    NodeIntrinsicFunction,
29
    NodeIntrinsicFunctionFnTransform,
30
    NodeProperties,
31
    NodeProperty,
32
    NodeResource,
33
    NodeResources,
34
    NodeTransform,
35
    Nothing,
36
    Scope,
37
    is_nothing,
38
)
39
from localstack.services.cloudformation.engine.v2.change_set_model_preproc import (
1✔
40
    ChangeSetModelPreproc,
41
    PreprocEntityDelta,
42
    PreprocProperties,
43
)
44
from localstack.services.cloudformation.engine.validations import ValidationError
1✔
45
from localstack.services.cloudformation.stores import get_cloudformation_store
1✔
46
from localstack.services.cloudformation.v2.entities import ChangeSet
1✔
47
from localstack.services.cloudformation.v2.types import EngineParameter, engine_parameter_value
1✔
48
from localstack.utils import testutil
1✔
49
from localstack.utils.strings import long_uid
1✔
50

51
LOG = logging.getLogger(__name__)
1✔
52

53
SERVERLESS_TRANSFORM = "AWS::Serverless-2016-10-31"
1✔
54
EXTENSIONS_TRANSFORM = "AWS::LanguageExtensions"
1✔
55
SECRETSMANAGER_TRANSFORM = "AWS::SecretsManager-2020-07-23"
1✔
56
INCLUDE_TRANSFORM = "AWS::Include"
1✔
57

58
_SCOPE_TRANSFORM_TEMPLATE_OUTCOME: Final[Scope] = Scope("TRANSFORM_TEMPLATE_OUTCOME")
1✔
59

60

61
def engine_parameters_to_stack_parameters(
1✔
62
    engine_parameters: dict[str, EngineParameter],
63
) -> dict[str, StackParameter]:
64
    out = {}
1✔
65
    for name, engine_param in engine_parameters.items():
1✔
66
        out[name] = StackParameter(
1✔
67
            ParameterKey=name,
68
            ParameterValue=engine_parameter_value(engine_param),
69
            ResolvedValue=engine_param.get("resolved_value"),
70
            ParameterType=engine_param["type_"],
71
        )
72
    return out
1✔
73

74

75
# TODO: evaluate the use of subtypes to represent and validate types of transforms
76
class GlobalTransform:
1✔
77
    name: str
1✔
78
    parameters: Maybe[dict]
1✔
79

80
    def __init__(self, name: str, parameters: Maybe[dict]):
1✔
81
        self.name = name
1✔
82
        self.parameters = parameters
1✔
83

84

85
class TransformPreprocParameter(TypedDict):
1✔
86
    # TODO: expand
87
    ParameterKey: str
1✔
88
    ParameterValue: Any
1✔
89
    ParameterType: str | None
1✔
90

91

92
class ChangeSetModelTransform(ChangeSetModelPreproc):
1✔
93
    _before_parameters: Final[dict[str, EngineParameter] | None]
1✔
94
    _after_parameters: Final[dict[str, EngineParameter] | None]
1✔
95
    _before_template: Final[Maybe[dict]]
1✔
96
    _after_template: Final[Maybe[dict]]
1✔
97

98
    def __init__(
1✔
99
        self,
100
        change_set: ChangeSet,
101
        before_parameters: dict,
102
        after_parameters: dict,
103
        before_template: dict | None,
104
        after_template: dict | None,
105
    ):
106
        super().__init__(change_set=change_set)
1✔
107
        self._before_parameters = before_parameters
1✔
108
        self._after_parameters = after_parameters
1✔
109
        self._before_template = before_template or Nothing
1✔
110
        self._after_template = after_template or Nothing
1✔
111

112
    def transform(self) -> tuple[dict, dict]:
1✔
113
        self._setup_runtime_cache()
1✔
114
        self._execute_local_transforms()
1✔
115
        transformed_before_template, transformed_after_template = self._execute_global_transforms()
1✔
116
        self._save_runtime_cache()
1✔
117

118
        return transformed_before_template, transformed_after_template
1✔
119

120
    # Ported from v1:
121
    @staticmethod
1✔
122
    def _apply_global_serverless_transformation(
1✔
123
        region_name: str, template: dict, parameters: dict
124
    ) -> dict:
125
        """only returns string when parsing SAM template, otherwise None"""
126
        # TODO: we might also want to override the access key ID to account ID
127
        region_before = os.environ.get("AWS_DEFAULT_REGION")
1✔
128
        if boto3.session.Session().region_name is None:
1✔
129
            os.environ["AWS_DEFAULT_REGION"] = region_name
1✔
130
        loader = create_policy_loader()
1✔
131
        # The following transformation function can carry out in-place changes ensure this cannot occur.
132
        template = copy.deepcopy(template)
1✔
133
        parameters = copy.deepcopy(parameters)
1✔
134
        try:
1✔
135
            transformed = transform_sam(template, parameters, loader)
1✔
136
            return transformed
1✔
137
        except Exception as e:
×
138
            raise FailedTransformationException(transformation=SERVERLESS_TRANSFORM, message=str(e))
×
139
        finally:
140
            # Note: we need to fix boto3 region, otherwise AWS SAM transformer fails
141
            os.environ.pop("AWS_DEFAULT_REGION", None)
1✔
142
            if region_before is not None:
1✔
143
                os.environ["AWS_DEFAULT_REGION"] = region_before
×
144

145
    def _compute_include_transform(self, parameters: dict, fragment: dict) -> dict:
1✔
146
        location = parameters.get("Location")
1✔
147
        if not location or not location.startswith("s3://"):
1✔
148
            raise FailedTransformationException(
×
149
                transformation=INCLUDE_TRANSFORM,
150
                message=f"Unexpected Location parameter for AWS::Include transformer: {location}",
151
            )
152

153
        s3_client = connect_to(
1✔
154
            aws_access_key_id=self._change_set.account_id, region_name=self._change_set.region_name
155
        ).s3
156
        bucket, _, path = location.removeprefix("s3://").partition("/")
1✔
157
        try:
1✔
158
            content = testutil.download_s3_object(s3_client, bucket, path)
1✔
159
        except ClientError:
1✔
160
            raise FailedTransformationException(
1✔
161
                transformation=INCLUDE_TRANSFORM,
162
                message=f"Error downloading S3 object '{bucket}/{path}'",
163
            )
164
        try:
1✔
165
            template_to_include = parse_template(content)
1✔
166
        except Exception as e:
×
167
            raise FailedTransformationException(transformation=INCLUDE_TRANSFORM, message=str(e))
×
168

169
        return {**fragment, **template_to_include}
1✔
170

171
    def _apply_global_transform(
1✔
172
        self,
173
        global_transform: GlobalTransform,
174
        template: dict,
175
        parameters: dict[str, EngineParameter],
176
    ) -> dict:
177
        transform_name = global_transform.name
1✔
178
        if transform_name == EXTENSIONS_TRANSFORM:
1✔
179
            resources = template["Resources"]
1✔
180
            mappings = template.get("Mappings", {})
1✔
181
            conditions = template.get("Conditions", {})
1✔
182

183
            resolve_context = ResolveRefsRecursivelyContext(
1✔
184
                self._change_set.account_id,
185
                self._change_set.region_name,
186
                self._change_set.stack.stack_name,
187
                resources,
188
                mappings,
189
                conditions,
190
                parameters=engine_parameters_to_stack_parameters(parameters),
191
            )
192
            transformed_template = apply_language_extensions_transform(template, resolve_context)
1✔
193
        elif transform_name == SERVERLESS_TRANSFORM:
1✔
194
            # serverless transform just requires the key/value pairs
195
            serverless_parameters = {}
1✔
196
            for name, param in parameters.items():
1✔
197
                serverless_parameters[name] = param.get("resolved_value") or engine_parameter_value(
1✔
198
                    param
199
                )
200
            transformed_template = self._apply_global_serverless_transformation(
1✔
201
                region_name=self._change_set.region_name,
202
                template=template,
203
                parameters=serverless_parameters,
204
            )
205
        elif transform_name == SECRETSMANAGER_TRANSFORM:
1✔
206
            # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-secretsmanager.html
207
            LOG.warning("%s is not yet supported. Ignoring.", SECRETSMANAGER_TRANSFORM)
×
208
            transformed_template = template
×
209
        elif transform_name == INCLUDE_TRANSFORM:
1✔
210
            transformed_template = self._compute_include_transform(
1✔
211
                parameters=global_transform.parameters,
212
                fragment=template,
213
            )
214
        else:
215
            transformed_template = self._invoke_macro(
1✔
216
                name=global_transform.name,
217
                parameters=global_transform.parameters
218
                if not is_nothing(global_transform.parameters)
219
                else {},
220
                fragment=template,
221
                allow_string=False,
222
            )
223
        return transformed_template
1✔
224

225
    def _execute_local_transforms(self):
1✔
226
        node_template = self._change_set.update_model.node_template
1✔
227
        self.visit_node_resources(node_template.resources)
1✔
228

229
    def _execute_global_transforms(self) -> tuple[dict, dict]:
1✔
230
        node_template = self._change_set.update_model.node_template
1✔
231

232
        transform_delta: PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]] = (
1✔
233
            self.visit_node_transform(node_template.transform)
234
        )
235
        transform_before: Maybe[list[GlobalTransform]] = transform_delta.before
1✔
236
        transform_after: Maybe[list[GlobalTransform]] = transform_delta.after
1✔
237

238
        transformed_before_template = self._before_template
1✔
239
        if transform_before and not is_nothing(self._before_template):
1✔
240
            if _SCOPE_TRANSFORM_TEMPLATE_OUTCOME in self._before_cache:
1✔
241
                transformed_before_template = self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME]
1✔
242
            else:
243
                for before_global_transform in transform_before:
1✔
244
                    if not is_nothing(before_global_transform.name):
1✔
245
                        transformed_before_template = self._apply_global_transform(
×
246
                            global_transform=before_global_transform,
247
                            parameters=self._before_parameters,
248
                            template=transformed_before_template,
249
                        )
250

251
                # Macro transformations won't remove the transform from the template
252
                if "Transform" in transformed_before_template:
1✔
253
                    transformed_before_template.pop("Transform")
×
254
                self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template
1✔
255

256
        transformed_after_template = self._after_template
1✔
257
        if transform_after and not is_nothing(self._after_template):
1✔
258
            transformed_after_template = self._after_template
1✔
259
            for after_global_transform in transform_after:
1✔
260
                if not is_nothing(after_global_transform.name):
1✔
261
                    transformed_after_template = self._apply_global_transform(
1✔
262
                        global_transform=after_global_transform,
263
                        parameters=self._after_parameters,
264
                        template=transformed_after_template,
265
                    )
266
            # Macro transformations won't remove the transform from the template
267
            if "Transform" in transformed_after_template:
1✔
268
                transformed_after_template.pop("Transform")
1✔
269
            self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_after_template
1✔
270

271
        return transformed_before_template, transformed_after_template
1✔
272

273
    def visit_node_global_transform(
1✔
274
        self, node_global_transform: NodeGlobalTransform
275
    ) -> PreprocEntityDelta[GlobalTransform, GlobalTransform]:
276
        change_type = node_global_transform.change_type
1✔
277

278
        name_delta = self.visit(node_global_transform.name)
1✔
279
        parameters_delta = self.visit(node_global_transform.parameters)
1✔
280

281
        before = Nothing
1✔
282
        if change_type != ChangeType.CREATED:
1✔
283
            before = GlobalTransform(name=name_delta.before, parameters=parameters_delta.before)
1✔
284
        after = Nothing
1✔
285
        if change_type != ChangeType.REMOVED:
1✔
286
            after = GlobalTransform(name=name_delta.after, parameters=parameters_delta.after)
1✔
287
        return PreprocEntityDelta(before=before, after=after)
1✔
288

289
    def visit_node_transform(
1✔
290
        self, node_transform: NodeTransform
291
    ) -> PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]]:
292
        change_type = node_transform.change_type
1✔
293
        before = [] if change_type != ChangeType.CREATED else Nothing
1✔
294
        after = [] if change_type != ChangeType.REMOVED else Nothing
1✔
295
        for change_set_entity in node_transform.global_transforms:
1✔
296
            if not isinstance(change_set_entity.name.value, str):
1✔
297
                raise ValidationError("Key Name of transform definition must be a string.")
1✔
298

299
            delta: PreprocEntityDelta[GlobalTransform, GlobalTransform] = self.visit(
1✔
300
                change_set_entity=change_set_entity
301
            )
302
            delta_before = delta.before
1✔
303
            delta_after = delta.after
1✔
304
            if not is_nothing(before) and not is_nothing(delta_before):
1✔
305
                before.append(delta_before)
1✔
306
            if not is_nothing(after) and not is_nothing(delta_after):
1✔
307
                after.append(delta_after)
1✔
308
        return PreprocEntityDelta(before=before, after=after)
1✔
309

310
    def _compute_fn_transform(
1✔
311
        self, macro_definition: Any, siblings: Any, allow_string: False
312
    ) -> Any:
313
        def _normalize_transform(obj):
1✔
314
            transforms = []
1✔
315

316
            if isinstance(obj, str):
1✔
317
                transforms.append({"Name": obj, "Parameters": {}})
1✔
318

319
            if isinstance(obj, dict):
1✔
320
                transforms.append(obj)
1✔
321

322
            if isinstance(obj, list):
1✔
323
                for v in obj:
1✔
324
                    if isinstance(v, str):
1✔
325
                        transforms.append({"Name": v, "Parameters": {}})
×
326

327
                    if isinstance(v, dict):
1✔
328
                        if not v.get("Parameters"):
1✔
329
                            v["Parameters"] = {}
×
330
                        transforms.append(v)
1✔
331

332
            return transforms
1✔
333

334
        normalized_transforms = _normalize_transform(macro_definition)
1✔
335
        transform_output = copy.deepcopy(siblings)
1✔
336
        for transform in normalized_transforms:
1✔
337
            transform_name = transform["Name"]
1✔
338
            if transform_name == INCLUDE_TRANSFORM:
1✔
339
                transform_output = self._compute_include_transform(
1✔
340
                    parameters=transform["Parameters"], fragment=transform_output
341
                )
342
            else:
343
                transform_output: dict | str = self._invoke_macro(
1✔
344
                    fragment=transform_output,
345
                    name=transform["Name"],
346
                    parameters=transform.get("Parameters", {}),
347
                    allow_string=allow_string,
348
                )
349

350
        if isinstance(transform_output, dict) and FnTransform in transform_output:
1✔
351
            transform_output.pop(FnTransform)
1✔
352

353
        return transform_output
1✔
354

355
    def _replace_at_jsonpath(self, template: dict, path: str, result: Any):
1✔
356
        pattern = jsonpath_ng.parse(path)
1✔
357
        result_template = pattern.update(template, result)
1✔
358

359
        return result_template
1✔
360

361
    def visit_node_for_each(self, node_foreach: NodeForEach) -> PreprocEntityDelta:
1✔
362
        return PreprocEntityDelta()
×
363

364
    def visit_node_intrinsic_function_fn_transform(
1✔
365
        self, node_intrinsic_function: NodeIntrinsicFunctionFnTransform
366
    ) -> PreprocEntityDelta:
367
        arguments_delta = self.visit(node_intrinsic_function.arguments)
1✔
368
        parent_json_path = node_intrinsic_function.scope.parent.jsonpath
1✔
369

370
        # Only when a FnTransform is used as Property value the macro function is allowed to return a str
371
        property_value_regex = r"\.(Properties)"
1✔
372
        allow_string = False
1✔
373
        if re.search(property_value_regex, parent_json_path):
1✔
374
            allow_string = True
1✔
375

376
        if not is_nothing(arguments_delta.before):
1✔
UNCOV
377
            before = self._compute_fn_transform(
×
378
                arguments_delta.before,
379
                node_intrinsic_function.before_siblings,
380
                allow_string=allow_string,
381
            )
UNCOV
382
            updated_before_template = self._replace_at_jsonpath(
×
383
                self._before_template, parent_json_path, before
384
            )
UNCOV
385
            self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_before_template
×
386
        else:
387
            before = Nothing
1✔
388

389
        if not is_nothing(arguments_delta.after):
1✔
390
            after = self._compute_fn_transform(
1✔
391
                arguments_delta.after,
392
                node_intrinsic_function.after_siblings,
393
                allow_string=allow_string,
394
            )
395
            updated_after_template = self._replace_at_jsonpath(
1✔
396
                self._after_template, parent_json_path, after
397
            )
398
            self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_after_template
1✔
399
        else:
UNCOV
400
            after = Nothing
×
401

402
        self._save_runtime_cache()
1✔
403
        return PreprocEntityDelta(before=before, after=after)
1✔
404

405
    def visit_node_properties(
1✔
406
        self, node_properties: NodeProperties
407
    ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
408
        if not is_nothing(node_properties.fn_transform):
1✔
409
            self.visit_node_intrinsic_function_fn_transform(node_properties.fn_transform)
1✔
410

411
        return super().visit_node_properties(node_properties=node_properties)
1✔
412

413
    def visit_node_resource(self, node_resource: NodeResource) -> PreprocEntityDelta:
1✔
414
        if not is_nothing(node_resource.fn_transform):
1✔
415
            self.visit_node_intrinsic_function_fn_transform(
1✔
416
                node_intrinsic_function=node_resource.fn_transform
417
            )
418

419
        try:
1✔
420
            if delta := super().visit_node_resource(node_resource):
1✔
421
                return delta
1✔
422
            return super().visit_node_properties(node_resource.properties)
×
423
        except RuntimeError:
1✔
424
            return super().visit_node_properties(node_resource.properties)
1✔
425

426
    def visit_node_resources(self, node_resources: NodeResources) -> PreprocEntityDelta:
1✔
427
        if not is_nothing(node_resources.fn_transform):
1✔
428
            self.visit_node_intrinsic_function_fn_transform(
1✔
429
                node_intrinsic_function=node_resources.fn_transform
430
            )
431

432
        return super().visit_node_resources(node_resources=node_resources)
1✔
433

434
    def _invoke_macro(self, name: str, parameters: dict, fragment: dict, allow_string=False):
1✔
435
        account_id = self._change_set.account_id
1✔
436
        region_name = self._change_set.region_name
1✔
437
        macro_definition = get_cloudformation_store(
1✔
438
            account_id=account_id, region_name=region_name
439
        ).macros.get(name)
440

441
        if not macro_definition:
1✔
442
            raise FailedTransformationException(name, f"Transformation {name} is not supported.")
×
443

444
        simplified_parameters = {}
1✔
445
        if resolved_parameters := self._change_set.resolved_parameters:
1✔
446
            for key, resolved_parameter in resolved_parameters.items():
1✔
447
                final_value = engine_parameter_value(resolved_parameter)
1✔
448
                simplified_parameters[key] = (
1✔
449
                    final_value.split(",")
450
                    if resolved_parameter["type_"] == "CommaDelimitedList"
451
                    else final_value
452
                )
453

454
        transformation_id = f"{account_id}::{name}"
1✔
455
        event = {
1✔
456
            "region": region_name,
457
            "accountId": account_id,
458
            "fragment": fragment,
459
            "transformId": transformation_id,
460
            "params": parameters,
461
            "requestId": long_uid(),
462
            "templateParameterValues": simplified_parameters,
463
        }
464

465
        client = connect_to(aws_access_key_id=account_id, region_name=region_name).lambda_
1✔
466
        try:
1✔
467
            invocation = client.invoke(
1✔
468
                FunctionName=macro_definition["FunctionName"], Payload=json.dumps(event)
469
            )
470
        except ClientError:
×
471
            LOG.error(
×
472
                "client error executing lambda function '%s' with payload '%s'",
473
                macro_definition["FunctionName"],
474
                json.dumps(event),
475
            )
476
            raise
×
477
        if invocation.get("StatusCode") != 200 or invocation.get("FunctionError") == "Unhandled":
1✔
478
            raise FailedTransformationException(
1✔
479
                transformation=name,
480
                message=f"Received malformed response from transform {transformation_id}. Rollback requested by user.",
481
            )
482
        result = json.loads(invocation["Payload"].read())
1✔
483

484
        if result.get("status") != "success":
1✔
485
            error_message = result.get("errorMessage")
1✔
486
            message = (
1✔
487
                f"Transform {transformation_id} failed with: {error_message}. Rollback requested by user."
488
                if error_message
489
                else f"Transform {transformation_id} failed without an error message.. Rollback requested by user."
490
            )
491
            raise FailedTransformationException(transformation=name, message=message)
1✔
492

493
        if not isinstance(result.get("fragment"), dict) and not allow_string:
1✔
494
            raise FailedTransformationException(
1✔
495
                transformation=name,
496
                message="Template format error: unsupported structure.. Rollback requested by user.",
497
            )
498

499
        return result.get("fragment")
1✔
500

501
    def visit_node_intrinsic_function_fn_get_att(
1✔
502
        self, node_intrinsic_function: NodeIntrinsicFunction
503
    ) -> PreprocEntityDelta:
504
        try:
1✔
505
            return super().visit_node_intrinsic_function_fn_get_att(node_intrinsic_function)
1✔
506
        except RuntimeError:
1✔
507
            return self.visit(node_intrinsic_function.arguments)
1✔
508

509
    def visit_node_intrinsic_function_fn_sub(
1✔
510
        self, node_intrinsic_function: NodeIntrinsicFunction
511
    ) -> PreprocEntityDelta:
512
        try:
1✔
513
            # If an argument is a Parameter it should be resolved, any other case, ignore it
514
            return super().visit_node_intrinsic_function_fn_sub(node_intrinsic_function)
1✔
515
        except RuntimeError:
1✔
516
            return self.visit(node_intrinsic_function.arguments)
1✔
517

518
    def visit_node_intrinsic_function_fn_split(
1✔
519
        self, node_intrinsic_function: NodeIntrinsicFunction
520
    ) -> PreprocEntityDelta:
521
        try:
1✔
522
            # If an argument is a Parameter it should be resolved, any other case, ignore it
523
            return super().visit_node_intrinsic_function_fn_split(node_intrinsic_function)
1✔
524
        except RuntimeError:
1✔
525
            return self.visit(node_intrinsic_function.arguments)
1✔
526

527
    def visit_node_intrinsic_function_fn_select(
1✔
528
        self, node_intrinsic_function: NodeIntrinsicFunction
529
    ) -> PreprocEntityDelta:
530
        try:
1✔
531
            # If an argument is a Parameter it should be resolved, any other case, ignore it
532
            return super().visit_node_intrinsic_function_fn_select(node_intrinsic_function)
1✔
533
        except RuntimeError:
1✔
534
            return self.visit(node_intrinsic_function.arguments)
1✔
535

536
    def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
1✔
537
        try:
1✔
538
            return super().visit_node_property(node_property)
1✔
539
        except ParamValidationError:
1✔
540
            return self.visit(node_property.value)
×
541

542
    # ignore errors from dynamic replacements
543
    def _maybe_perform_dynamic_replacements(self, delta: PreprocEntityDelta) -> PreprocEntityDelta:
1✔
544
        try:
1✔
545
            return super()._maybe_perform_dynamic_replacements(delta)
1✔
546
        except Exception:
×
547
            return delta
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc