From 333ca3602b3fb55655656f6579a120113391e4ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:16:01 +0000 Subject: [PATCH 1/3] Initial plan From ac3491cf5ac5fb509e5bf84d9c6f6fdc2f6194c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:39:01 +0000 Subject: [PATCH 2/3] Add SerializableMethodsCheck for JsonSerializable and XmlSerializable validation Co-authored-by: srnagar <51379715+srnagar@users.noreply.github.com> --- .../checkstyle/clientcore/checkstyle.xml | 4 + .../checkstyle/track2/checkstyle.xml | 4 + .../checkstyle/vnext/checkstyle.xml | 4 + .../checks/SerializableMethodsCheck.java | 171 ++++++++++++++ .../checks/SerializableMethodsCheckTest.java | 212 ++++++++++++++++++ 5 files changed, 395 insertions(+) create mode 100644 sdk/tools/linting-extensions/src/main/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheck.java create mode 100644 sdk/tools/linting-extensions/src/test/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheckTest.java diff --git a/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml b/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml index 18abe9430410..b85291b9e9eb 100644 --- a/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml +++ b/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml @@ -426,5 +426,9 @@ + + + + diff --git a/eng/lintingconfigs/checkstyle/track2/checkstyle.xml b/eng/lintingconfigs/checkstyle/track2/checkstyle.xml index 6c6c6082878e..8717a8d674e3 100644 --- a/eng/lintingconfigs/checkstyle/track2/checkstyle.xml +++ b/eng/lintingconfigs/checkstyle/track2/checkstyle.xml @@ -420,5 +420,9 @@ + + + + diff --git a/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml b/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml index 6432e8cc91ba..1519ce4b7134 100644 --- a/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml +++ b/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml @@ -425,5 +425,9 @@ + + + + diff --git a/sdk/tools/linting-extensions/src/main/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheck.java b/sdk/tools/linting-extensions/src/main/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheck.java new file mode 100644 index 000000000000..2614323041b3 --- /dev/null +++ b/sdk/tools/linting-extensions/src/main/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheck.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.linting.extensions.checkstyle.checks; + +import com.puppycrawl.tools.checkstyle.api.AbstractCheck; +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.TokenTypes; + +import java.util.ArrayList; +import java.util.List; + +/** + * Validates serialization method completeness for JsonSerializable and XmlSerializable implementations. + */ +public class SerializableMethodsCheck extends AbstractCheck { + static final String ERR_NO_TO_JSON = "Class implementing JsonSerializable must provide a toJson method."; + static final String ERR_NO_FROM_JSON = "Class implementing JsonSerializable must provide a static fromJson method."; + static final String ERR_NO_TO_XML = "Class implementing XmlSerializable must provide a toXml method."; + static final String ERR_NO_FROM_XML = "Class implementing XmlSerializable must provide a static fromXml method."; + + private List snapshotArchive; + + @Override + public int[] getDefaultTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getAcceptableTokens() { + return getRequiredTokens(); + } + + @Override + public int[] getRequiredTokens() { + return new int[] { TokenTypes.CLASS_DEF, TokenTypes.METHOD_DEF }; + } + + @Override + public void beginTree(DetailAST rootNode) { + snapshotArchive = new ArrayList<>(); + } + + @Override + public void visitToken(DetailAST currentNode) { + int tokenKind = currentNode.getType(); + + if (tokenKind == TokenTypes.CLASS_DEF) { + TypeSnapshot snapshot = captureTypeSnapshot(currentNode); + snapshotArchive.add(snapshot); + } else if (tokenKind == TokenTypes.METHOD_DEF) { + integrateMethodIntoSnapshot(currentNode); + } + } + + @Override + public void leaveToken(DetailAST currentNode) { + if (currentNode.getType() == TokenTypes.CLASS_DEF) { + performSnapshotAudit(currentNode); + } + } + + private TypeSnapshot captureTypeSnapshot(DetailAST classNode) { + TypeSnapshot snapshot = new TypeSnapshot(); + snapshot.classNode = classNode; + + DetailAST interfaceSection = classNode.findFirstToken(TokenTypes.IMPLEMENTS_CLAUSE); + if (interfaceSection != null) { + digestInterfaceSection(interfaceSection, snapshot); + } + + return snapshot; + } + + private void digestInterfaceSection(DetailAST interfaceSection, TypeSnapshot snapshot) { + DetailAST cursor = interfaceSection.getFirstChild(); + + while (cursor != null) { + if (cursor.getType() == TokenTypes.IDENT) { + String interfaceLabel = cursor.getText(); + + if ("JsonSerializable".equals(interfaceLabel)) { + snapshot.mandatesJson = true; + } else if ("XmlSerializable".equals(interfaceLabel)) { + snapshot.mandatesXml = true; + } + } + cursor = cursor.getNextSibling(); + } + } + + private void integrateMethodIntoSnapshot(DetailAST methodNode) { + if (snapshotArchive.isEmpty()) { + return; + } + + TypeSnapshot latestSnapshot = snapshotArchive.get(snapshotArchive.size() - 1); + + if (!latestSnapshot.mandatesJson && !latestSnapshot.mandatesXml) { + return; + } + + DetailAST nameNode = methodNode.findFirstToken(TokenTypes.IDENT); + if (nameNode == null) { + return; + } + + String methodLabel = nameNode.getText(); + boolean markedStatic = probeForStaticMarker(methodNode); + + latestSnapshot.digestMethod(methodLabel, markedStatic); + } + + private boolean probeForStaticMarker(DetailAST methodNode) { + DetailAST modifierBlock = methodNode.findFirstToken(TokenTypes.MODIFIERS); + + if (modifierBlock == null) { + return false; + } + + return modifierBlock.findFirstToken(TokenTypes.LITERAL_STATIC) != null; + } + + private void performSnapshotAudit(DetailAST classNode) { + if (snapshotArchive.isEmpty()) { + return; + } + + TypeSnapshot snapshot = snapshotArchive.remove(snapshotArchive.size() - 1); + + if (snapshot.mandatesJson) { + if (!snapshot.observedToJson) { + log(classNode, ERR_NO_TO_JSON); + } + if (!snapshot.observedFromJson) { + log(classNode, ERR_NO_FROM_JSON); + } + } + + if (snapshot.mandatesXml) { + if (!snapshot.observedToXml) { + log(classNode, ERR_NO_TO_XML); + } + if (!snapshot.observedFromXml) { + log(classNode, ERR_NO_FROM_XML); + } + } + } + + private static class TypeSnapshot { + DetailAST classNode; + boolean mandatesJson; + boolean mandatesXml; + boolean observedToJson; + boolean observedFromJson; + boolean observedToXml; + boolean observedFromXml; + + void digestMethod(String methodLabel, boolean markedStatic) { + if ("toJson".equals(methodLabel)) { + observedToJson = true; + } else if ("fromJson".equals(methodLabel) && markedStatic) { + observedFromJson = true; + } else if ("toXml".equals(methodLabel)) { + observedToXml = true; + } else if ("fromXml".equals(methodLabel) && markedStatic) { + observedFromXml = true; + } + } + } +} diff --git a/sdk/tools/linting-extensions/src/test/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheckTest.java b/sdk/tools/linting-extensions/src/test/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheckTest.java new file mode 100644 index 000000000000..2bab72be5120 --- /dev/null +++ b/sdk/tools/linting-extensions/src/test/java/io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheckTest.java @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.linting.extensions.checkstyle.checks; + +import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport; +import com.puppycrawl.tools.checkstyle.Checker; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static io.clientcore.linting.extensions.checkstyle.checks.SerializableMethodsCheck.ERR_NO_FROM_JSON; +import static io.clientcore.linting.extensions.checkstyle.checks.SerializableMethodsCheck.ERR_NO_FROM_XML; +import static io.clientcore.linting.extensions.checkstyle.checks.SerializableMethodsCheck.ERR_NO_TO_JSON; +import static io.clientcore.linting.extensions.checkstyle.checks.SerializableMethodsCheck.ERR_NO_TO_XML; + +/** + * Tests {@link SerializableMethodsCheck}. + */ +public class SerializableMethodsCheckTest extends AbstractModuleTestSupport { + private Checker lintingChecker; + + @BeforeEach + public void setupChecker() throws Exception { + lintingChecker = createChecker(createModuleConfig(SerializableMethodsCheck.class)); + } + + @AfterEach + public void teardownChecker() { + lintingChecker.destroy(); + } + + @Override + protected String getPackageLocation() { + return "io/clientcore/linting/extensions/checkstyle/checks/SerializableMethodsCheck"; + } + + @Test + public void jsonSerializableWithBothMethods() throws Exception { + File testFile = TestUtils.createCheckFile("jsonComplete", "package com.azure;", + "public class JsonComplete implements JsonSerializable {", " public void toJson() {}", + " public static JsonComplete fromJson() { return null; }", "}"); + + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath()); + } + + @Test + public void jsonSerializableMissingToJson() throws Exception { + File testFile = TestUtils.createCheckFile("jsonMissingTo", "package com.azure;", + "public class JsonMissingTo implements JsonSerializable {", + " public static JsonMissingTo fromJson() { return null; }", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_TO_JSON }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void jsonSerializableMissingFromJson() throws Exception { + File testFile = TestUtils.createCheckFile("jsonMissingFrom", "package com.azure;", + "public class JsonMissingFrom implements JsonSerializable {", " public void toJson() {}", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_FROM_JSON }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void jsonSerializableMissingBothMethods() throws Exception { + File testFile = TestUtils.createCheckFile("jsonMissingBoth", "package com.azure;", + "public class JsonMissingBoth implements JsonSerializable {", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_TO_JSON, "2:1: " + ERR_NO_FROM_JSON }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void jsonSerializableWithNonStaticFromJson() throws Exception { + File testFile = TestUtils.createCheckFile("jsonNonStaticFrom", "package com.azure;", + "public class JsonNonStaticFrom implements JsonSerializable {", " public void toJson() {}", + " public JsonNonStaticFrom fromJson() { return null; }", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_FROM_JSON }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void xmlSerializableWithBothMethods() throws Exception { + File testFile = TestUtils.createCheckFile("xmlComplete", "package com.azure;", + "public class XmlComplete implements XmlSerializable {", " public void toXml() {}", + " public static XmlComplete fromXml() { return null; }", "}"); + + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath()); + } + + @Test + public void xmlSerializableMissingToXml() throws Exception { + File testFile = TestUtils.createCheckFile("xmlMissingTo", "package com.azure;", + "public class XmlMissingTo implements XmlSerializable {", + " public static XmlMissingTo fromXml() { return null; }", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_TO_XML }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void xmlSerializableMissingFromXml() throws Exception { + File testFile = TestUtils.createCheckFile("xmlMissingFrom", "package com.azure;", + "public class XmlMissingFrom implements XmlSerializable {", " public void toXml() {}", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_FROM_XML }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void xmlSerializableMissingBothMethods() throws Exception { + File testFile = TestUtils.createCheckFile("xmlMissingBoth", "package com.azure;", + "public class XmlMissingBoth implements XmlSerializable {", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_TO_XML, "2:1: " + ERR_NO_FROM_XML }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void xmlSerializableWithNonStaticFromXml() throws Exception { + File testFile = TestUtils.createCheckFile("xmlNonStaticFrom", "package com.azure;", + "public class XmlNonStaticFrom implements XmlSerializable {", " public void toXml() {}", + " public XmlNonStaticFrom fromXml() { return null; }", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_FROM_XML }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void bothInterfacesWithAllMethods() throws Exception { + File testFile = TestUtils.createCheckFile("bothComplete", "package com.azure;", + "public class BothComplete implements JsonSerializable, XmlSerializable {", " public void toJson() {}", + " public static BothComplete fromJson() { return null; }", " public void toXml() {}", + " public static BothComplete fromXml() { return null; }", "}"); + + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath()); + } + + @Test + public void bothInterfacesMissingAllMethods() throws Exception { + File testFile = TestUtils.createCheckFile("bothMissing", "package com.azure;", + "public class BothMissing implements JsonSerializable, XmlSerializable {", "}"); + + String[] expectedErrors = { + "2:1: " + ERR_NO_TO_JSON, + "2:1: " + ERR_NO_FROM_JSON, + "2:1: " + ERR_NO_TO_XML, + "2:1: " + ERR_NO_FROM_XML }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void bothInterfacesMissingJsonMethods() throws Exception { + File testFile = TestUtils.createCheckFile("bothMissingJson", "package com.azure;", + "public class BothMissingJson implements JsonSerializable, XmlSerializable {", " public void toXml() {}", + " public static BothMissingJson fromXml() { return null; }", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_TO_JSON, "2:1: " + ERR_NO_FROM_JSON }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void bothInterfacesMissingXmlMethods() throws Exception { + File testFile = TestUtils.createCheckFile("bothMissingXml", "package com.azure;", + "public class BothMissingXml implements JsonSerializable, XmlSerializable {", " public void toJson() {}", + " public static BothMissingXml fromJson() { return null; }", "}"); + + String[] expectedErrors = { "2:1: " + ERR_NO_TO_XML, "2:1: " + ERR_NO_FROM_XML }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void classNotImplementingInterface() throws Exception { + File testFile = TestUtils.createCheckFile("noInterface", "package com.azure;", "public class NoInterface {", + " public void someMethod() {}", "}"); + + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath()); + } + + @Test + public void nestedClassWithJsonSerializable() throws Exception { + File testFile = TestUtils.createCheckFile("nestedJson", "package com.azure;", "public class OuterClass {", + " public static class InnerClass implements JsonSerializable {", " public void toJson() {}", + " public static InnerClass fromJson() { return null; }", " }", "}"); + + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath()); + } + + @Test + public void nestedClassMissingMethods() throws Exception { + File testFile = TestUtils.createCheckFile("nestedMissing", "package com.azure;", "public class OuterClass {", + " public static class InnerClass implements JsonSerializable {", " }", "}"); + + String[] expectedErrors = { "3:5: " + ERR_NO_TO_JSON, "3:5: " + ERR_NO_FROM_JSON }; + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath(), expectedErrors); + } + + @Test + public void classWithExtraMethodsAndCorrectSerializationMethods() throws Exception { + File testFile = TestUtils.createCheckFile("extraMethods", "package com.azure;", + "public class ExtraMethods implements JsonSerializable {", " public void toJson() {}", + " public static ExtraMethods fromJson() { return null; }", " public void otherMethod() {}", + " public String getData() { return null; }", "}"); + + verify(lintingChecker, new File[] { testFile }, testFile.getAbsolutePath()); + } +} From 90aa153df9d5c2bb0a11b20e23fa43594f5782a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:40:05 +0000 Subject: [PATCH 3/3] Fix duplicate comment headers in checkstyle configurations Co-authored-by: srnagar <51379715+srnagar@users.noreply.github.com> --- eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml | 1 - eng/lintingconfigs/checkstyle/track2/checkstyle.xml | 1 - eng/lintingconfigs/checkstyle/vnext/checkstyle.xml | 1 - 3 files changed, 3 deletions(-) diff --git a/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml b/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml index b85291b9e9eb..e5f2cafd3123 100644 --- a/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml +++ b/eng/lintingconfigs/checkstyle/clientcore/checkstyle.xml @@ -427,7 +427,6 @@ - diff --git a/eng/lintingconfigs/checkstyle/track2/checkstyle.xml b/eng/lintingconfigs/checkstyle/track2/checkstyle.xml index 8717a8d674e3..ea5bc3edb785 100644 --- a/eng/lintingconfigs/checkstyle/track2/checkstyle.xml +++ b/eng/lintingconfigs/checkstyle/track2/checkstyle.xml @@ -421,7 +421,6 @@ - diff --git a/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml b/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml index 1519ce4b7134..efd19a736c8a 100644 --- a/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml +++ b/eng/lintingconfigs/checkstyle/vnext/checkstyle.xml @@ -426,7 +426,6 @@ -