diff --git a/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java b/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java index 50ada18..b90a262 100644 --- a/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java +++ b/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java @@ -35,6 +35,7 @@ import fr.greencodeinitiative.java.checks.InitializeBufferWithAppropriateSize; import fr.greencodeinitiative.java.checks.NoFunctionCallWhenDeclaringForLoop; import fr.greencodeinitiative.java.checks.OptimizeReadFileExceptions; +import fr.greencodeinitiative.java.checks.UseEveryColumnQueried; import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; import org.sonarsource.api.sonarlint.SonarLintSide; @@ -62,7 +63,8 @@ public class JavaCheckRegistrar implements CheckRegistrar { InitializeBufferWithAppropriateSize.class, AvoidSetConstantInBatchUpdate.class, FreeResourcesOfAutoCloseableInterface.class, - AvoidMultipleIfElseStatement.class + AvoidMultipleIfElseStatement.class, + UseEveryColumnQueried.class ); /** diff --git a/src/main/java/fr/greencodeinitiative/java/checks/UseEveryColumnQueried.java b/src/main/java/fr/greencodeinitiative/java/checks/UseEveryColumnQueried.java new file mode 100644 index 0000000..7e85e11 --- /dev/null +++ b/src/main/java/fr/greencodeinitiative/java/checks/UseEveryColumnQueried.java @@ -0,0 +1,357 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Symbol.VariableSymbol; +import org.sonar.plugins.java.api.tree.Arguments; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.VariableTree; +import org.sonar.plugins.java.api.tree.Tree.Kind; + +@Rule(key = "EC1044") +public class UseEveryColumnQueried extends IssuableSubscriptionVisitor { + + protected static final String MESSAGERULE = "Avoid querying SQL columns that are not used"; + private static final String JAVA_SQL_STATEMENT = "java.sql.Statement"; + private static final String JAVA_SQL_RESULTSET = "java.sql.ResultSet"; + private static final MethodMatchers SQL_STATEMENT_DECLARE_SQL = MethodMatchers.create() + .ofSubTypes(JAVA_SQL_STATEMENT) + .names("executeQuery", "execute") + .addParametersMatcher("java.lang.String") + .build(); + private static final MethodMatchers SQL_STATEMENT_RETRIEVE_RESULTSET = MethodMatchers.create() + .ofSubTypes(JAVA_SQL_STATEMENT) + .names("executeQuery", "getResultSet") + .withAnyParameters() + .build(); + private static final MethodMatchers SQL_RESULTSET_GET_COLNAME = MethodMatchers.create() + .ofSubTypes(JAVA_SQL_RESULTSET) + .name(n -> n.startsWith("get")) + .addParametersMatcher("java.lang.String") + .build(); + private static final MethodMatchers SQL_RESULTSET_GET_COLID = MethodMatchers.create() + .ofSubTypes(JAVA_SQL_RESULTSET) + .name(n -> n.startsWith("get")) + .addParametersMatcher("int") + .build(); + private static final Pattern SELECTED_COLUMNS_PATTERN = Pattern.compile("SELECT\\s+(.*)\\s+FROM\\s+.*"); + + @Override + public List nodesToVisit() { + return Arrays.asList(Kind.METHOD_INVOCATION); + } + + /** + * How this rule works : + * We start from the method invocation that declares the SQL query ( stmt.executeQuery("SELECT ... FROM ...") ) + * the selected columns are directly extracted from the parameters of this method invocation + * We explore the stmt object to find where the method invocation that returns the ResultSet object + * finally we explore all invocations of this ResultSet object to list all the column used. + * the selected and used columns are compared, and an issue is reported if columns are selected but not used. + * + * + * stmt.execute("SELECT ... FROM ...") or stmt.executeQuery(...) + * | | + * | ----> Selected Columns + * [Statement Object] + * | + * | + * v + * res = stmt.getResultSet() or stmt.executeQuery(...) + * | + * | + * [ResultSet Object] + * | + * | + * v + * res.getInt(...) or any column extraction method + * | + * ----> Used Column + * + */ + @Override + public void visitNode(Tree tree) { + MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree; + if (!SQL_STATEMENT_DECLARE_SQL.matches(methodInvocationTree)) { + return; + } + + // extraction of the selected columns + List selectedColumns = getSelectedColumns(methodInvocationTree); + if (selectedColumns.isEmpty()) { + return; + } + + //if selected columns includes "*", stop the search + if (selectedColumns.contains("*")) { + return; + } + + // get the ResultSet object and check it's validity + Symbol resultSet = getResultSetNode(methodInvocationTree); + if (resultSet == null) { + return; + } + if(isResultSetInvalid(resultSet)){ + return; + } + + // extraction of the used columns + List usedColumns = getUsedColumns(resultSet, selectedColumns); + + // if there are selected columns that are not used in the code, report the issue + List differences = selectedColumns.stream() + .filter(element -> !usedColumns.contains(element)) + .collect(Collectors.toList()); + if (!differences.isEmpty()) { + reportIssue(methodInvocationTree, MESSAGERULE); + } + } + + private static List getSelectedColumns(MethodInvocationTree methodInvocationTree) { + // get the first argument of the query definition method + Arguments arguments = methodInvocationTree.arguments(); + if (arguments.isEmpty()) { + return new ArrayList<>(); + } + ExpressionTree argument = arguments.get(0); + // get the contents of the string in this first parameters + LiteralTree literal = extractLiteralFromVariable(argument); + if (literal == null) { + return new ArrayList<>(); + } + String query = literal.value(); + //get the list of selected columns from this string + return extractSelectedSQLColumns(query); + } + + /** + * returns a list of used columns from a resultset object and a list of the selected columns + */ + private static List getUsedColumns(Symbol resultSet, List selectedColumns) { + // iterate across all usages of the ResultSet + List usedColumns = new ArrayList<>(); + List resultSetUsages = resultSet.usages(); + for (IdentifierTree usage : resultSetUsages) { + // check this usage is an assignement, and the method parameters + if (usage.parent().is(Tree.Kind.ASSIGNMENT)) { + break; + } + MethodInvocationTree methodInvocation = getMethodInvocationFromTree(usage); + if (methodInvocation == null || methodInvocation.arguments().isEmpty()) { + continue; + } + // get the value of the first parameter + ExpressionTree parameter = methodInvocation.arguments().get(0); + LiteralTree columnGot = extractLiteralFromVariable(parameter); + if (columnGot == null) { + continue; + } + String column; + String value = columnGot.value(); + // if this first parameter is a string, clean up and use as is for used column name + if (SQL_RESULTSET_GET_COLNAME.matches(methodInvocation) && columnGot.is(Tree.Kind.STRING_LITERAL)) { + column = value.toUpperCase() + .replaceAll("^['\"]", "") + .replaceAll("['\"]$", ""); + // if this first parameter is an int, use as and id for used column name + } else if (SQL_RESULTSET_GET_COLID.matches(methodInvocation) && columnGot.is(Tree.Kind.INT_LITERAL)) { + int columnId = Integer.parseInt(value); + if (columnId > selectedColumns.size()) { + break; + } + column = selectedColumns.get(columnId - 1); + } else { + continue; + } + usedColumns.add(column); + } + return usedColumns; + } + + /** + * returns the litteral assigned to a variable var + * var has to be either a litteral itself or a final variable + * returns null if the ExpressionTree is not a variable, + * if the variable is not final, or if the variable has not been initialized + */ + @Nullable + private static LiteralTree extractLiteralFromVariable(ExpressionTree tree) { + if (tree instanceof LiteralTree) { + return (LiteralTree) tree; + } + if (!tree.is(Tree.Kind.IDENTIFIER)) { + return null; + } + IdentifierTree identifierTree = (IdentifierTree) tree; + Symbol symbol = identifierTree.symbol(); + if (symbol == null || !symbol.isFinal() || !symbol.isVariableSymbol()) { + return null; + } + VariableSymbol variableSymbol = (VariableSymbol) symbol; + Tree assignment = variableSymbol.declaration(); + if (!assignment.is(Tree.Kind.VARIABLE)) { + return null; + } + VariableTree variableTree = (VariableTree) assignment; + ExpressionTree initializer = variableTree.initializer(); + if (initializer instanceof LiteralTree) { + return (LiteralTree) initializer; + } + return null; + } + + /** + * get the ResultSet Object assigned from the result of the retrieve resultset method + * from the sql declaration method (via the shared stmt object) + * stmt.execute(...) -> rs = stmt.getResultSet() + */ + @Nullable + private static Symbol getResultSetNode(MethodInvocationTree methodInvocationTree) { + // get the Statement object on witch the method is called + ExpressionTree et = methodInvocationTree.methodSelect(); + if (!et.is(Tree.Kind.MEMBER_SELECT)) { + return null; + } + MemberSelectExpressionTree mset = (MemberSelectExpressionTree) et; + ExpressionTree expression = mset.expression(); + if (!expression.is(Tree.Kind.IDENTIFIER)) { + return null; + } + IdentifierTree id = (IdentifierTree) expression; + Symbol statement = id.symbol(); + if (statement == null) { + return null; + } + // iterate over all usages of this Statement object + List usages = statement.usages(); + Symbol resultSet = null; + for (IdentifierTree usage : usages) { + // does this usage of the Statement object match SQL_STATEMENT_RETRIEVE_RESULTSET ? + MethodInvocationTree methodInvocation = getMethodInvocationFromTree(usage); + if (methodInvocation == null || !SQL_STATEMENT_RETRIEVE_RESULTSET.matches(methodInvocation)) { + continue; + } + // if so end the search, we have found our resultSet object + Tree parent = methodInvocation.parent(); + if (parent.is(Tree.Kind.VARIABLE)) { + resultSet = ((VariableTree) parent).symbol(); + break; + } + } + return resultSet; + } + + /** + * unpacks a chain call to get the method invocation node + * example : this.object.chain.method() -> method() + */ + @Nullable + private static MethodInvocationTree getMethodInvocationFromTree(IdentifierTree tree) { + Tree parent = tree; + while (parent != null && !parent.is(Tree.Kind.METHOD_INVOCATION)) { + parent = parent.parent(); + } + return (MethodInvocationTree) parent; + } + + /** + * checks the two conditions that make a ResultSet object invalid, + * and would stop the search for used columns because of side effects + * - the ResultSet object being passed in a method + * - the ResultSet object being reassigned + */ + private static boolean isResultSetInvalid(Symbol resultSet) { + return isObjectUsedInMethodParameters(resultSet) + || isObjectReassigned(resultSet); + } + + /** + * checks if an object is used as a parameter a method + */ + private static boolean isObjectUsedInMethodParameters(Symbol obj) { + List usages = obj.usages(); + for (IdentifierTree usage : usages) { + Tree parent = usage.parent(); + if (parent.is(Tree.Kind.ARGUMENTS)) { + return true; + } + } + return false; + } + + /** + * checks if an object is reassigned + */ + private static boolean isObjectReassigned(Symbol obj) { + List usages = obj.usages(); + for (IdentifierTree usage : usages) { + Tree parent = usage.parent(); + if (parent.is(Tree.Kind.ASSIGNMENT)) { + AssignmentExpressionTree assignment = (AssignmentExpressionTree) parent; + ExpressionTree expressionTree = assignment.variable(); + if (expressionTree.is(Tree.Kind.IDENTIFIER) + && obj.equals(((IdentifierTree) expressionTree).symbol())) { + return true; + } + } + } + return false; + } + + /** + * extract from a SQL query in the form of "SELECT X, Y AS Z FROM TABLE ..." + * a list of all the column names and aliases (X and Z) without whitespace and in uppercase + */ + static List extractSelectedSQLColumns(String query) { + if (query == null) { + return new ArrayList<>(); + } + query = query.toUpperCase() + .replaceAll("^['\"]", "") + .replaceAll("['\"]$", ""); + List columns = new ArrayList<>(); + Matcher matcher = SELECTED_COLUMNS_PATTERN.matcher(query); + if (matcher.matches()) { + String columnString = matcher.group(1); + columns = Arrays.asList(columnString.split(",")); + columns.replaceAll(column -> column.replaceAll("\\s+", " ")); + columns.replaceAll(column -> column.contains(" AS ") ? column.split(" AS ")[1].trim() : column.trim()); + } + return columns; + } +} diff --git a/src/main/resources/fr/greencodeinitiative/java/ecoCode_way_profile.json b/src/main/resources/fr/greencodeinitiative/java/ecoCode_way_profile.json index 88e381a..b2f127c 100644 --- a/src/main/resources/fr/greencodeinitiative/java/ecoCode_way_profile.json +++ b/src/main/resources/fr/greencodeinitiative/java/ecoCode_way_profile.json @@ -16,6 +16,7 @@ "EC76", "EC77", "EC78", - "EC79" + "EC79", + "EC1044" ] } diff --git a/src/test/files/UseEveryColumnQueried/AttributeQueryCompliant.java b/src/test/files/UseEveryColumnQueried/AttributeQueryCompliant.java new file mode 100644 index 0000000..c12e375 --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/AttributeQueryCompliant.java @@ -0,0 +1,54 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * This is the nominal test case, where the SQL query is an attribute of the class. + * All Fields are accessed, so no issue is raised + */ +public class AttributeQueryCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT id, first, last, age FROM Registration"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(QUERY);) { + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", Age: " + rs.getInt("age")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/AttributeQueryNonCompliant.java b/src/test/files/UseEveryColumnQueried/AttributeQueryNonCompliant.java new file mode 100644 index 0000000..e42ff48 --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/AttributeQueryNonCompliant.java @@ -0,0 +1,53 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * This is the nominal test case, where the SQL query is an attribute of the class. + * One field is not accesed, so an issue is raised + */ +public class AttributeQueryNonCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT id, first, last, age FROM Registration"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(QUERY);) { // Noncompliant {{Avoid querying SQL columns that are not used}} + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/LitteralQueryCompliant.java b/src/test/files/UseEveryColumnQueried/LitteralQueryCompliant.java new file mode 100644 index 0000000..6146dfa --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/LitteralQueryCompliant.java @@ -0,0 +1,55 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, the query is a litteral string directly inserted in the method + * All Fields are accessed, so no issue is raised + */ +public class LitteralQueryCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, first, last, age FROM Registration");) { + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", Age: " + rs.getInt("age")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + +} diff --git a/src/test/files/UseEveryColumnQueried/LitteralQueryNonCompliant.java b/src/test/files/UseEveryColumnQueried/LitteralQueryNonCompliant.java new file mode 100644 index 0000000..476233d --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/LitteralQueryNonCompliant.java @@ -0,0 +1,54 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, the query is a litteral string directly inserted in the method + * One field is not accesed, so an issue is raised + */ +public class LitteralQueryNonCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, first, last, age FROM Registration");) { // Noncompliant {{Avoid querying SQL columns that are not used}} + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + +} \ No newline at end of file diff --git a/src/test/files/UseEveryColumnQueried/MultipleQueriesCompliant.java b/src/test/files/UseEveryColumnQueried/MultipleQueriesCompliant.java new file mode 100644 index 0000000..94f7a29 --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/MultipleQueriesCompliant.java @@ -0,0 +1,69 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, multiple queries are done using the same Statement Object. + * All Fields are accessed, so no issue is raised + */ +public class MultipleQueriesCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT id, first, last, age FROM Registration"; + private static final String QUERY2 = "SELECT id, first, last FROM Registration2"; + + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ) { + + ResultSet rs = stmt.executeQuery(QUERY); + while (rs.next()) { + // Display values + System.out.print("Age: " + rs.getInt("age")); + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + rs = stmt.executeQuery(QUERY2); + + + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + + + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/MultipleQueriesNonCompliant.java b/src/test/files/UseEveryColumnQueried/MultipleQueriesNonCompliant.java new file mode 100644 index 0000000..f7cf12c --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/MultipleQueriesNonCompliant.java @@ -0,0 +1,68 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, multiple queries are done using the same Statement Object. + * One field is not accesed, so an issue is raised + */ +public class MultipleQueriesNonCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT id, first, last, age FROM Registration"; + private static final String QUERY2 = "SELECT id, first, last, age FROM Registration2"; + + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ) { + + ResultSet rs = stmt.executeQuery(QUERY); // Noncompliant {{Avoid querying SQL columns that are not used}} + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + rs = stmt.executeQuery(QUERY2); + + while (rs.next()) { + // Display values + System.out.print("Age: " + rs.getInt("age")); + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + + + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/SelectStar.java b/src/test/files/UseEveryColumnQueried/SelectStar.java new file mode 100644 index 0000000..84fe348 --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/SelectStar.java @@ -0,0 +1,55 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, we use a "select * from" statement + * Since we can't know what columns exist, we can't know if all columns are being used, no issue is raised + * "select * from" is bad practice but is already covered by EC74 + */ +public class SelectStar { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT * FROM Registration"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(QUERY);) { + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", Age: " + rs.getInt("age")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesCompliant.java b/src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesCompliant.java new file mode 100644 index 0000000..03ac224 --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesCompliant.java @@ -0,0 +1,56 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, columns are accesed by IDs and names, some of them being in final variables + * All Fields are accessed, so no issue is raised + */ +public class UseColumnIdsAndNameAttributesCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT id, first, last, age FROM Registration"; + private static final String ID = "id"; + private static final int AGE = 4; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(QUERY);) { + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt(ID)); + System.out.print(", Age: " + rs.getInt(AGE)); + System.out.print(", First: " + rs.getString(2)); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesNonCompliant.java b/src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesNonCompliant.java new file mode 100644 index 0000000..c7972d4 --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesNonCompliant.java @@ -0,0 +1,54 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, columns are accesed by IDs and names, some of them being in final variables + * One field is not accesed, so an issue is raised + */ +public class UseColumnIdsAndNameAttributesNonCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + private static final String QUERY = "SELECT id, first, last, age FROM Registration"; + private static final String ID = "id"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(QUERY);) { // Noncompliant {{Avoid querying SQL columns that are not used}} + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt(ID)); + System.out.print(", First: " + rs.getString(2)); + System.out.println(", Last: " + rs.getString("last")); + } + } catch (SQLException e) { + e.printStackTrace(); + } + + } +} diff --git a/src/test/files/UseEveryColumnQueried/UseMethodCompliant.java b/src/test/files/UseEveryColumnQueried/UseMethodCompliant.java new file mode 100644 index 0000000..db7ae3e --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/UseMethodCompliant.java @@ -0,0 +1,57 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, the ResultSet is passed through a method + * All Fields are accessed, so no issue is raised + */ +public class UseMethodCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, first, last, age FROM Registration");) { + extractGet(rs); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + private void extractGet(ResultSet rs) throws SQLException { + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", Age: " + rs.getInt("age")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } +} diff --git a/src/test/files/UseEveryColumnQueried/UseMethodNonCompliant.java b/src/test/files/UseEveryColumnQueried/UseMethodNonCompliant.java new file mode 100644 index 0000000..559c06b --- /dev/null +++ b/src/test/files/UseEveryColumnQueried/UseMethodNonCompliant.java @@ -0,0 +1,56 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * In this test case, the ResultSet is passed through a method + * One field is not accesed, so an issue is raised + */ +public class UseMethodNonCompliant { + + private static final String DB_URL = "jdbc:mysql://localhost/TEST"; + private static final String USER = "guest"; + private static final String PASS = "guest123"; + + public void callJdbc() { + + try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id, first, last, age FROM Registration");) { // Noncompliant {{Avoid querying SQL columns that are not used}} + extractGet(rs); + } catch (SQLException e) { + e.printStackTrace(); + } + + } + + private void extractGet(ResultSet rs) throws SQLException { + while (rs.next()) { + // Display values + System.out.print("ID: " + rs.getInt("id")); + System.out.print(", First: " + rs.getString("first")); + System.out.println(", Last: " + rs.getString("last")); + } + } +} diff --git a/src/test/java/fr/greencodeinitiative/java/checks/UseEveryColumnQueriedTest.java b/src/test/java/fr/greencodeinitiative/java/checks/UseEveryColumnQueriedTest.java new file mode 100644 index 0000000..edf2b76 --- /dev/null +++ b/src/test/java/fr/greencodeinitiative/java/checks/UseEveryColumnQueriedTest.java @@ -0,0 +1,131 @@ +/* + * ecoCode - Java language - Provides rules to reduce the environmental footprint of your Java programs + * Copyright © 2023 Green Code Initiative (https://www.ecocode.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package fr.greencodeinitiative.java.checks; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +class UseEveryColumnQueriedTest { + + @Test + void testExtractSelectedSQLColumns(){ + String query = "\"SELECT id AS registration_id,\tfirst, last as Final, AGE FROM Registration\""; + List columns = UseEveryColumnQueried.extractSelectedSQLColumns(query); + assertEquals(4, columns.size()); + assertEquals("REGISTRATION_ID", columns.get(0)); + assertEquals("FIRST", columns.get(1)); + assertEquals("FINAL", columns.get(2)); + assertEquals("AGE", columns.get(3)); + } + + @Test + void testHasIssues1() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/AttributeQueryNonCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyIssues(); + } + + @Test + void testHasIssues2() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/LitteralQueryNonCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyIssues(); + } + + @Test + void testHasIssues3() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesNonCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyIssues(); + } + + @Test + @Disabled // case not handled (multiple queries with the same ResultSet object) + void testHasIssues4() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/MultipleQueriesNonCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyIssues(); + } + + @Test + @Disabled // case not handled (usage of a method) + void testHasIssues5() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/UseMethodNonCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyIssues(); + } + + + @Test + void testHasNoIssues1() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/AttributeQueryCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyNoIssues(); + } + + @Test + void testHasNoIssues2() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/LitteralQueryCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyNoIssues(); + } + + @Test + void testHasNoIssues3() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/UseColumnIdsAndNameAttributesCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyNoIssues(); + } + + @Test + void testHasNoIssues4() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/MultipleQueriesCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyNoIssues(); + } + + @Test + void testHasNoIssues5() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/UseMethodCompliant.java") + .withCheck(new UseEveryColumnQueried()) + .verifyNoIssues(); + } + + @Test + void testHasNoIssues6() { + CheckVerifier.newVerifier() + .onFile("src/test/files/UseEveryColumnQueried/SelectStar.java") + .withCheck(new UseEveryColumnQueried()) + .verifyNoIssues(); + } +}