Skip to content

Commit 939aff6

Browse files
thomas-serre-sonarsourcesonartech
authored andcommitted
SONARPY-3781 Detect package root from setup.py (#847)
GitOrigin-RevId: 549d33f6887bc6624be25eb09e74753d09095ebb
1 parent 516cedc commit 939aff6

4 files changed

Lines changed: 435 additions & 47 deletions

File tree

python-commons/src/main/java/org/sonar/plugins/python/indexer/BuildSystemSourceRoots.java renamed to python-commons/src/main/java/org/sonar/plugins/python/indexer/PyProjectTomlSourceRoots.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,15 @@
4444
* <li>Flit: auto-detects src/ layout by convention</li>
4545
* </ul>
4646
*/
47-
public class BuildSystemSourceRoots {
47+
public class PyProjectTomlSourceRoots {
4848

4949
private static final TomlMapper TOML_MAPPER = new TomlMapper();
5050

5151
static {
5252
TOML_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
5353
}
5454

55-
private BuildSystemSourceRoots() {
55+
private PyProjectTomlSourceRoots() {
5656
}
5757

5858
/**
@@ -149,7 +149,7 @@ private static List<String> extractFromHatchling(@Nullable Hatch hatch) {
149149
// Fall back to parsing directory from packages
150150
if (!wheel.packages().isEmpty()) {
151151
return wheel.packages().stream()
152-
.map(BuildSystemSourceRoots::extractDirectoryFromPackagePath)
152+
.map(PyProjectTomlSourceRoots::extractDirectoryFromPackagePath)
153153
.filter(dir -> dir != null && !dir.isEmpty())
154154
.distinct()
155155
.toList();
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python.indexer;
18+
19+
import com.sonar.sslr.api.AstNode;
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.LinkedHashSet;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Set;
26+
import javax.annotation.Nullable;
27+
import org.sonar.plugins.python.api.tree.AssignmentStatement;
28+
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
29+
import org.sonar.plugins.python.api.tree.CallExpression;
30+
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
31+
import org.sonar.plugins.python.api.tree.Expression;
32+
import org.sonar.plugins.python.api.tree.ExpressionList;
33+
import org.sonar.plugins.python.api.tree.FileInput;
34+
import org.sonar.plugins.python.api.tree.KeyValuePair;
35+
import org.sonar.plugins.python.api.tree.Name;
36+
import org.sonar.plugins.python.api.tree.RegularArgument;
37+
import org.sonar.plugins.python.api.tree.StringLiteral;
38+
import org.sonar.python.parser.PythonParser;
39+
import org.sonar.python.tree.PythonTreeMaker;
40+
import org.sonar.python.tree.TreeUtils;
41+
42+
/**
43+
* Extracts source root directories from setup.py files.
44+
*/
45+
public class SetupPySourceRoots {
46+
47+
private SetupPySourceRoots() {
48+
}
49+
50+
public static List<String> extract(String setupPyContent) {
51+
try {
52+
PythonParser parser = PythonParser.create();
53+
AstNode astNode = parser.parse(setupPyContent);
54+
PythonTreeMaker treeMaker = new PythonTreeMaker();
55+
FileInput fileInput = treeMaker.fileInput(astNode);
56+
57+
SetupCallVisitor visitor = new SetupCallVisitor();
58+
fileInput.accept(visitor);
59+
60+
return new ArrayList<>(visitor.sourceRoots);
61+
} catch (Exception e) {
62+
return List.of();
63+
}
64+
}
65+
66+
private static class SetupCallVisitor extends BaseTreeVisitor {
67+
private final Set<String> sourceRoots = new LinkedHashSet<>();
68+
private final Map<String, Expression> variables = new HashMap<>();
69+
70+
@Override
71+
public void visitAssignmentStatement(AssignmentStatement assignmentStatement) {
72+
ExpressionList lhs = assignmentStatement.lhsExpressions().get(0);
73+
if (lhs.expressions().size() == 1 && lhs.expressions().get(0) instanceof Name name) {
74+
variables.put(name.name(), assignmentStatement.assignedValue());
75+
}
76+
super.visitAssignmentStatement(assignmentStatement);
77+
}
78+
79+
@Override
80+
public void visitCallExpression(CallExpression callExpression) {
81+
Expression callee = callExpression.callee();
82+
83+
if (callee instanceof Name name && "setup".equals(name.name())) {
84+
RegularArgument packagesArgument = TreeUtils.argumentByKeyword("packages", callExpression.arguments());
85+
if (packagesArgument != null) {
86+
Expression packagesExpr = resolveExpression(packagesArgument.expression());
87+
if (packagesExpr instanceof CallExpression call) {
88+
extractFromFindPackages(call);
89+
}
90+
}
91+
92+
RegularArgument packageDirArgument = TreeUtils.argumentByKeyword("package_dir", callExpression.arguments());
93+
if (packageDirArgument != null) {
94+
Expression packageDirExpr = resolveExpression(packageDirArgument.expression());
95+
if (packageDirExpr instanceof DictionaryLiteral dictLiteral) {
96+
extractFromDictionary(dictLiteral);
97+
} else if (packageDirExpr instanceof CallExpression call) {
98+
extractFromFindPackages(call);
99+
}
100+
}
101+
}
102+
103+
super.visitCallExpression(callExpression);
104+
}
105+
106+
private void extractFromDictionary(DictionaryLiteral dictLiteral) {
107+
for (var element : dictLiteral.elements()) {
108+
if (element instanceof KeyValuePair keyValuePair) {
109+
Expression value = keyValuePair.value();
110+
String resolvedValue = resolveToString(value);
111+
if (resolvedValue != null && !resolvedValue.isEmpty()) {
112+
sourceRoots.add(resolvedValue);
113+
}
114+
}
115+
}
116+
}
117+
118+
private void extractFromFindPackages(CallExpression callExpression) {
119+
Expression callee = callExpression.callee();
120+
if (callee instanceof Name name && "find_packages".equals(name.name())) {
121+
RegularArgument whereArgument = TreeUtils.argumentByKeyword("where", callExpression.arguments());
122+
if(whereArgument!= null) {
123+
String whereValue = resolveToString(whereArgument.expression());
124+
if (whereValue != null && !whereValue.isEmpty()) {
125+
sourceRoots.add(whereValue);
126+
}
127+
}
128+
}
129+
}
130+
131+
@Nullable
132+
private String resolveToString(Expression expression) {
133+
Expression resolved = resolveExpression(expression);
134+
if (resolved instanceof StringLiteral stringLiteral) {
135+
return stringLiteral.trimmedQuotesValue();
136+
}
137+
return null;
138+
}
139+
140+
private Expression resolveExpression(Expression expression) {
141+
if (expression instanceof Name name) {
142+
Expression resolved = variables.get(name.name());
143+
if (resolved != null) {
144+
return resolveExpression(resolved);
145+
}
146+
}
147+
return expression;
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)