From d7d50a61c1e871b2ca030bb44e5dd4194f8ec7a8 Mon Sep 17 00:00:00 2001 From: PheonBest Date: Wed, 5 Apr 2023 21:21:09 +0200 Subject: [PATCH 1/3] :sparkles: Optimized Image vectors size --- .../java/checks/OptimizeImageVectorsSize.java | 140 ++++++++++++++++++ .../test/files/OptimizeImageVectorsSize.java | 38 +++++ .../checks/OptimizeImageVectorsSizeTest.java | 16 ++ 3 files changed, 194 insertions(+) create mode 100644 java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java create mode 100644 java-plugin/src/test/files/OptimizeImageVectorsSize.java create mode 100644 java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSizeTest.java diff --git a/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java b/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java new file mode 100644 index 000000000..d4f88b853 --- /dev/null +++ b/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java @@ -0,0 +1,140 @@ +package fr.greencodeinitiative.java.checks; + +import org.sonar.check.Priority; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.ForEachStatement; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Field; +import java.rmi.UnmarshalException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Rule( + key = "EC53", + name = "Developpement", + description = OptimizeImageVectorsSize.MESSAGERULE, + priority = Priority.MINOR, + tags = {"bug"}) +public class OptimizeImageVectorsSize extends IssuableSubscriptionVisitor { + protected final String SVG_BEGIN = ", . Redundant tags. Superfluous attributes (xmlns:dc, xmlns:cc, xmlns:rdv, xmlns: svg, xmlns, xmlns:sodipodi, xmlns:inkscape, most of id attributes, version, inkscape:version, inkscape:label, inkscape:groupmode, sodipodi:docname)"; + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.STRING_LITERAL); + } + + @Override + public void visitNode(Tree tree) { + + String value = ((LiteralTree) tree).value(); + // convert to lower case (not case sensitve anymore) + value = value.toLowerCase(); + // trim a beginning and ending double quote (") + value = value.replaceAll("^\"|\"$", ""); + // stop escaping double quotes + value = value.replaceAll("\\\\\"", "\"");; + + // search for svg beginning tag + int beginIndex = value.indexOf(SVG_BEGIN); + if (beginIndex < 0) { + // the string doesn't contain any svg + return; + } + + // Parse svg as xml and explore its tree + try { + // Build the doc from the XML file + InputSource source = new InputSource(new StringReader(value)); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + Document doc = db.parse(source); + + // the string is a valid xml file + + XPath xpath = XPathFactory.newInstance().newXPath(); + + // Note to developers: check xpath expressions by using xpather: http://xpather.com/ + + // Superfluous tags, e.g: , + if (((Double) xpath.evaluate("count(*//sodipodi:namedview)", + doc, XPathConstants.NUMBER)).intValue() > 0 || ((Double) xpath.evaluate("count(*//metadata)", + doc, XPathConstants.NUMBER)).intValue() > 0) { + reportIssue(tree, MESSAGERULE); + return; + } + + int nbId = ((Double) xpath.evaluate("count(//@id)", doc, XPathConstants.NUMBER)).intValue(); + int nbDistinctId = ((Double) xpath.evaluate("count(//@id[not(. = preceding::*/@id)])", doc, XPathConstants.NUMBER)).intValue(); + int nbHref = ((Double) xpath.evaluate("count(//@xlink:href)", doc, XPathConstants.NUMBER)).intValue(); + int nbDistinctHref = ((Double) xpath.evaluate("count(//@xlink:href[not(. = preceding::*/@xlink:href)])", doc, XPathConstants.NUMBER)).intValue(); + + // Duplicated tags (tags with the same xlink:href or same id attributes) + if (nbId != nbDistinctId || nbHref != nbDistinctHref) { + // count(//@id) returns the number of elements having an attribute "id" + // count(//@id[not(. = preceding::*/@id)]) returns the number of unique "id" attribute values + // if the number of "id" attributes and the number of unique "id" attributes values + // aren't equal, that means at least two elements have an "id" attribute. + reportIssue(tree, MESSAGERULE); + return; + } + + // The attribute value length can be reduced + if ((boolean) xpath.evaluate("boolean(//@id[string-length() > 3])", + doc, XPathConstants.BOOLEAN)) { + reportIssue(tree, MESSAGERULE); + return; + } + + // The attributes' value should be approximated (there are attributes with a numeric value that have more than 3 decimals) + // @* selects all the attributes of the document + // [number(.) = .] is a predicate that filters all attributes whose value can be converted to a numeric one. + if ((boolean) xpath.evaluate("boolean(//@*[number(.) = . and contains(., '.') and string-length(substring-after(., '.')) > 3])", doc, XPathConstants.BOOLEAN)) { + reportIssue(tree, MESSAGERULE); + return; + } + + // Avoid superfluous attributes + // TODO: make it work with namespaces (xmlns). + for (String superfluousAttribute : SUPERFLUOUS_ATTRIBUTES) { + String expression = String.format("boolean(//@*[name()=%s])\n", superfluousAttribute); + + if ((boolean) xpath.evaluate(expression, doc, XPathConstants.BOOLEAN)) { + reportIssue(tree, MESSAGERULE); + return; + } + } + } catch (ParserConfigurationException | IOException | SAXException | XPathExpressionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/java-plugin/src/test/files/OptimizeImageVectorsSize.java b/java-plugin/src/test/files/OptimizeImageVectorsSize.java new file mode 100644 index 000000000..4a981c773 --- /dev/null +++ b/java-plugin/src/test/files/OptimizeImageVectorsSize.java @@ -0,0 +1,38 @@ +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +class TestClass { + + TestClass(TestClass tc) { + } + + int compliantCase() { + return ""; + } + + int idCanBeReduced() { + return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + } + + int superfluousTag() { + return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + } + + int duplicatedTags() { + return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + } + + int unapproximatedNumericValues() { + return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + } + + int superfluousAttributes1() { + return "
"; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + } + + int superfluousAttributes2() { + return "
"; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + } + +} \ No newline at end of file diff --git a/java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSizeTest.java b/java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSizeTest.java new file mode 100644 index 000000000..dfa13f741 --- /dev/null +++ b/java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSizeTest.java @@ -0,0 +1,16 @@ +package fr.greencodeinitiative.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +class OptimizeImageVectorsSizeTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile("src/test/files/OptimizeImageVectorsSize.java") + .withCheck(new OptimizeImageVectorsSize()) + .verifyIssues(); + } + +} \ No newline at end of file From fed9970b062c0b193a53b13f0fe9763d64b589cd Mon Sep 17 00:00:00 2001 From: PheonBest Date: Wed, 5 Apr 2023 21:22:00 +0200 Subject: [PATCH 2/3] :bug: Fixed sonar container starting before db handles requests --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index bc48387aa..982501d8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: - "extensions:/opt/sonarqube/extensions" - "logs:/opt/sonarqube/logs" - "data:/opt/sonarqube/data" - + restart: unless-stopped db: image: postgres:12 container_name: postgresql_ecocode @@ -43,6 +43,7 @@ services: POSTGRES_PASSWORD: sonar POSTGRES_DB: sonarqube PGDATA: pg_data:/var/lib/postgresql/data/pgdata + restart: unless-stopped networks: sonarnet: From 5e56b8a333231952d0b44c72734e68e7f3f29c81 Mon Sep 17 00:00:00 2001 From: PheonBest Date: Thu, 6 Apr 2023 13:27:30 +0200 Subject: [PATCH 3/3] :bug: Fixed superfluous attributes detection, :sparkles: Analyzed text blocks --- .../java/checks/OptimizeImageVectorsSize.java | 65 ++++++++++++------- .../test/files/OptimizeImageVectorsSize.java | 30 ++++++--- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java b/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java index d4f88b853..7313252ac 100644 --- a/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java +++ b/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeImageVectorsSize.java @@ -3,12 +3,8 @@ import org.sonar.check.Priority; import org.sonar.check.Rule; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.tree.ForEachStatement; -import org.sonar.plugins.java.api.tree.LiteralTree; -import org.sonar.plugins.java.api.tree.Tree; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; +import org.sonar.plugins.java.api.tree.*; +import org.w3c.dom.*; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -33,6 +29,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; @Rule( key = "EC53", @@ -42,26 +39,35 @@ tags = {"bug"}) public class OptimizeImageVectorsSize extends IssuableSubscriptionVisitor { protected final String SVG_BEGIN = ""}; + protected final List SUPERFLUOUS_ATTRIBUTES = Arrays.asList("xmlns:dc", "xmlns:cc", "xmlns:rdv", "xmlns:svg", "xmlns", "xmlns:sodipodi", "xmlns:inkscape", "inkscape:version", "inkscape:label", "inkscape:groupmode", "sodipodi:docname"); protected static final String MESSAGERULE = "Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner"; // COMPLETE MESSAGE: // "Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner. The following should be avoided: The attribute value length can be reduced, e.g: id=\"filter4210\" to id=\"a\" (the same goes for xlink:href attribute). The attributes' value should be approximated: stdDeviation=\"0.57264819\" to stdDeviation=\"0.573\". Duplicated tags. Superfluous tags, e.g: , . Redundant tags. Superfluous attributes (xmlns:dc, xmlns:cc, xmlns:rdv, xmlns: svg, xmlns, xmlns:sodipodi, xmlns:inkscape, most of id attributes, version, inkscape:version, inkscape:label, inkscape:groupmode, sodipodi:docname)"; @Override public List nodesToVisit() { - return List.of(Tree.Kind.STRING_LITERAL); + return List.of(Tree.Kind.STRING_LITERAL, Tree.Kind.TEXT_BLOCK); } @Override public void visitNode(Tree tree) { - String value = ((LiteralTree) tree).value(); + // convert to lower case (not case sensitve anymore) value = value.toLowerCase(); - // trim a beginning and ending double quote (") - value = value.replaceAll("^\"|\"$", ""); - // stop escaping double quotes - value = value.replaceAll("\\\\\"", "\"");; + + // trim a beginning and ending double quotes (") and single quotes (') + if (tree.is(Tree.Kind.STRING_LITERAL)) { + value = value.substring(1, value.length() - 1); + } else { + // a text block begins and ends with 3 double quotes + value = value.substring(3, value.length() - 3); + } + + // stop escaping double quotes (") and single quotes (') + value = value.replaceAll("\\\\\"", "\""); + value = value.replaceAll("\\\\\'", "\'"); // search for svg beginning tag int beginIndex = value.indexOf(SVG_BEGIN); @@ -70,6 +76,17 @@ public void visitNode(Tree tree) { return; } + // search for svg ending tag + int endIndex = -1; + int j = 0; + while (j < SVG_END.length && endIndex == -1) { + endIndex = value.indexOf(SVG_END[j]); + ++j; + } + if (endIndex == -1) { + return; // svg is invalid or is broken into multiple strings + } + // Parse svg as xml and explore its tree try { // Build the doc from the XML file @@ -124,17 +141,21 @@ public void visitNode(Tree tree) { } // Avoid superfluous attributes - // TODO: make it work with namespaces (xmlns). - for (String superfluousAttribute : SUPERFLUOUS_ATTRIBUTES) { - String expression = String.format("boolean(//@*[name()=%s])\n", superfluousAttribute); - - if ((boolean) xpath.evaluate(expression, doc, XPathConstants.BOOLEAN)) { - reportIssue(tree, MESSAGERULE); - return; + // We do not search for the superfluous attributes directly in the xml as a string as the attributes could be the text inside of xml tags. + NodeList list = (NodeList) xpath.evaluate("//*", doc, XPathConstants.NODESET); + int nbNodes = list.getLength(); + for (int i = 0; i < nbNodes; ++i) { + NamedNodeMap attributes = list.item(i).getAttributes(); + int nbAttr = attributes.getLength(); + for (j = 0; j < nbAttr; ++j) { + if (SUPERFLUOUS_ATTRIBUTES.contains(((Attr) attributes.item(j)).getName())) { + reportIssue(tree, MESSAGERULE); + return; + } } } - } catch (ParserConfigurationException | IOException | SAXException | XPathExpressionException e) { - throw new RuntimeException(e); + + } catch (Throwable ignored) { } } } diff --git a/java-plugin/src/test/files/OptimizeImageVectorsSize.java b/java-plugin/src/test/files/OptimizeImageVectorsSize.java index 4a981c773..7a9445240 100644 --- a/java-plugin/src/test/files/OptimizeImageVectorsSize.java +++ b/java-plugin/src/test/files/OptimizeImageVectorsSize.java @@ -7,32 +7,46 @@ class TestClass { TestClass(TestClass tc) { } - int compliantCase() { + String compliantCase() { return ""; } - int idCanBeReduced() { + String idCanBeReduced() { return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} } - int superfluousTag() { + String superfluousTag() { return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} } - int duplicatedTags() { + String duplicatedTags() { return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} } - int unapproximatedNumericValues() { + String unapproximatedNumericValues() { return ""; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} } - int superfluousAttributes1() { - return "
"; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} + String validAttribute() { + return "
"; } - int superfluousAttributes2() { + String superfluousAttribute() { return "
"; // Noncompliant {{Optimize your svg by using open source tools such as SVGGO, Compressor.io or SVG Cleaner}} } + + String blockCompliantCase() { + return """ + + """; + } + + /* can't write test as all comments are included in the text block and blocks the xml analysis + String blockIdCanBeReduced() { + return """ + + """; + } + */ } \ No newline at end of file