Skip to content

Commit df3b2a2

Browse files
authored
Merge pull request #127 from green-code-initiative/ISSUE_73_GCI35
Issue 73 gci35
2 parents db2f53e + 8cca992 commit df3b2a2

9 files changed

Lines changed: 213 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- reduce noise on resource unit test files
1616
- [#111](https://github.com/green-code-initiative/creedengo-php/issues/111) refacto to have all the test files in the same place (for UT and IT), to avoid maintaining 2 test directories
1717
- [#111](https://github.com/green-code-initiative/creedengo-php/issues/111) refacto all test files to add sub-directories for each rule, to be more clear and to be able to add more tests for each rule in the future
18-
- [#126](https://github.com/green-code-initiative/creedengo-php/pull/126) Fix NPE on GCI2
18+
- [#126](https://github.com/green-code-initiative/creedengo-php/pull/126) Fix NPE on GCI2 and GCI35 + global NPE robustness for these 2 rules
1919

2020
### Deleted
2121

src/it/test-projects/creedengo-php-plugin-test-project/src/GCI2/AvoidMultipleIfElseStatement.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ public function shouldBeNotCompliantBecauseTheSameVariableIsUsedManyTimes() // N
451451
}
452452

453453
// COMPLIANT
454-
// previous NPE error in else case
454+
// (previously NPE error in else case)
455455
public function median(array $numbers)
456456
{
457457
sort($numbers);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
interface StatsContract
4+
{
5+
// No method body: covers METHOD_DECLARATION without BLOCK.
6+
public function compute(array $values);
7+
}
8+
9+
abstract class BaseStats
10+
{
11+
// No method body: covers METHOD_DECLARATION without BLOCK in abstract class.
12+
abstract protected function normalize(array $values);
13+
}
14+
15+
class AvoidMultipleIfElseStatementEdgeCases extends BaseStats implements StatsContract
16+
{
17+
public function compute(array $values)
18+
{
19+
// Early return keeps fixture simple and avoids duplicating main conditional chains.
20+
if (empty($values)) {
21+
return 0;
22+
}
23+
24+
$isReady = count($values) > 1;
25+
26+
// Core complementary scenario for GCI2: non-binary IF condition.
27+
// No variable is collected from condition, ELSE traversal must remain safe (no NPE).
28+
if ($isReady) {
29+
return $values[0];
30+
} else {
31+
return -1;
32+
}
33+
}
34+
35+
protected function normalize(array $values)
36+
{
37+
// Keep this method intentionally branch-free to avoid duplicating tested IF patterns.
38+
return $values;
39+
}
40+
}
41+
42+

src/it/test-projects/creedengo-php-plugin-test-project/src/GCI35/AvoidTryCatchWithFileOpenedCheck.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,21 @@
160160
} catch (\Exception $e) {
161161
echo $e->getMessage() . ' catch in\n';
162162
}
163+
164+
// COMPLIANT
165+
// (previously NPE)
166+
function vq_cache_set_page(string $identifiant, string $content, string $slug): void {
167+
if(!is_user_logged_in())
168+
{
169+
global $wpdb;
170+
171+
try
172+
{
173+
$wpdb->replace('commune_content', array('code_insee'=>$identifiant, 'content'=>$content, 'slug'=>$slug));
174+
}
175+
catch(Exception $e)
176+
{
177+
vq_log(level: 'warning', filename: basename(__FILE__), message: 'Failed to store commune page content in cache. ' . $e->getMessage());
178+
}
179+
}
180+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
class AvoidTryCatchWithFileOpenedCheckEdgeCases
4+
{
5+
public function shouldCoverCompliantAndNonCompliantBranches(bool $flag): void
6+
{
7+
try {
8+
echo strtoupper('hello');
9+
echo 42;
10+
} catch (\Exception $e) {
11+
echo $e->getMessage();
12+
}
13+
14+
try {
15+
$callable = 'fopen';
16+
echo $callable('myfile.txt', 'r');
17+
} catch (\Exception $e) {
18+
echo $e->getMessage();
19+
}
20+
21+
try {
22+
fOpen('myfile.txt', 'r'); // NOK {{Avoid the use of try-catch with a file open in try block}}
23+
} catch (\Exception $e) {
24+
echo $e->getMessage();
25+
}
26+
27+
try {
28+
// Function call not in the banned list: should stay compliant.
29+
parse_url('https://green-code-initiative.org');
30+
} catch (\Exception $e) {
31+
echo $e->getMessage();
32+
}
33+
34+
try {
35+
// Assignment whose value is not a function call: should be ignored by the rule.
36+
$result = 1;
37+
echo $result;
38+
} catch (\Exception $e) {
39+
echo $e->getMessage();
40+
}
41+
42+
try {
43+
if ($flag) {
44+
// RETURN_STATEMENT is intentionally not analyzed by GCI35 and should be ignored.
45+
return;
46+
}
47+
} catch (\Exception $e) {
48+
echo $e->getMessage();
49+
}
50+
}
51+
}
52+
53+

src/main/java/fr/greencodeinitiative/php/checks/GCI2AvoidMultipleIfElseStatementCheck.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.HashMap;
3232
import java.util.List;
3333
import java.util.Map;
34+
import java.util.Collections;
3435

3536
/**
3637
* FUNCTIONAL DESCRIPTION : please see ASCIIDOC description file of this rule (inside `creedengo-rules-spcifications`)
@@ -259,8 +260,6 @@ private void computeElseVariables(ElseClauseTree pElseTree, int pLevel) {
259260

260261
Map<String, Integer> currentIfVars = variablesStruct.getVariablesForCurrentIfStruct(pLevel);
261262

262-
if (currentIfVars == null) { return; }
263-
264263
for (Map.Entry<String, Integer> entry : currentIfVars.entrySet()) {
265264
String variableName = entry.getKey();
266265

@@ -338,7 +337,9 @@ private Integer internalGetVariableUsageOfNearestParent(Map<Integer, Map<String,
338337
Integer nbParentUsed = null;
339338
for (int i = pLevel; i >= 0 && nbParentUsed == null; i--) {
340339
Map<String, Integer> variablesParentLevelMap = pDataMap.get(i);
341-
nbParentUsed = variablesParentLevelMap.get(variableName);
340+
if (variablesParentLevelMap != null) {
341+
nbParentUsed = variablesParentLevelMap.get(variableName);
342+
}
342343
}
343344

344345
return nbParentUsed;
@@ -355,12 +356,7 @@ public void reinitVariableUsageForLevel(int pLevel) {
355356
* reinitialization of variable usages in input level in input map
356357
*/
357358
private void internalReinitVariableUsageForLevelForCurrentIfStruct(Map<Integer, Map<String, Integer>> pDataMap, int pLevel) {
358-
if (pDataMap.get(pLevel) == null) { return; }
359-
360-
// cleaning of current If Structure beginning at level specified
361-
for (int i = pLevel; i < pDataMap.size(); i++) {
362-
pDataMap.remove(i);
363-
}
359+
pDataMap.keySet().removeIf(level -> level >= pLevel);
364360

365361
}
366362

@@ -382,7 +378,7 @@ public void incrementVariableUsageForLevelForCurrentIfStruct(String variableName
382378
* get usage of a variable in a level on if/elseif map
383379
*/
384380
public Map<String, Integer> getVariablesForCurrentIfStruct(int pLevel) {
385-
return mapVariablesPerLevelForCurrentIfStruct.get(pLevel);
381+
return mapVariablesPerLevelForCurrentIfStruct.getOrDefault(pLevel, Collections.emptyMap());
386382
}
387383

388384
}

src/main/java/fr/greencodeinitiative/php/checks/GCI35AvoidTryCatchWithFileOpenedCheck.java

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -45,65 +45,105 @@ public List<Tree.Kind> nodesToVisit() {
4545
}
4646

4747
@Override
48-
public void visitNode(Tree tree) {
48+
public void visitNode(@SuppressWarnings("NullableProblems") Tree tree) {
4949

50-
TryStatementTree tryStatement = (TryStatementTree) tree;
50+
if (!(tree instanceof TryStatementTree tryStatement)) {
51+
return;
52+
}
5153

5254
// parcours des statements du bloc try
53-
visitStatementsList(tryStatement.block().statements());
55+
if (tryStatement.block() != null) {
56+
visitStatementsList(tryStatement.block().statements());
57+
}
5458

5559
}
5660

5761
private void visitStatementsList(List<StatementTree> lstStmts) {
62+
if (lstStmts == null || lstStmts.isEmpty()) {
63+
return;
64+
}
5865
for (StatementTree stmt : lstStmts){
66+
if (stmt == null) {
67+
continue;
68+
}
5969
Tree.Kind kind = stmt.getKind();
6070
switch (kind) {
6171
case EXPRESSION_STATEMENT -> visitExpressionStatement(((ExpressionStatementTree) stmt).expression());
62-
case BLOCK -> visitStatementsList(((BlockTree) stmt).statements());
72+
case BLOCK -> visitIfNotNullStatements(((BlockTree) stmt).statements());
6373
case IF_STATEMENT, ALTERNATIVE_IF_STATEMENT -> visitIfStatement((IfStatementTree) stmt);
64-
case FOR_STATEMENT, ALTERNATIVE_FOR_STATEMENT -> visitStatementsList(((ForStatementTree) stmt).statements());
65-
case WHILE_STATEMENT, ALTERNATIVE_WHILE_STATEMENT -> visitStatementsList(((WhileStatementTree) stmt).statements());
74+
case FOR_STATEMENT, ALTERNATIVE_FOR_STATEMENT -> visitIfNotNullStatements(((ForStatementTree) stmt).statements());
75+
case WHILE_STATEMENT, ALTERNATIVE_WHILE_STATEMENT -> visitIfNotNullStatements(((WhileStatementTree) stmt).statements());
6676
case DO_WHILE_STATEMENT -> visitStatementsList(Collections.singletonList(((DoWhileStatementTree) stmt).statement()));
67-
case FOREACH_STATEMENT, ALTERNATIVE_FOREACH_STATEMENT -> visitStatementsList(((ForEachStatementTree) stmt).statements());
68-
case CASE_CLAUSE -> visitStatementsList(((CaseClauseTree) stmt).statements());
77+
case FOREACH_STATEMENT, ALTERNATIVE_FOREACH_STATEMENT -> visitIfNotNullStatements(((ForEachStatementTree) stmt).statements());
78+
case CASE_CLAUSE -> visitIfNotNullStatements(((CaseClauseTree) stmt).statements());
6979
case SWITCH_STATEMENT -> visitSwitchStatement((SwitchStatementTree) stmt);
70-
case DEFAULT_CLAUSE -> visitStatementsList(((DefaultClauseTree) stmt).statements());
80+
case DEFAULT_CLAUSE -> visitIfNotNullStatements(((DefaultClauseTree) stmt).statements());
7181
case TRY_STATEMENT -> visitTryStatement((TryStatementTree) stmt);
82+
default -> {
83+
// Nothing to visit for unsupported statement kinds.
84+
}
7285
}
7386
}
7487
}
7588

89+
private void visitIfNotNullStatements(List<StatementTree> statements) {
90+
if (statements != null) {
91+
visitStatementsList(statements);
92+
}
93+
}
94+
7695
private void visitIfStatement(IfStatementTree ifStatement) {
96+
if (ifStatement == null) {
97+
return;
98+
}
7799
visitStatementsList(ifStatement.statements());
78100

79-
if (ifStatement.elseClause() != null) {
80-
visitStatementsList(ifStatement.elseClause().statements());
101+
ElseClauseTree elseClause = ifStatement.elseClause();
102+
if (elseClause != null) {
103+
visitIfNotNullStatements(elseClause.statements());
81104
}
82105

83-
if (ifStatement.elseifClauses() != null) {
84-
for (ElseifClauseTree stmtElseIf : ifStatement.elseifClauses()) {
85-
visitStatementsList(stmtElseIf.statements());
106+
List<ElseifClauseTree> elseIfClauses = ifStatement.elseifClauses();
107+
if (elseIfClauses != null) {
108+
for (ElseifClauseTree stmtElseIf : elseIfClauses) {
109+
if (stmtElseIf != null) {
110+
visitIfNotNullStatements(stmtElseIf.statements());
111+
}
86112
}
87113
}
88114
}
89115

90116
private void visitTryStatement(TryStatementTree tryStatement) {
117+
if (tryStatement == null) {
118+
return;
119+
}
91120
for (CatchBlockTree stmtCatch : tryStatement.catchBlocks()) {
92-
visitStatementsList(stmtCatch.block().statements());
121+
if (stmtCatch.block() != null) {
122+
visitIfNotNullStatements(stmtCatch.block().statements());
123+
}
93124
}
94125

95-
if (tryStatement.finallyBlock() != null) {
96-
visitStatementsList(tryStatement.finallyBlock().statements());
126+
var finallyBlock = tryStatement.finallyBlock();
127+
if (finallyBlock != null) {
128+
visitIfNotNullStatements(finallyBlock.statements());
97129
}
98130
}
99131

100132
private void visitSwitchStatement(SwitchStatementTree switchStatement) {
133+
if (switchStatement == null || switchStatement.cases() == null) {
134+
return;
135+
}
101136
for (SwitchCaseClauseTree switchCaseStmt : switchStatement.cases()) {
102-
visitStatementsList(switchCaseStmt.statements());
137+
if (switchCaseStmt != null && switchCaseStmt.statements() != null) {
138+
visitIfNotNullStatements(switchCaseStmt.statements());
139+
}
103140
}
104141
}
105142

106143
private void visitExpressionStatement(ExpressionTree exprTree){
144+
if (exprTree == null) {
145+
return;
146+
}
107147
if (exprTree.is(Tree.Kind.FUNCTION_CALL)) { // si directement "function call"
108148
visitCallExpression((FunctionCallTree) exprTree);
109149
} else if (exprTree.is(Tree.Kind.ASSIGNMENT)) { // ou si assignment
@@ -124,8 +164,10 @@ private void visitCallExpression(FunctionCallTree functionCall){
124164
}
125165

126166
private String getFunctionNameFromCallExpression(FunctionCallTree functionCall) {
127-
NamespaceNameTree nspTree = (NamespaceNameTree)functionCall.callee();
128-
return nspTree != null && nspTree.fullName() != null ? nspTree.fullName() : "";
167+
if (functionCall == null || !(functionCall.callee() instanceof NamespaceNameTree nspTree)) {
168+
return "";
169+
}
170+
return nspTree.fullName() != null ? nspTree.fullName() : "";
129171
}
130172

131173
}

src/test/java/fr/greencodeinitiative/php/checks/GCI2AvoidMultipleIfElseStatementCheckTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,22 @@
2525

2626
class GCI2AvoidMultipleIfElseStatementCheckTest {
2727

28+
// All GCI2 fixtures are grouped in this folder to keep each test focused on one use-case family.
29+
private static final String TESTFILES_PATH = System.getProperty("testfiles.path") + "/GCI2/";
30+
31+
@Test
32+
void shouldValidateAllMainUseCases() {
33+
// Covers the full official catalog of compliant/non-compliant IF/ELSEIF/ELSE chains,
34+
// including the historical median() scenario that previously triggered a NPE.
35+
PHPCheckTest.check(new GCI2AvoidMultipleIfElseStatementCheck(), new PhpTestFile(new File(TESTFILES_PATH + "AvoidMultipleIfElseStatement.php")));
36+
}
37+
2838
@Test
29-
void test() {
30-
PHPCheckTest.check(new GCI2AvoidMultipleIfElseStatementCheck(), new PhpTestFile(new File(System.getProperty("testfiles.path") + "/GCI2/AvoidMultipleIfElseStatement.php")));
39+
void shouldValidateEdgeCasesWithoutNpe() {
40+
// Covers complementary defensive paths (without duplicating main fixture use cases):
41+
// methods without body, non-binary conditions and safe ELSE traversal when no variable
42+
// has been collected for the current IF structure.
43+
PHPCheckTest.check(new GCI2AvoidMultipleIfElseStatementCheck(), new PhpTestFile(new File(TESTFILES_PATH + "AvoidMultipleIfElseStatementEdgeCases.php")));
3144
}
3245

3346
}

src/test/java/fr/greencodeinitiative/php/checks/GCI35AvoidTryCatchWithFileOpenedCheckTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,22 @@
2525

2626
class GCI35AvoidTryCatchWithFileOpenedCheckTest {
2727

28+
// All GCI35 fixtures are grouped in this folder to keep each test focused on one use-case family.
29+
private static final String TESTFILES_PATH = System.getProperty("testfiles.path") + "/GCI35/";
30+
31+
@Test
32+
void shouldValidateAllMainUseCases() {
33+
// Covers the official non-compliant catalog (fopen/readfile/PDF_OPEN in nested structures)
34+
// and a previously failing compliant scenario around try/catch traversal.
35+
PHPCheckTest.check(new GCI35AvoidTryCatchWithFileOpenedCheck(), new PhpTestFile(new File(TESTFILES_PATH + "AvoidTryCatchWithFileOpenedCheck.php")));
36+
}
37+
2838
@Test
29-
void test() {
30-
PHPCheckTest.check(new GCI35AvoidTryCatchWithFileOpenedCheck(), new PhpTestFile(new File(System.getProperty("testfiles.path") + "/GCI35/AvoidTryCatchWithFileOpenedCheck.php")));
39+
void shouldValidateEdgeCasesAndDefensiveBranches() {
40+
// Covers complementary paths (without duplicating main fixture scenarios):
41+
// case-insensitive match, dynamic callable (compliant), non-function assignment and
42+
// unsupported statement kinds that must be ignored.
43+
PHPCheckTest.check(new GCI35AvoidTryCatchWithFileOpenedCheck(), new PhpTestFile(new File(TESTFILES_PATH + "AvoidTryCatchWithFileOpenedCheckEdgeCases.php")));
3144
}
3245

3346
}

0 commit comments

Comments
 (0)