Skip to content
Draft
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
167 changes: 158 additions & 9 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
name: verify
on:
pull_request:

# Code Scanning needs write access to upload SARIF results for inline annotations.
permissions:
contents: read
security-events: write

jobs:
pmd:
# Fast, early-fail lint lane: PMD + Checkstyle (+ CPD). Turns red in a few
# minutes on any violation, independent of the long build below, so a stray
# PMD/Checkstyle issue is reported immediately rather than after `verify`.
#
# `compile` is in the same invocation as the analysis goals: PMD's
# type-resolving rules (e.g. InvalidLogMessageFormat on the SLF4J
# trailing-Throwable idiom) need Tycho's aux-classpath, which a fresh `mvn`
# does not inherit from a prior step's target/classes.
#
# `--fail-never` lets every module produce its report (no Maven cascade-skip),
# so the uploaded SARIF — and therefore the inline annotations — are complete.
# The gate is a jq count over the merged SARIFs; compile errors still halt
# (MojoExecutionException is not suppressed by --fail-never).
lint:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
Expand All @@ -12,10 +31,90 @@ jobs:
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: PMD Check
run: mvn pmd:pmd pmd:cpd pmd:check pmd:cpd-check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
checkstyle:
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: /home/runner/.m2/repository
key: ${{ runner.os }}-maven-0-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-0-

- name: PMD + Checkstyle reports (SARIF)
# PMD: SarifRenderer FQCN — emits pmd.sarif.json AND keeps pmd.xml.
# Checkstyle: output.format=sarif — SARIF content in checkstyle-result.xml.
# CPD is excluded here: the global -Dformat flag uses PMD's Renderer
# hierarchy and would ClassCastException CPD's CPDReportRenderer.
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
compile \
pmd:pmd checkstyle:checkstyle \
-Dformat=net.sourceforge.pmd.renderers.SarifRenderer \
-Dcheckstyle.output.format=sarif

- name: CPD report (separate invocation — no SARIF support)
# CPD has no SARIF renderer; emits cpd.xml only. Run standalone so the
# PMD -Dformat flag isn't in scope.
# NOTE: project CPD token threshold is currently very high (issue #1339),
# which effectively disables detection; re-tune once #1339 lands.
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
compile \
pmd:cpd-check

- name: Merge per-module SARIFs (PMD + Checkstyle)
if: always()
# Code Scanning accepts one run per category per upload; each analyzer
# writes one SARIF per module, so concatenate each analyzer's results
# into a single run under .sarif-merged/.
run: |
mkdir -p .sarif-merged
# Merge per-module SARIFs into one run. Filter to JSON-parseable files:
# the ddk-parent aggregator writes a plain-XML checkstyle-result.xml that
# would break jq, and modules with no findings may emit non-SARIF stubs.
merge() { # $1 = find-glob, $2 = output
local f valid=()
while IFS= read -r f; do
jq -e . "$f" >/dev/null 2>&1 && valid+=("$f")
done < <(find . -path "$1")
if [ ${#valid[@]} -gt 0 ]; then
jq -s '{
"$schema": .[0]."$schema", version: .[0].version,
runs: [{ tool: .[0].runs[0].tool,
results: [.[].runs[].results[]?],
invocations: [.[].runs[].invocations[]?] }]
}' "${valid[@]}" > "$2"
fi
}
merge '*/target/pmd.sarif.json' .sarif-merged/pmd.sarif
merge '*/target/checkstyle-result.xml' .sarif-merged/checkstyle.sarif

- name: Gate on PMD / CPD / Checkstyle violations
run: |
set -eu
sarif_total=$(jq '[.runs[].results[]?] | length' \
.sarif-merged/pmd.sarif .sarif-merged/checkstyle.sarif 2>/dev/null \
| awk '{s+=$1} END {print s+0}')
cpd_total=$(find . -name 'cpd.xml' -path '*/target/*' -exec grep -c '<duplication ' {} + 2>/dev/null \
| awk -F: '{s+=$2} END {print s+0}')
echo "PMD/Checkstyle SARIF violations: $sarif_total"
echo "CPD duplications: $cpd_total"
if [ "$sarif_total" != "0" ] || [ "$cpd_total" != "0" ]; then
echo "::error::Static analysis found violations (PMD/CPD/Checkstyle)."
exit 1
fi

- name: Upload PMD/Checkstyle SARIF to Code Scanning
if: always()
uses: github/codeql-action/upload-sarif@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3.35.4
with:
sarif_file: .sarif-merged
category: lint

# SpotBugs is the slow critical-path analysis (the experiments' durable
# finding), so it runs in its own parallel lane and never delays `lint`.
spotbugs:
runs-on: ubuntu-24.04
env:
MAVEN_OPTS: -Xmx4g
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
Expand All @@ -24,14 +123,66 @@ jobs:
java-version: '21'
- name: Set up Workspace Environment Variable
run: echo "WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV
- name: Checkstyle Check
run: mvn checkstyle:checkstyle checkstyle:check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
- name: Cache Maven dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: /home/runner/.m2/repository
key: ${{ runner.os }}-maven-0-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-0-

- name: SpotBugs report (SARIF)
# sarifOutput=true emits spotbugsSarif.json (also writes spotbugsXml.xml).
run: |
mvn -T 2C -f ./ddk-parent/pom.xml --batch-mode --fail-never \
compile \
spotbugs:spotbugs \
-Dspotbugs.sarifOutput=true

- name: Merge per-module SpotBugs SARIFs
if: always()
run: |
mkdir -p .sarif-merged
valid=()
while IFS= read -r f; do
jq -e . "$f" >/dev/null 2>&1 && valid+=("$f")
done < <(find . -path '*/target/spotbugsSarif.json')
if [ ${#valid[@]} -gt 0 ]; then
jq -s '{
"$schema": .[0]."$schema", version: .[0].version,
runs: [{ tool: .[0].runs[0].tool,
results: [.[].runs[].results[]?],
invocations: [.[].runs[].invocations[]?] }]
}' "${valid[@]}" > .sarif-merged/spotbugs.sarif
fi

- name: Gate on SpotBugs violations
run: |
set -eu
sb_total=$(jq '[.runs[].results[]?] | length' .sarif-merged/spotbugs.sarif 2>/dev/null || echo 0)
echo "SpotBugs SARIF violations: $sb_total"
if [ "$sb_total" != "0" ]; then
echo "::error::SpotBugs found violations."
exit 1
fi

- name: Upload SpotBugs SARIF to Code Scanning
if: always()
uses: github/codeql-action/upload-sarif@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3.35.4
with:
sarif_file: .sarif-merged
category: spotbugs

line-endings:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Check LF line endings
run: bash .github/scripts/check-line-endings.sh

# Build + tests only. Static analysis now lives in the `lint` and `spotbugs`
# jobs, so the redundant checkstyle/pmd/spotbugs goals are dropped from here —
# this is the wall-clock long pole and no longer re-runs analysis.
# No `-T 2C`: tests are not known to pass reliably under reactor parallelism.
maven-verify:
runs-on: ubuntu-24.04
steps:
Expand All @@ -51,9 +202,7 @@ jobs:
path: /home/runner/.m2/repository
key: ${{ runner.os }}-maven-0-${{ hashFiles('**/pom.xml') }}
- name: Build with Maven within a virtual X Server Environment
# Run pmd:pmd and pmd:cpd first to generate reports for all modules, then run pmd:check and pmd:cpd-check
# This ensures all violations are collected and reported before the build fails
run: xvfb-run mvn clean verify checkstyle:check pmd:pmd pmd:cpd pmd:check pmd:cpd-check spotbugs:check -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
run: xvfb-run mvn clean verify -f ./ddk-parent/pom.xml --batch-mode --fail-at-end
- name: Fail on missing surefire reports
if: always()
run: bash .github/scripts/check-surefire-reports.sh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
@SuppressWarnings({"checkstyle:AbstractClassName"})
public abstract class DispatchingCheckImpl extends AbstractCheckImpl {

// CPD-OFF — incidental token overlap (DI field + trace/try idiom), not an extractable unit (#1339)
@Inject
private ITraceSet traceSet;

Expand Down Expand Up @@ -72,6 +73,7 @@ public boolean validate(final EClass eClass, final EObject object, final Diagnos

State state = new State();
state.chain = diagnostics;
// CPD-ON
state.eventCollector = eventCollector;

validate(checkMode, object, state);
Expand Down Expand Up @@ -123,6 +125,7 @@ protected void validate(final String contextName, final String qContextName, fin
if (!disabledMethodTracker.isDisabled(contextName)) {
Collector eventCollector = diagnosticCollector.getEventCollector();
try {
// CPD-OFF — incidental token overlap (DI field + trace/try idiom), not an extractable unit (#1339)
traceStart(qContextName, object, eventCollector);
checkAction.run();
} catch (Exception e) {
Expand Down Expand Up @@ -160,6 +163,7 @@ protected static class State implements ValidationMessageAcceptorMixin, Diagnost
// CHECKSTYLE:OFF
public DiagnosticChain chain;
public CheckType currentCheckType;
// CPD-ON
public boolean hasErrors;
public ResourceValidationRuleSummaryEvent.Collector eventCollector;
// CHECKSTYLE:ON
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,18 @@ protected boolean isExtensionEnabled(final IPluginModelBase base, final CheckCat
return !config.isGenerateLanguageInternalChecks();
}

/**
* Documentation extensions do not reference a generated target class, so documentation helpers have no target class name and
* provide their own {@link #isExtensionUpdateRequired} logic that never consults it.
*
* @param catalog
* the check catalog
* @return never returns normally
*/
@SuppressWarnings("PMD.UnusedFormalParameter")
@Override
protected String getTargetClassName(final CheckCatalog catalog) {
throw new UnsupportedOperationException("Documentation extension helpers have no target class"); //$NON-NLS-1$
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,41 @@ protected boolean isExtensionUpdateRequired(final CheckCatalog catalog, final IP
return extension.getPoint().equals(getExtensionPointId()); // if points are different, given extension must not be updated
}

/**
* Checks whether a class-referencing extension (validator / quickfix) needs updating: it must point at this helper's
* extension point and reference exactly one element whose target class, language and catalog name still match the
* check catalog. Shared by the validator and quickfix helpers, whose only difference is {@link #getTargetClassName}.
*
* @param catalog
* the catalog
* @param extension
* the extension
* @param elements
* the elements
* @return true, if the extension must be regenerated
*/
protected boolean isTargetClassExtensionUpdateRequired(final CheckCatalog catalog, final IPluginExtension extension, final Iterable<IPluginElement> elements) {
// CHECKSTYLE:OFF
// @Format-Off
return getExtensionPointId().equals(extension.getPoint())
&& (!extensionNameMatches(extension, catalog)
|| Iterables.size(elements) != 1
|| !targetClassMatches(Iterables.get(elements, 0), getTargetClassName(catalog))
|| catalog.getGrammar() == null && Iterables.get(elements, 0).getAttribute(LANGUAGE_ELEMENT_TAG) != null
|| catalog.getGrammar() != null && !languageNameMatches(Iterables.get(elements, 0), catalog.getGrammar().getName()));
// @Format-On
// CHECKSTYLE:ON
}

/**
* Gets the target class name based on the package path of given check catalog.
*
* @param catalog
* the check catalog
* @return the target class FQN
*/
protected abstract String getTargetClassName(CheckCatalog catalog);

/**
* Updates a given extension to values calculated using given check catalog.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ public String getExtensionPointName(final CheckCatalog catalog) {
* the check catalog
* @return the target class FQN
*/
private String getTargetClassName(final CheckCatalog catalog) {
@Override
protected String getTargetClassName(final CheckCatalog catalog) {
return getFromServiceProvider(CheckGeneratorNaming.class, catalog).qualifiedPreferenceInitializerClassName(catalog);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,14 @@ protected void doUpdateExtension(final CheckCatalog catalog, final IPluginExtens
* the check catalog
* @return the target class FQN
*/
private String getTargetClassName(final CheckCatalog catalog) {
@Override
protected String getTargetClassName(final CheckCatalog catalog) {
return getFromServiceProvider(CheckGeneratorNaming.class, catalog).qualifiedQuickfixClassName(catalog);
}

@Override
public boolean isExtensionUpdateRequired(final CheckCatalog catalog, final IPluginExtension extension, final Iterable<IPluginElement> elements) {
// CHECKSTYLE:OFF
// @Format-Off
return QUICKFIX_EXTENSION_POINT_ID.equals(extension.getPoint())
&& (!extensionNameMatches(extension, catalog)
|| Iterables.size(elements) != 1
|| !targetClassMatches(Iterables.get(elements, 0), getTargetClassName(catalog))
|| catalog.getGrammar() == null && Iterables.get(elements, 0).getAttribute(LANGUAGE_ELEMENT_TAG) != null
|| catalog.getGrammar() != null && !languageNameMatches(Iterables.get(elements, 0), catalog.getGrammar().getName()));
// @Format-On
// CHECKSTYLE:ON
protected boolean isExtensionUpdateRequired(final CheckCatalog catalog, final IPluginExtension extension, final Iterable<IPluginElement> elements) {
return isTargetClassExtensionUpdateRequired(catalog, extension, elements);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,14 @@ private String getCatalogResourceName(final CheckCatalog catalog) {
* the check catalog
* @return the target class FQN
*/
private String getTargetClassName(final CheckCatalog catalog) {
@Override
protected String getTargetClassName(final CheckCatalog catalog) {
return getFromServiceProvider(CheckGeneratorNaming.class, catalog).qualifiedValidatorClassName(catalog);
}

@Override
public boolean isExtensionUpdateRequired(final CheckCatalog catalog, final IPluginExtension extension, final Iterable<IPluginElement> elements) {
// CHECKSTYLE:OFF
// @Format-Off
return CHECK_EXTENSION_POINT_ID.equals(extension.getPoint())
&& (!extensionNameMatches(extension, catalog)
|| Iterables.size(elements) != 1
|| !targetClassMatches(Iterables.get(elements, 0), getTargetClassName(catalog))
|| catalog.getGrammar() == null && Iterables.get(elements, 0).getAttribute(LANGUAGE_ELEMENT_TAG) != null
|| catalog.getGrammar() != null && !languageNameMatches(Iterables.get(elements, 0), catalog.getGrammar().getName())
);
// @Format-On
// CHECKSTYLE:ON
protected boolean isExtensionUpdateRequired(final CheckCatalog catalog, final IPluginExtension extension, final Iterable<IPluginElement> elements) {
return isTargetClassExtensionUpdateRequired(catalog, extension, elements);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ void testUnnamedFormalAfterNamed4() {
assertSame(unnamedFormal2, matchResult.getUnnamedFormalsAfterNamed().get(1), UNNAMED_FORMAL_AFTER_NAMED_NOT_LOCATED);
}

// CPD-OFF — explicit parameterized test cases, kept readable over shared (#1339)
@Test
void testForceMatchByPosition1() {
List<NamedFormalParameter> formals = new ArrayList<NamedFormalParameter>();
Expand Down Expand Up @@ -946,5 +947,6 @@ void testForceMatchByPosition3() {
checkParameterMatch(IParameterMatchChecker.MatchStatus.MATCH, actuals.get(1), formals.get(1), matches.get(1));
checkParameterMatch(IParameterMatchChecker.MatchStatus.MATCH, actuals.get(2), formals.get(2), matches.get(2));
}
// CPD-ON

}
Loading
Loading