Skip to content

Commit cbb41c9

Browse files
authored
Merge pull request #47 from tjprescott/suppressions
Add Supression Mechanism
2 parents 511a159 + def995f commit cbb41c9

25 files changed

+1523
-442
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# @azure-tools/rest-api-diff
22

3+
## 0.2.1 (TBD)
4+
5+
- Added `--suppressions` option to point to a filing containing point suppressions of violations.
6+
37
## 0.2.0 (2025-03-11)
48

59
- Fixed issue where relative references would sometimes be resolved incorrectly. `--lhs-root`

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ references as if it had been generated in the "correct" folder. This allows the
6161
when compiling TypeSpec files. If omitted, the tool will attempt to resolve the references without it.
6262
- `--rhs-root`: The root path for the right-hand side Swagger files. This is used to resolve references
6363
when compiling TypeSpec files. If omitted, the tool will attempt to resolve the references without it.
64+
- `--suppressions`: Path to a YAML file containing suppressions.
6465

6566
### .env File
6667

@@ -141,3 +142,14 @@ rule can make one of three determinations:
141142
- `RuleResult.ContinueProcessing`: the logic of the rule doesn't apply to this diff and thus the tool should continue processing rules.
142143

143144
When processing a diff item against the rules, if `NoViolation` is returned by a rule, it immediately suspends processing additional rules. If `FlaggedViolation` is returned, rules will continue to be processed in case another rule marks it as `NoViolation`. If all rules are run and no determination is made, then the diff is assumed to affect the API surface area and is tracked as an assumed violation. It will be reported by the tool and will appear in a visual diff. When violations are grouped, these violations will appear in the `UNGROUPED` group.
145+
146+
## Suppressions
147+
148+
It is possible that there will be differences between Swaggers that do matter but must be accepted. For this, you can point to a suppression file using the `--suppressions` option. This file should be a YAML file with the following schema:
149+
150+
```yaml
151+
- path: "path/to/target/"
152+
reason: "This is a reason why this path is suppressed."
153+
```
154+
155+
You can target paths directly from the `diff.json` file assuming `--flatten-paths` is used; however, often violations originate in definitions or parameters, and the way `rest-api-diff` expands these references to create the transformation, the result is that these diffs will be multiplied. In this case, you can use the path to the element in question from the Swagger and it will automatically resolve all diffs that might be a result of reference expansion. Because `rest-api-diff` compares these as Swagger, you cannot use a TypeSpec path even if you are targeting TypeSpec. You must use the Swagger path associated with the TypeSpec-generated Swagger.

compute-suppressions.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- path: "definitions/OperationListResult/type"
2+
reason: "Common type"
3+
- path: "definitions/Operation/properties/display/type"
4+
reason: "Common type"

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@azure-tools/typespec-azure-resource-manager": ">=0.44.0, <1.0.0",
3535
"@azure/avocado": "^0.9.1",
3636
"@types/deep-diff": "^1.0.5",
37+
"@types/diff": "^7.0.0",
3738
"@types/node": "^20.14.7",
3839
"@types/yargs": "^17.0.32",
3940
"@typespec/compiler": ">=0.57.0, <1.0.0",
@@ -59,7 +60,8 @@
5960
"arg": "^4.1.3",
6061
"create-require": "^1.1.1",
6162
"deep-diff": "^1.0.2",
62-
"diff": "^4.0.2",
63+
"diff": "^7.0.0",
64+
"diff2html": "^3.4.0",
6365
"dotenv": "^16.4.5",
6466
"make-error": "^1.3.6",
6567
"undici-types": "^5.26.5",

pnpm-lock.yaml

+757-401
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/test-paths.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from collections import Counter
2+
import json
3+
4+
# open output/diff.json
5+
with open('output/diff.json') as f:
6+
diff = json.load(f)
7+
8+
# gather the items into a flat list
9+
items = []
10+
if isinstance(diff, list):
11+
items = diff
12+
else:
13+
for value in diff.values():
14+
items.extend(value["items"])
15+
16+
keys = []
17+
for item in items:
18+
diff_item = item['diff']
19+
kind = diff_item['kind']
20+
if kind == 'A':
21+
index = diff_item['index']
22+
keys.append(f'{diff_item["path"]}/{index}')
23+
else:
24+
keys.append(diff_item['path'])
25+
26+
counter = Counter(keys)
27+
assert all(val == 1 for val in counter.values())
28+
print(f'{len(counter)} unique paths')

src/definitions.ts

+32-25
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
parseReference,
66
ReferenceMetadata,
77
toSorted,
8+
getRegistryName,
89
} from "./util.js";
910
import path from "path";
11+
import { DiffClient } from "./diff-client.js";
1012

1113
/** The registry to look up the name within. */
1214
export enum RegistryKind {
@@ -31,7 +33,7 @@ class CollectionRegistry {
3133
if (subdata !== undefined) {
3234
for (const [name, _] of toSorted(Object.entries(subdata))) {
3335
const resolvedPath = getResolvedPath(filepath).replace(/\\/g, "/");
34-
const pathKey = `${resolvedPath}#/${this.getRegistryName()}/${name}`;
36+
const pathKey = `${resolvedPath}#/${getRegistryName(this.kind)}/${name}`;
3537
// we don't care about unreferenced common-types
3638
if (!resolvedPath.includes("common-types")) {
3739
this.unreferenced.add(pathKey);
@@ -41,19 +43,6 @@ class CollectionRegistry {
4143
}
4244
}
4345

44-
getRegistryName(): string {
45-
switch (this.kind) {
46-
case RegistryKind.Definition:
47-
return "definitions";
48-
case RegistryKind.Parameter:
49-
return "parameters";
50-
case RegistryKind.Response:
51-
return "responses";
52-
case RegistryKind.SecurityDefinition:
53-
return "securityDefinitions";
54-
}
55-
}
56-
5746
/** Add or update an item. */
5847
add(itemPath: string, name: string, value: any) {
5948
const resolvedPath = getResolvedPath(itemPath);
@@ -75,7 +64,7 @@ class CollectionRegistry {
7564
countReference(path: string, name: string, kind: RegistryKind) {
7665
// convert backslashes to forward slashes
7766
path = path.replace(/\\/g, "/");
78-
const pathKey = `${path}#/${this.getRegistryName()}/${name}`;
67+
const pathKey = `${path}#/${getRegistryName(this.kind)}/${name}`;
7968
this.unreferenced.delete(pathKey);
8069
}
8170

@@ -99,10 +88,10 @@ export class DefinitionRegistry {
9988
private providedPaths = new Set<string>();
10089
private referenceStack: string[] = [];
10190
private referenceMap = new Map<string, Set<string>>();
102-
private currentPath: string | undefined;
103-
private args: any;
91+
private currentPath: string[];
92+
private client: DiffClient;
10493

105-
constructor(map: Map<string, OpenAPIV2.Document>, args: any) {
94+
constructor(map: Map<string, OpenAPIV2.Document>, client: DiffClient) {
10695
this.data = {
10796
definitions: new CollectionRegistry(
10897
map,
@@ -128,9 +117,9 @@ export class DefinitionRegistry {
128117
for (const key of map.keys()) {
129118
this.providedPaths.add(path.normalize(key));
130119
}
131-
this.currentPath;
120+
this.currentPath = [];
121+
this.client = client;
132122
this.#gatherDefinitions(map);
133-
this.args = args;
134123
this.#expandReferences();
135124
}
136125

@@ -155,6 +144,10 @@ export class DefinitionRegistry {
155144
for (const [key, value] of toSorted(Object.entries(itemCopy))) {
156145
matchCopy[key] = value;
157146
}
147+
this.client?.suppressions?.propagateSuppression(
148+
refResult,
149+
this.currentPath
150+
);
158151
return this.#expand(matchCopy, refResult.name);
159152
}
160153
} else {
@@ -168,7 +161,9 @@ export class DefinitionRegistry {
168161
this.#expandDerivedClasses(item);
169162
this.#expandAllOf(item);
170163
for (const [propName, propValue] of toSorted(Object.entries(item))) {
164+
this.currentPath.push(propName);
171165
expanded[propName] = this.#expand(propValue);
166+
this.currentPath.pop();
172167
}
173168
return expanded;
174169
}
@@ -225,7 +220,10 @@ export class DefinitionRegistry {
225220
const itemVal = item[key];
226221
switch (key) {
227222
case "required":
228-
base[key] = (baseVal ?? []).concat(itemVal ?? []);
223+
const mergedRequired = new Set(
224+
(baseVal ?? []).concat(itemVal ?? [])
225+
);
226+
base[key] = [...mergedRequired];
229227
break;
230228
case "properties":
231229
base[key] = { ...(baseVal ?? {}), ...(itemVal ?? {}) };
@@ -302,11 +300,12 @@ export class DefinitionRegistry {
302300

303301
#expandReferencesForCollection(collection: CollectionRegistry) {
304302
this.referenceStack = [];
305-
for (const [path, values] of collection.data.entries()) {
306-
this.currentPath = path;
303+
for (const [filepath, values] of collection.data.entries()) {
307304
for (const [key, value] of values.entries()) {
308-
let expanded = this.#expand(value, key, path);
309-
collection.data.get(path)!.set(key, expanded);
305+
this.currentPath.push(key);
306+
let expanded = this.#expand(value, key, filepath);
307+
collection.data.get(filepath)!.set(key, expanded);
308+
this.currentPath.pop();
310309
}
311310
}
312311
// replace $derivedClasses with $anyOf that contains the expansions of the derived classes
@@ -319,10 +318,18 @@ export class DefinitionRegistry {
319318
}
320319

321320
#expandReferences() {
321+
this.currentPath.push("definitions");
322322
this.#expandReferencesForCollection(this.data.definitions);
323+
this.currentPath.pop();
324+
this.currentPath.push("parameters");
323325
this.#expandReferencesForCollection(this.data.parameters);
326+
this.currentPath.pop();
327+
this.currentPath.push("responses");
324328
this.#expandReferencesForCollection(this.data.responses);
329+
this.currentPath.pop();
330+
this.currentPath.push("securityDefinitions");
325331
this.#expandReferencesForCollection(this.data.securityDefinitions);
332+
this.currentPath.pop();
326333
}
327334

328335
/**

0 commit comments

Comments
 (0)