Skip to content

Commit 0bc642d

Browse files
Add check for unflattened models (#41404)
* add support for post-processing checks * add checker for unflattened models * refactor * clean up --------- Co-authored-by: catalinaperalta <caperal@microsoft.com>
1 parent 285582f commit 0bc642d

File tree

5 files changed

+102
-7
lines changed

5 files changed

+102
-7
lines changed

scripts/breaking_changes_checker/_models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,8 @@ class ChangesChecker(Protocol):
4747

4848
def run_check(self, diff: dict, stable_nodes: dict, current_nodes: dict, **kwargs) -> List[BreakingChange]:
4949
...
50+
51+
@runtime_checkable
52+
class PostProcessingChecker(Protocol):
53+
def run_check(self, breaking_changes: List, features_added: List, *, diff: dict, stable_nodes: dict, current_nodes: dict, **kwargs) -> tuple[list, list]:
54+
...

scripts/breaking_changes_checker/breaking_changes_tracker.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from enum import Enum
1111
from typing import Any, Dict, List, Union
1212
from copy import deepcopy
13-
from _models import ChangesChecker, Suppression, RegexSuppression
13+
from _models import ChangesChecker, Suppression, RegexSuppression, PostProcessingChecker
1414

1515

1616
class BreakingChangeType(str, Enum):
@@ -91,8 +91,8 @@ def __init__(self, stable: Dict, current: Dict, package_name: str, **kwargs: Any
9191
self.stable = stable
9292
self.current = current
9393
self.diff = jsondiff.diff(stable, current)
94-
self.features_added = []
95-
self.breaking_changes = []
94+
self.features_added: List = []
95+
self.breaking_changes: List = []
9696
self.package_name = package_name
9797
self._module_name = None
9898
self._class_name = None
@@ -103,13 +103,30 @@ def __init__(self, stable: Dict, current: Dict, package_name: str, **kwargs: Any
103103
for checker in checkers:
104104
if not isinstance(checker, ChangesChecker):
105105
raise TypeError(f"Checker {checker} does not implement ChangesChecker protocol")
106+
post_processing_checkers: List[PostProcessingChecker] = kwargs.get("post_processing_checkers", [])
107+
for checker in post_processing_checkers:
108+
if not isinstance(checker, PostProcessingChecker):
109+
raise TypeError(f"Checker {checker} does not implement PostProcessingChecker protocol")
106110
self.checkers = checkers
111+
self.post_processing_checkers = post_processing_checkers
107112

108113
def run_checks(self) -> None:
109114
self.run_breaking_change_diff_checks()
110115
self.check_parameter_ordering() # not part of diff
111-
self.run_async_cleanup(self.breaking_changes)
116+
self.run_post_processing()
112117

118+
def run_post_processing(self) -> None:
119+
# Remove duplicate reporting of changes that apply to both sync and async package components
120+
self.run_async_cleanup(self.breaking_changes)
121+
# Run user-defined post-processing checks
122+
for checker in self.post_processing_checkers:
123+
bc_list, fa_list = checker.run_check(
124+
self.breaking_changes, self.features_added,
125+
diff=self.diff, stable_nodes=self.stable, current_nodes=self.current
126+
)
127+
self.breaking_changes = bc_list
128+
self.features_added = fa_list
129+
113130
# Remove duplicate reporting of changes that apply to both sync and async package components
114131
def run_async_cleanup(self, changes_list: List) -> None:
115132
# Create a list of all sync changes
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env python
2+
3+
# --------------------------------------------------------------------------------------------
4+
# Copyright (c) Microsoft Corporation. All rights reserved.
5+
# Licensed under the MIT License. See License.txt in the project root for license information.
6+
# --------------------------------------------------------------------------------------------
7+
8+
# --------------------------------------------------------------------------------------------
9+
# This checker is designed to clean up reported changes related to libraries migrating from
10+
# swagger -> typespec where models that where previously flattened are no longer flattened.
11+
# The checker will check for the following:
12+
# 1. Check if a model has a new `properties`` field
13+
# 2. Check if there is a corresponding `<model name>Properties` model
14+
# 3. Remove entries related to the new `properties`` field from the diff
15+
# 4. Remove entries related to the new `<model name>Properties` model from the diff
16+
# 5. Remove entries related losing properties on the original model
17+
# --------------------------------------------------------------------------------------------
18+
19+
import copy
20+
import sys
21+
import os
22+
sys.path.append(os.path.abspath("../../scripts/breaking_changes_checker"))
23+
24+
25+
class UnflattenedModelsChecker:
26+
def run_check(self, breaking_changes: list, features_added: list, *, diff: dict, stable_nodes: dict, current_nodes: dict, **kwargs) -> tuple[list, list]:
27+
properties_model_entry = ()
28+
properties_field_entry = ()
29+
30+
breaking_changes_copy = copy.deepcopy(breaking_changes)
31+
features_added_copy = copy.deepcopy(features_added)
32+
# Look for new "*Properties" models and their corresponding properties field in the original model
33+
for fa in features_added:
34+
_, change_type, module_name, class_name, *args = fa
35+
if change_type == "AddedClass" and class_name.endswith("Properties"):
36+
properties_model_entry = fa
37+
# Check if the corresponding model has a properties field
38+
original_model_name = class_name[:-len("Properties")]
39+
if original_model_name in stable_nodes[module_name]["class_nodes"]:
40+
original_model = stable_nodes[module_name]["class_nodes"][original_model_name]
41+
for fa2 in features_added:
42+
_, change_type2, module_name2, class_name2, *args2 = fa2
43+
# Check if the class name is the original model name
44+
if class_name2 and class_name2 == original_model_name:
45+
if change_type2 == "AddedClassProperty" and "properties" == args2[0]:
46+
properties_field_entry = fa2
47+
break
48+
if not properties_field_entry or not properties_model_entry:
49+
continue
50+
# Check the breaking changes list for entries about removing any of the properties that existed in the original model
51+
for prop in original_model["properties"]:
52+
for breaking_change in breaking_changes_copy:
53+
_, bc_change_type, bc_module_name, bc_class_name, *bc_args = breaking_change
54+
if (bc_change_type == "RemovedOrRenamedInstanceAttribute" and
55+
bc_class_name == original_model_name and
56+
bc_args[0] == prop):
57+
# If we find a breaking change about removing a property, delete it from the breaking changes list
58+
breaking_changes_copy.remove(breaking_change)
59+
break
60+
if properties_model_entry and properties_field_entry:
61+
# If we found both the properties model and the properties field, remove them from the features added list
62+
# since this means the model was unflattened
63+
features_added_copy.remove(properties_model_entry)
64+
features_added_copy.remove(properties_field_entry)
65+
properties_field_entry, properties_model_entry = (), ()
66+
return breaking_changes_copy, features_added_copy

scripts/breaking_changes_checker/detect_breaking_changes.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from breaking_changes_tracker import BreakingChangesTracker
2626
from changelog_tracker import ChangelogTracker
2727
from pathlib import Path
28-
from supported_checkers import CHECKERS
28+
from supported_checkers import CHECKERS, POST_PROCESSING_CHECKERS
2929

3030
root_dir = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", ".."))
3131
_LOGGER = logging.getLogger(__name__)
@@ -457,10 +457,11 @@ def test_compare_reports(pkg_dir: str, changelog: bool, source_report: str = "st
457457
current,
458458
package_name,
459459
checkers = CHECKERS,
460-
ignore = IGNORE_BREAKING_CHANGES
460+
ignore = IGNORE_BREAKING_CHANGES,
461+
post_processing_checkers = POST_PROCESSING_CHECKERS
461462
)
462463
if changelog:
463-
checker = ChangelogTracker(stable, current, package_name, checkers = CHECKERS, ignore = IGNORE_BREAKING_CHANGES)
464+
checker = ChangelogTracker(stable, current, package_name, checkers = CHECKERS, ignore = IGNORE_BREAKING_CHANGES, post_processing_checkers = POST_PROCESSING_CHECKERS)
464465
checker.run_checks()
465466

466467
remove_json_files(pkg_dir)

scripts/breaking_changes_checker/supported_checkers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77

88
from checkers.removed_method_overloads_checker import RemovedMethodOverloadChecker
99
from checkers.added_method_overloads_checker import AddedMethodOverloadChecker
10+
from checkers.unflattened_models_checker import UnflattenedModelsChecker
1011

1112
CHECKERS = [
1213
RemovedMethodOverloadChecker(),
1314
AddedMethodOverloadChecker(),
1415
]
16+
17+
POST_PROCESSING_CHECKERS = [
18+
# Add any post-processing checkers here
19+
UnflattenedModelsChecker(),
20+
]

0 commit comments

Comments
 (0)