Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,90 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.5.0] - 2026-07-28

### Added

- OAR022 - OrderbyParameterCheck - Added `single-resource` test cases (v2, v3, v31, v32) verifying that paths ending with a path parameter (e.g. `/examples/{id}`) are correctly excluded by `applyToParameterizedPaths = false`.
- OAR025 - LimitParameterCheck - Added `single-resource` test cases (v2, v3, v31, v32) verifying that paths ending with a path parameter (e.g. `/examples/{id}`) are correctly excluded by `applyToParameterizedPaths = false`.
- OAR031 - ExamplesCheck - Per-level configuration via rule parameters `validate-response`, `validate-request-body`, `validate-parameter` and `validate-property` (all `true` by default); each level can be disabled independently.

### Fixed

- OAR017 - ResourcePathCheck - Added `delete` to the `exclude_patterns` default (now `get,me,search,delete`); paths ending with `/delete` (e.g. `/orders/delete`, `/orders/{orderId}/delete`) are now treated as pseudo-parameters and no longer trigger the alternation rule.
- OAR020 - ExpandParameterCheck - Fixed `verifyInV2PathEndingWithParam` test method that was incorrectly calling `verifyV3("with-param")` instead of `verifyV2("with-param")`; the Swagger 2.0 `with-param` test fixtures are now correctly exercised in v2 mode.
- OAR021 - ExcludeParameterCheck - Fixed `verifyInV2PathEndingWithParam` test, now correctly calls `verifyV2("with-param")`.
- OAR044 - MediaTypeCheck - Fixed `MEDIA_RANGE_PATTERN` to allow `*/*` (full wildcard) as a valid OAP3 media range; the type component now accepts `*` in addition to RFC 6838 type names. Added test coverage for vendor-specific types (`application/vnd.ms-excel`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `application/ld+json`, `application/vnd.github+json`).
- OAR004 - ValidWso2ScopesRoles - Fixed a field-shadowing bug in the shared base class that made the `pattern` parameter have no effect.
- OAR014 - ResourceLevelWithinNonSuggestedRange - `matchLevel` ignored `maxLevel` entirely; the parameter is now applied.
- OAR019 - SelectParameterCheck - Added real support for `paths` and `pathValidationStrategy`, and re-added the `parameterName` parameter (removed in an earlier refactor).
- OAR020 - ExpandParameterCheck - Removed hardcoded path-exclusion logic that bypassed the configurable `paths` parameter; re-added `parameterName`.
- OAR021 - ExcludeParameterCheck - Removed hardcoded path-exclusion logic that bypassed the configurable `paths` parameter; re-added `parameterName`.
- OAR038 - StandardCreateResponse - The `dataNode` parameter was never read; the check always used its default value instead.
- OAR040 - StandardWso2ScopesName - Fixed the same field-shadowing bug as OAR004.
- OAR082 - BinaryOrByteFormat - `fields-to-apply` was read before Sonar injected its configured value, so the parameter had no effect.
- OAR085 - OpenAPIVersion - `valid-versions` was read before Sonar injected its configured value, so the parameter had no effect.

### Changed

- OAR019, OAR020, OAR021 - `paths` now takes plain path segments (e.g. `/status`) instead of a regular expression. Default excluded paths (`/me`, `/health`, `/ping`, `/status`) are now matched as real path segments instead of a loose substring, so a path like `/subscription-status-reports` is no longer wrongly excluded.
- OAR037 - StringFormatCheck - Reclassified as a security rule (`VULNERABILITY`, tag `safety`, keeping its existing `format` rule group/package). String schemas must now declare a valid `format`, or — when no `format` is declared — a non-empty, syntactically valid `pattern`; schemas with neither a valid `format` nor a valid `pattern` are reported.
- OAR037 - StringFormatCheck - Rule no longer fires when a string schema omits the `format` field entirely; it only fires when `format` is present but not a recognized value.
- OAR031 - ExamplesCheck - Examples are now validated as four **independent** levels (response, request body, parameter, property). The response/request-body/parameter levels require an example declared at the media-type or schema **root** (non-recursive); examples nested inside schema properties no longer satisfy them. Aligns OAR031 with the Spectral ruleset (identical findings on the same document) and is stricter than before, so existing specs may surface new findings.

## [1.5.0-beta-4] - 2026-07-14

### Fixed

- OAR004 - ValidWso2ScopesRoles - Fixed a field-shadowing bug in the shared base class that made the `pattern` parameter have no effect.
- OAR014 - ResourceLevelWithinNonSuggestedRange - `matchLevel` ignored `maxLevel` entirely; the parameter is now applied.
- OAR019 - SelectParameterCheck - Added real support for `paths` and `pathValidationStrategy`, and re-added the `parameterName` parameter (removed in an earlier refactor).
- OAR020 - ExpandParameterCheck - Removed hardcoded path-exclusion logic that bypassed the configurable `paths` parameter; re-added `parameterName`.
- OAR021 - ExcludeParameterCheck - Removed hardcoded path-exclusion logic that bypassed the configurable `paths` parameter; re-added `parameterName`.
- OAR038 - StandardCreateResponse - The `dataNode` parameter was never read; the check always used its default value instead.
- OAR040 - StandardWso2ScopesName - Fixed the same field-shadowing bug as OAR004.
- OAR082 - BinaryOrByteFormat - `fields-to-apply` was read before Sonar injected its configured value, so the parameter had no effect.
- OAR085 - OpenAPIVersion - `valid-versions` was read before Sonar injected its configured value, so the parameter had no effect.

### Changed

- OAR019, OAR020, OAR021 - `paths` now takes plain path segments (e.g. `/status`) instead of a regular expression. Default excluded paths (`/me`, `/health`, `/ping`, `/status`) are now matched as real path segments instead of a loose substring, so a path like `/subscription-status-reports` is no longer wrongly excluded.

## [1.5.0-beta-3] - 2026-07-08

### Changed

- OAR037 - StringFormatCheck - Reclassified as a security rule (`VULNERABILITY`, tag `safety`, keeping its existing `format` rule group/package). String schemas must now declare a valid `format`, or — when no `format` is declared — a non-empty, syntactically valid `pattern`; schemas with neither a valid `format` nor a valid `pattern` are reported.

## [1.5.0-beta-2] - 2026-06-24

### Changed

- OAR037 - StringFormatCheck - Rule no longer fires when a string schema omits the `format` field entirely; it only fires when `format` is present but not a recognized value.

### Fixed

- OAR017 - ResourcePathCheck - Added `delete` to the `exclude_patterns` default (now `get,me,search,delete`); paths ending with `/delete` (e.g. `/orders/delete`, `/orders/{orderId}/delete`) are now treated as pseudo-parameters and no longer trigger the alternation rule.
- OAR020 - ExpandParameterCheck - Fixed `verifyInV2PathEndingWithParam` test method that was incorrectly calling `verifyV3("with-param")` instead of `verifyV2("with-param")`; the Swagger 2.0 `with-param` test fixtures are now correctly exercised in v2 mode.
- OAR021 - ExcludeParameterCheck - Fixed `verifyInV2PathEndingWithParam` test, now correctly calls `verifyV2("with-param")`.
- OAR044 - MediaTypeCheck - Fixed `MEDIA_RANGE_PATTERN` to allow `*/*` (full wildcard) as a valid OAP3 media range; the type component now accepts `*` in addition to RFC 6838 type names. Added test coverage for vendor-specific types (`application/vnd.ms-excel`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `application/ld+json`, `application/vnd.github+json`).

### Added

- OAR022 - OrderbyParameterCheck - Added `single-resource` test cases (v2, v3, v31, v32) verifying that paths ending with a path parameter (e.g. `/examples/{id}`) are correctly excluded by `applyToParameterizedPaths = false`.
- OAR025 - LimitParameterCheck - Added `single-resource` test cases (v2, v3, v31, v32) verifying that paths ending with a path parameter (e.g. `/examples/{id}`) are correctly excluded by `applyToParameterizedPaths = false`.

## [1.5.0-beta-1] - 2026-06-15

### Added

- OAR031 - ExamplesCheck - Per-level configuration via rule parameters `validate-response`, `validate-request-body`, `validate-parameter` and `validate-property` (all `true` by default); each level can be disabled independently.

### Changed

- OAR031 - ExamplesCheck - Examples are now validated as four **independent** levels (response, request body, parameter, property). The response/request-body/parameter levels require an example declared at the media-type or schema **root** (non-recursive); examples nested inside schema properties no longer satisfy them. Aligns OAR031 with the Spectral ruleset (identical findings on the same document) and is stricter than before, so existing specs may surface new findings.


## [1.4.1] - 2026-06-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.apiaddicts.apitools.dosonarapi</groupId>
<artifactId>sonaropenapi-rules-community</artifactId>
<version>1.4.1</version>
<version>1.5.0</version>
<packaging>sonar-plugin</packaging>

<name>SonarQube OpenAPI Community Rules</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public abstract class AbstractPatternWso2ScopesCheck extends AbstractWso2ScopesC
protected final String defaultPatternValue;

protected Pattern pattern;
private String patternStr;

protected AbstractPatternWso2ScopesCheck(String key, String message, String fieldName, String defaultPatternValue) {
this.ruleKey = key;
Expand All @@ -22,8 +21,11 @@ protected AbstractPatternWso2ScopesCheck(String key, String message, String fiel
this.defaultPatternValue = defaultPatternValue;
}

protected abstract String getPatternStr();

@Override
protected void visitFile(JsonNode root) {
String patternStr = getPatternStr();
pattern = Pattern.compile(patternStr != null ? patternStr : defaultPatternValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public class OAR004ValidWso2ScopesRolesCheck extends AbstractPatternWso2ScopesCh
public OAR004ValidWso2ScopesRolesCheck() {
super(KEY, MESSAGE, "roles", DEFAULT_PATTERN_VALUE);
}

@Override
protected String getPatternStr() {
return patternStr;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public class OAR040StandardWso2ScopesNameCheck extends AbstractPatternWso2Scopes
public OAR040StandardWso2ScopesNameCheck() {
super(KEY, MESSAGE, "name", DEFAULT_PATTERN_VALUE);
}

@Override
protected String getPatternStr() {
return patternStr;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.apiaddicts.apitools.dosonarapi.api.v32.OpenApi32Grammar;
import org.apiaddicts.apitools.dosonarapi.sslr.yaml.grammar.JsonNode;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;

@Rule(key = OAR031ExamplesCheck.KEY)
public class OAR031ExamplesCheck extends BaseCheck {
Expand All @@ -27,6 +28,39 @@
private static final String ITEMS = "items";

private static final String ERROR_RESPONSE = "OAR031.error-response";
private static final String ERROR_REQUEST = "OAR031.error-request";

@RuleProperty(
key = "validateResponse",
description = "Validate that responses declare a body-level example",
defaultValue = "true",
type = "BOOLEAN"
)
private boolean validateResponse = true;

@RuleProperty(
key = "validateRequestBody",
description = "Validate that request bodies declare a body-level example",
defaultValue = "true",
type = "BOOLEAN"
)
private boolean validateRequestBody = true;

@RuleProperty(
key = "validateParameter",
description = "Validate that parameters declare an example",
defaultValue = "true",
type = "BOOLEAN"
)
private boolean validateParameter = true;

@RuleProperty(
key = "validateProperty",
description = "Validate that each schema property declares an example",
defaultValue = "true",
type = "BOOLEAN"
)
private boolean validateProperty = true;

private final ExternalRefHandler handleExternalRef = new ExternalRefHandler();

Expand Down Expand Up @@ -67,11 +101,13 @@

JsonNode schema = resolved.get(SCHEMA);

// Parameter level: the parameter itself, or its schema's ROOT, must declare an
// example. Examples buried inside schema properties do NOT satisfy this level.
boolean hasExample = !resolved.get(EXAMPLE).isMissing()
|| !resolved.get(EXAMPLES).isMissing()
|| (!schema.isMissing() && isSchemaCovered(schema));
|| schemaHasRootExample(schema);

if (!hasExample) {
if (validateParameter && !hasExample) {
addIssue(KEY, translate("OAR031.error-parameter"), handleExternalRef.getTrueNode(node));
}
});
Expand All @@ -89,10 +125,11 @@
private void visitResponseV2Node(JsonNode node) {
handleExternalRef.resolve(node, resolved -> {
JsonNode schemaNode = resolved.get(SCHEMA);
// Response level: a response-level examples map or the schema ROOT example.
boolean hasExample = !resolved.get(EXAMPLES).isMissing()
|| (!schemaNode.isMissing() && isSchemaCovered(schemaNode));
|| schemaHasRootExample(schemaNode);

if (!hasExample) {
if (validateResponse && !hasExample) {
addIssue(KEY, translate(ERROR_RESPONSE), handleExternalRef.getTrueNode(node.key()));
}
});
Expand All @@ -117,26 +154,42 @@
}

private void visitRequestBodyOrResponseV3Node(JsonNode node) {
boolean isRequestBody = node.getType().equals(OpenApi3Grammar.REQUEST_BODY)
|| node.getType().equals(OpenApi31Grammar.REQUEST_BODY)
|| node.getType().equals(OpenApi32Grammar.REQUEST_BODY);
boolean levelOn = isRequestBody ? validateRequestBody : validateResponse;
if (!levelOn) return;

String errorKey = isRequestBody ? ERROR_REQUEST : ERROR_RESPONSE;
JsonNode content = node.at("/content");

if (content.isMissing()) {
String errorKey = (node.getType().equals(OpenApi3Grammar.REQUEST_BODY) || node.getType().equals(OpenApi31Grammar.REQUEST_BODY) || node.getType().equals(OpenApi32Grammar.REQUEST_BODY)) ? "OAR031.error-request" : ERROR_RESPONSE;
addIssue(KEY, translate(errorKey), handleExternalRef.getTrueNode(node.key()));
return;
}

for (JsonNode mediaTypeNode : content.propertyMap().values()) {
JsonNode schemaNode = mediaTypeNode.get(SCHEMA);
boolean hasExplicitExample = !mediaTypeNode.get(EXAMPLES).isMissing()
|| !mediaTypeNode.get(EXAMPLE).isMissing();
// Body level: a media-type example/examples or the schema ROOT example.
// Examples buried inside schema properties do NOT satisfy this level.
boolean hasBodyExample = !mediaTypeNode.get(EXAMPLES).isMissing()
|| !mediaTypeNode.get(EXAMPLE).isMissing()
|| schemaHasRootExample(schemaNode);

if (!hasExplicitExample && !isSchemaCovered(schemaNode)) {
String errorKey = (node.getType().equals(OpenApi3Grammar.REQUEST_BODY) || node.getType().equals(OpenApi31Grammar.REQUEST_BODY) || node.getType().equals(OpenApi32Grammar.REQUEST_BODY)) ? "OAR031.error-request" : ERROR_RESPONSE;
if (!hasBodyExample) {
addIssue(KEY, translate(errorKey), handleExternalRef.getTrueNode(node.key()));
}
}
}

// Non-recursive: only an example declared directly on the schema root counts.
private boolean schemaHasRootExample(JsonNode schemaNode) {
if (schemaNode.isMissing()) return false;
return handleExternalRef.resolve(schemaNode, resolved ->
!resolved.get(EXAMPLE).isMissing() || !resolved.get(EXAMPLES).isMissing());
}

// Recursive coverage, used only by the property-level walk.
private boolean isSchemaCovered(JsonNode schemaNode) {
if (schemaNode.isMissing()) return false;

Expand All @@ -160,6 +213,8 @@
}

private void visitSchemaNode(JsonNode node) {
if (!validateProperty) return;

JsonNode parentNode = (JsonNode) node.getParent().getParent();

if (parentNode.getType().equals(OpenApi3Grammar.PARAMETER) || parentNode.getType().equals(OpenApi31Grammar.PARAMETER) || parentNode.getType().equals(OpenApi32Grammar.PARAMETER)) {
Expand All @@ -184,7 +239,9 @@
}
}

private void visitPathNode(JsonNode node) {

Check failure on line 242 in src/main/java/apiaddicts/sonar/openapi/checks/examples/OAR031ExamplesCheck.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=apiaddicts_sonaropenapi-rules&issues=AZ-nUAjQXfKCs2VzK3r6&open=AZ-nUAjQXfKCs2VzK3r6&pullRequest=124
if (!validateProperty) return;

node.properties().stream()
.filter(prop -> isOperation(prop))
.map(JsonNode::value)
Expand Down Expand Up @@ -223,4 +280,4 @@
});
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ private void visitV2Node(JsonNode node) {
String type = typeNode.getTokenValue();
JsonNode formatNode = node.get("format");
if (formatNode.isMissing()) {
validate(type, null, typeNode);
validate(type, null, typeNode, node);
return;
}
String format = formatNode.getTokenValue();
if (format == null || format.isBlank()) return;
validate(type, format.trim(), typeNode);
validate(type, format.trim(), typeNode, node);
}

public abstract void validate(String type, String format, JsonNode typeNode);
public abstract void validate(String type, String format, JsonNode typeNode, JsonNode node);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class OAR016NumericFormatCheck extends AbstractFormatCheck {
private static final String MESSAGE = "OAR016.error";

@Override
public void validate(String type, String format, JsonNode typeNode) {
public void validate(String type, String format, JsonNode typeNode, JsonNode node) {
if (isInvalidInteger(type, format) || isInvalidNumber(type, format)) {
addIssue(KEY, translate(MESSAGE), typeNode.key());
}
Expand Down
Loading
Loading