Skip to content

Commit 6290b53

Browse files
authored
Merge pull request #90 from apiaddicts/develop
Develop
2 parents b4f05c9 + cf6fe52 commit 6290b53

9 files changed

Lines changed: 241 additions & 32 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.3.2] - 2026-03-05
9+
10+
### Fixed
11+
- OAR031 - Examples
12+
813
## [1.3.1] - 2026-02-19
914

1015
### Changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>org.apiaddicts.apitools.dosonarapi</groupId>
55
<artifactId>sonaropenapi-rules-community</artifactId>
6-
<version>1.3.1</version>
6+
<version>1.3.2</version>
77
<packaging>sonar-plugin</packaging>
88

99
<name>SonarQube OpenAPI Community Rules</name>

src/main/java/apiaddicts/sonar/openapi/checks/examples/OAR031ExamplesCheck.java

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,60 @@ public class OAR031ExamplesCheck extends BaseCheck {
2121

2222
private static final String EXAMPLE = "example";
2323
private static final String EXAMPLES = "examples";
24+
private static final String SCHEMA = "schema";
25+
private static final String PROPERTIES = "properties";
26+
private static final String ITEMS = "items";
27+
28+
private static final String ERROR_RESPONSE = "OAR031.error-response";
2429

2530
private final ExternalRefHandler handleExternalRef = new ExternalRefHandler();
2631

2732
@Override
2833
public Set<AstNodeType> subscribedKinds() {
29-
return ImmutableSet.of(OpenApi2Grammar.SCHEMA, OpenApi2Grammar.RESPONSES, OpenApi3Grammar.SCHEMA, OpenApi3Grammar.RESPONSES, OpenApi31Grammar.SCHEMA, OpenApi31Grammar.RESPONSES, OpenApi3Grammar.REQUEST_BODY, OpenApi31Grammar.REQUEST_BODY, OpenApi2Grammar.PATH, OpenApi3Grammar.PATH, OpenApi31Grammar.PATH);
34+
return ImmutableSet.of(
35+
OpenApi2Grammar.SCHEMA, OpenApi2Grammar.RESPONSES, OpenApi2Grammar.PARAMETER,
36+
OpenApi3Grammar.SCHEMA, OpenApi3Grammar.RESPONSES, OpenApi3Grammar.PARAMETER,
37+
OpenApi31Grammar.SCHEMA, OpenApi31Grammar.RESPONSES, OpenApi31Grammar.PARAMETER,
38+
OpenApi3Grammar.REQUEST_BODY, OpenApi31Grammar.REQUEST_BODY,
39+
OpenApi2Grammar.PATH, OpenApi3Grammar.PATH, OpenApi31Grammar.PATH
40+
);
3041
}
3142

3243
@Override
3344
public void visitNode(JsonNode node) {
34-
if (OpenApi2Grammar.PATH.equals(node.getType()) || OpenApi3Grammar.PATH.equals(node.getType()) || OpenApi31Grammar.PATH.equals(node.getType())) {
45+
AstNodeType type = node.getType();
46+
if (OpenApi2Grammar.PATH.equals(type) || OpenApi3Grammar.PATH.equals(type) || OpenApi31Grammar.PATH.equals(type)) {
3547
visitPathNode(node);
36-
} else if (node.getType().equals(OpenApi2Grammar.RESPONSES) || node.getType().equals(OpenApi2Grammar.SCHEMA)) {
48+
} else if (OpenApi2Grammar.PARAMETER.equals(type) || OpenApi3Grammar.PARAMETER.equals(type) || OpenApi31Grammar.PARAMETER.equals(type)) {
49+
visitParameterNode(node);
50+
} else if (type.equals(OpenApi2Grammar.RESPONSES) || type.equals(OpenApi2Grammar.SCHEMA)) {
3751
visitV2Node(node);
3852
} else {
3953
visitV3Node(node);
4054
}
4155
}
4256

57+
private void visitParameterNode(JsonNode node) {
58+
handleExternalRef.resolve(node, resolved -> {
59+
if (OpenApi2Grammar.PARAMETER.equals(resolved.getType())) {
60+
JsonNode inNode = resolved.get("in");
61+
if (!inNode.isMissing() && !"body".equals(inNode.getTokenValue())) {
62+
return;
63+
}
64+
}
65+
66+
JsonNode schema = resolved.get(SCHEMA);
67+
68+
boolean hasExample = !resolved.get(EXAMPLE).isMissing()
69+
|| !resolved.get(EXAMPLES).isMissing()
70+
|| (!schema.isMissing() && isSchemaCovered(schema));
71+
72+
if (!hasExample) {
73+
addIssue(KEY, translate("OAR031.error-parameter"), handleExternalRef.getTrueNode(node));
74+
}
75+
});
76+
}
77+
4378
private void visitV2Node(JsonNode node) {
4479
AstNodeType type = node.getType();
4580
if (OpenApi2Grammar.RESPONSES.equals(type)) {
@@ -50,9 +85,15 @@ private void visitV2Node(JsonNode node) {
5085
}
5186

5287
private void visitResponseV2Node(JsonNode node) {
53-
if (node.get(EXAMPLES).isMissing()) {
54-
addIssue(KEY, translate("OAR031.error-response"), node.key());
55-
}
88+
handleExternalRef.resolve(node, resolved -> {
89+
JsonNode schemaNode = resolved.get(SCHEMA);
90+
boolean hasExample = !resolved.get(EXAMPLES).isMissing()
91+
|| (!schemaNode.isMissing() && isSchemaCovered(schemaNode));
92+
93+
if (!hasExample) {
94+
addIssue(KEY, translate(ERROR_RESPONSE), handleExternalRef.getTrueNode(node.key()));
95+
}
96+
});
5697
}
5798

5899
private void visitV3Node(JsonNode node) {
@@ -75,35 +116,58 @@ private void processResponses(JsonNode node, java.util.function.Consumer<JsonNod
75116

76117
private void visitRequestBodyOrResponseV3Node(JsonNode node) {
77118
JsonNode content = node.at("/content");
119+
78120
if (content.isMissing()) {
121+
String errorKey = node.getType().equals(OpenApi3Grammar.REQUEST_BODY) ? "OAR031.error-request" : ERROR_RESPONSE;
122+
addIssue(KEY, translate(errorKey), handleExternalRef.getTrueNode(node.key()));
79123
return;
80124
}
125+
81126
for (JsonNode mediaTypeNode : content.propertyMap().values()) {
82-
AstNodeType type = node.getType();
83-
JsonNode schemaNode = mediaTypeNode.get("schema");
84-
boolean hasSchemaExample = false;
85-
if (schemaNode.getType().equals(OpenApi3Grammar.SCHEMA) && !schemaNode.get(EXAMPLE).isMissing()) {
86-
hasSchemaExample = true;
87-
}
88-
if (!hasSchemaExample && mediaTypeNode.get(EXAMPLES).isMissing() && mediaTypeNode.get(EXAMPLE).isMissing()) {
89-
if (type.equals(OpenApi3Grammar.REQUEST_BODY)) {
90-
addIssue(KEY, translate("OAR031.error-request"), handleExternalRef.getTrueNode(node.key()));
91-
} else {
92-
addIssue(KEY, translate("OAR031.error-response"), handleExternalRef.getTrueNode(node.key()));
93-
}
127+
JsonNode schemaNode = mediaTypeNode.get(SCHEMA);
128+
boolean hasExplicitExample = !mediaTypeNode.get(EXAMPLES).isMissing()
129+
|| !mediaTypeNode.get(EXAMPLE).isMissing();
130+
131+
if (!hasExplicitExample && !isSchemaCovered(schemaNode)) {
132+
String errorKey = node.getType().equals(OpenApi3Grammar.REQUEST_BODY) ? "OAR031.error-request" : ERROR_RESPONSE;
133+
addIssue(KEY, translate(errorKey), handleExternalRef.getTrueNode(node.key()));
94134
}
95135
}
96136
}
97137

138+
private boolean isSchemaCovered(JsonNode schemaNode) {
139+
if (schemaNode.isMissing()) return false;
140+
141+
return handleExternalRef.resolve(schemaNode, resolved -> {
142+
if (!resolved.get(EXAMPLE).isMissing() || !resolved.get(EXAMPLES).isMissing()) {
143+
return true;
144+
}
145+
146+
JsonNode props = resolved.get(PROPERTIES);
147+
if (!props.isMissing() && props.isObject()) {
148+
return props.propertyMap().values().stream().anyMatch(this::isSchemaCovered);
149+
}
150+
151+
JsonNode items = resolved.get(ITEMS);
152+
if (!items.isMissing()) {
153+
return isSchemaCovered(items);
154+
}
155+
156+
return false;
157+
});
158+
}
159+
98160
private void visitSchemaNode(JsonNode node) {
99161
JsonNode parentNode = (JsonNode) node.getParent().getParent();
100162

101163
if (parentNode.getType().equals(OpenApi3Grammar.PARAMETER)) {
102-
if (node.get(EXAMPLE).isMissing() && parentNode.get(EXAMPLE).isMissing() && parentNode.get(EXAMPLES).isMissing()) {
103-
addIssue(KEY, translate("OAR031.error-parameter"), parentNode);
104-
}
105-
} else if (parentNode.getType().equals(OpenApi3Grammar.SCHEMA_PROPERTIES)
106-
|| parentNode.getType().toString().equals("BLOCK_MAPPING") || parentNode.getType().toString().equals("FLOW_MAPPING")) {
164+
return;
165+
}
166+
167+
if (parentNode.getType().equals(OpenApi3Grammar.SCHEMA_PROPERTIES)
168+
|| parentNode.getType().toString().equals("BLOCK_MAPPING")
169+
|| parentNode.getType().toString().equals("FLOW_MAPPING")) {
170+
107171
JsonNode schemaParent = (JsonNode) parentNode.getParent().getParent();
108172
if (schemaParent != null && !schemaParent.get("allOf").isMissing()) {
109173
return;
@@ -140,11 +204,11 @@ private void visitPathNode(JsonNode node) {
140204
}
141205

142206
private void visitSchemaNode2(JsonNode responseNode) {
143-
JsonNode schemaNode = responseNode.value().get("schema");
207+
JsonNode schemaNode = responseNode.value().get(SCHEMA);
144208
if (schemaNode.isMissing()) return;
145209

146210
handleExternalRef.resolve(schemaNode, resolvedSchema -> {
147-
JsonNode props = resolvedSchema.get("properties");
211+
JsonNode props = resolvedSchema.get(PROPERTIES);
148212
if (props.isMissing() || !props.isObject()) return;
149213

150214
props.propertyMap().forEach((key, propertyNode) -> {
@@ -154,4 +218,4 @@ private void visitSchemaNode2(JsonNode responseNode) {
154218
});
155219
});
156220
}
157-
}
221+
}

src/test/java/apiaddicts/sonar/openapi/checks/examples/OAR031ExamplesCheckTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ public void verifyInV2WithoutExamples() {
2626
verifyV2("without-examples.yaml");
2727
}
2828

29+
@Test
30+
public void verifyvalidV2ExternalRefs() {
31+
verifyV2("externalref.yaml");
32+
}
33+
34+
@Test
35+
public void verifyInV2NestedProperties() {
36+
verifyV2("nested-properties-examples.yaml");
37+
}
38+
2939
@Test
3040
public void verifyInV3() {
3141
verifyV3("valid.yaml");
@@ -41,6 +51,11 @@ public void verifyvalidV3ExternalRefs() {
4151
verifyV3("externalref.yaml");
4252
}
4353

54+
@Test
55+
public void verifyInV3NestedProperties() {
56+
verifyV3("nested-properties-examples.yaml");
57+
}
58+
4459
@Override
4560
public void verifyRule() {
4661
assertRuleProperties("OAR031 - Examples - Responses, Request Body, Parameters and Properties must have an example defined", RuleType.BUG, Severity.MAJOR, tags("examples"));
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
swagger: "2.0"
2+
info:
3+
version: "1.0.0"
4+
title: Example API
5+
host: api.example.com
6+
basePath: /v1
7+
schemes:
8+
- https
9+
paths:
10+
/users:
11+
get:
12+
responses:
13+
200:
14+
description: OK
15+
schema:
16+
type: array
17+
items:
18+
$ref: '#/definitions/User'
19+
400:
20+
$ref: >- # Noncompliant {{OAR031: Responses must have one or more examples defined}}
21+
https://raw.githubusercontent.com/apiaddicts/sonaropenapi-rules/refs/heads/master/src/test/resources/externalRef/OAR031.yaml#/components/responses/server_error_response
22+
/users/{userId}:
23+
get:
24+
parameters:
25+
- name: userId
26+
in: path
27+
required: true
28+
type: string
29+
responses:
30+
200:
31+
description: A single user
32+
schema:
33+
$ref: '#/definitions/User'
34+
definitions:
35+
User:
36+
type: object
37+
properties:
38+
id:
39+
type: string
40+
example: "123"
41+
name:
42+
type: string
43+
example: "John"
44+
Error:
45+
type: object
46+
properties:
47+
message: # Noncompliant {{OAR031: Properties must have an example defined}}
48+
type: string
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
swagger: "2.0"
2+
info:
3+
version: "1.0.0"
4+
title: OAR031 Nested Example False Positive V2
5+
paths:
6+
/profile:
7+
put:
8+
parameters:
9+
- name: body
10+
in: body
11+
required: true
12+
schema:
13+
type: object
14+
properties:
15+
email:
16+
type: string
17+
example: "apellido@madrid.org"
18+
telefono_fijo:
19+
type: string
20+
example: "600345345"
21+
direccion:
22+
type: object
23+
properties:
24+
calle:
25+
type: string
26+
example: "Calle Indeterminada, 18"
27+
localidad:
28+
type: string
29+
example: "Madrid"
30+
provincia:
31+
type: string
32+
example: "Madrid"
33+
codigo_postal:
34+
type: string
35+
example: "28001"
36+
responses:
37+
200: # Noncompliant {{OAR031: Responses must have one or more examples defined}}
38+
description: OK

src/test/resources/checks/v2/examples/OAR031/valid.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ paths:
88
responses:
99
204:
1010
description: No content
11-
206: # Noncompliant {{OAR031: Responses must have one or more examples defined}}
11+
206:
1212
description: Pet list
1313
schema:
1414
$ref: '#/definitions/pets'
@@ -19,7 +19,7 @@ paths:
1919
parameters:
2020
- $ref: "#/parameters/id"
2121
responses:
22-
200: # Noncompliant {{OAR031: Responses must have one or more examples defined}}
22+
200:
2323
description: One pet
2424
schema:
2525
$ref: "#/definitions/pet"
@@ -56,7 +56,7 @@ definitions:
5656
items:
5757
$ref: '#/definitions/pet'
5858
responses:
59-
server_error_response: # Noncompliant {{OAR031: Responses must have one or more examples defined}}
59+
server_error_response:
6060
description: Default error response
6161
schema:
6262
type: object

src/test/resources/checks/v3/examples/OAR031/externalref.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ paths:
1414
summary: Get all users
1515
description: Returns a list of users.
1616
responses:
17-
'200': # Noncompliant {{OAR031: Responses must have one or more examples defined}}
17+
'200':
1818
description: A JSON array of user objects
1919
content:
2020
application/json:
@@ -40,7 +40,7 @@ paths:
4040
name: Puppy
4141
type: dog
4242
responses:
43-
'200': # Noncompliant {{OAR031: Responses must have one or more examples defined}}
43+
'200':
4444
description: A single user object
4545
content:
4646
application/json:

0 commit comments

Comments
 (0)