Skip to content

Commit 2ce0852

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3557 Add unknown symbol telemetry (#696)
GitOrigin-RevId: 39df2526d06c9895bf29c3dd44bfb84bb80e23fc
1 parent 7ead3ec commit 2ce0852

File tree

6 files changed

+443
-1
lines changed

6 files changed

+443
-1
lines changed

python-commons/src/main/java/org/sonar/plugins/python/PythonScanner.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class PythonScanner extends Scanner {
7979
private final MeasuresRepository measuresRepository;
8080
private final NoSonarLineInfoCollector noSonarLineInfoCollector;
8181
private final Lock lock;
82+
private final TypeInferenceTelemetryCollector typeInferenceTelemetryCollector;
8283

8384
public PythonScanner(
8485
SensorContext context, PythonChecks checks, FileLinesContextFactory fileLinesContextFactory, NoSonarFilter noSonarFilter,
@@ -100,6 +101,7 @@ public PythonScanner(
100101
this.pythonHighlighter = new PythonHighlighter(lock);
101102
this.issuesRepository = new IssuesRepository(context, checks, indexer, isInSonarLint(context), lock);
102103
this.measuresRepository = new MeasuresRepository(context, noSonarFilter, fileLinesContextFactory, isInSonarLint(context), noSonarLineInfoCollector, lock);
104+
this.typeInferenceTelemetryCollector = new TypeInferenceTelemetryCollector();
103105
}
104106

105107
@Override
@@ -138,6 +140,7 @@ protected void scanFile(PythonInputFile inputFile) throws IOException {
138140
if (visitorContext.rootTree() != null && !isInSonarLint(context)) {
139141
newSymbolsCollector.collect(context.newSymbolTable().onFile(inputFile.wrappedFile()), visitorContext.rootTree());
140142
pythonHighlighter.highlight(context, visitorContext, inputFile);
143+
typeInferenceTelemetryCollector.collect(visitorContext.rootTree());
141144
}
142145

143146
searchForDataBricks(visitorContext);
@@ -355,6 +358,10 @@ public boolean getFoundDatabricks() {
355358
return foundDatabricks.get();
356359
}
357360

361+
public TypeInferenceTelemetry getTypeInferenceTelemetry() {
362+
return typeInferenceTelemetryCollector.getTelemetry();
363+
}
364+
358365
private void runLockedByRepository(String repositoryKey, Runnable runnable) {
359366
var repositoryLock = repositoryLocks.computeIfAbsent(repositoryKey, k -> new ReentrantLock());
360367
try {

python-commons/src/main/java/org/sonar/plugins/python/PythonSensor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ public void execute(SensorContext context) {
153153
Duration sensorTime = Duration.between(sensorStartTime, Instant.now());
154154

155155
updateDatabricksTelemetry(scanner);
156+
updateTypeInferenceTelemetry(scanner);
156157
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOSONAR_RULE_ID_KEY, noSonarLineInfoCollector.getSuppressedRuleIds());
157158
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.NOSONAR_COMMENTS_KEY, noSonarLineInfoCollector.getCommentWithExactlyOneRuleSuppressed());
158159
updateNamespacePackageTelemetry(pythonIndexer);
@@ -181,6 +182,17 @@ private void updateDatabricksTelemetry(PythonScanner scanner) {
181182
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_DATABRICKS_FOUND, scanner.getFoundDatabricks());
182183
}
183184

185+
private void updateTypeInferenceTelemetry(PythonScanner scanner) {
186+
TypeInferenceTelemetry telemetry = scanner.getTypeInferenceTelemetry();
187+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_NAMES_TOTAL, telemetry.totalNames());
188+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_NAMES_UNKNOWN, telemetry.unknownTypeNames());
189+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_NAMES_UNRESOLVED_IMPORT, telemetry.unresolvedImportTypeNames());
190+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_IMPORTS_TOTAL, telemetry.totalImports());
191+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_IMPORTS_UNKNOWN, telemetry.importsWithUnknownType());
192+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_SYMBOLS_UNIQUE, telemetry.uniqueSymbols());
193+
sensorTelemetryStorage.updateMetric(TelemetryMetricKey.PYTHON_TYPES_SYMBOLS_UNKNOWN, telemetry.unknownSymbols());
194+
}
195+
184196
private void updateNamespacePackageTelemetry(PythonIndexer pythonIndexer) {
185197
NamespacePackageTelemetry telemetry = pythonIndexer.namespacePackageTelemetry();
186198
if (telemetry != null) {

python-commons/src/main/java/org/sonar/plugins/python/TelemetryMetricKey.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ public enum TelemetryMetricKey {
4040
PYTHON_PACKAGES_WITH_INIT("python.packages.with_init"),
4141
PYTHON_PACKAGES_WITHOUT_INIT("python.packages.without_init"),
4242
PYTHON_DUPLICATE_PACKAGES_WITHOUT_INIT("python.packages.duplicate_without_init"),
43-
PYTHON_NAMESPACE_PACKAGES_IN_REGULAR_PACKAGE("python.packages.namespace_packages_in_regular_package");
43+
PYTHON_NAMESPACE_PACKAGES_IN_REGULAR_PACKAGE("python.packages.namespace_packages_in_regular_package"),
44+
PYTHON_TYPES_NAMES_TOTAL("python.types.names.total"),
45+
PYTHON_TYPES_NAMES_UNKNOWN("python.types.names.unknown"),
46+
PYTHON_TYPES_NAMES_UNRESOLVED_IMPORT("python.types.names.unresolved_import"),
47+
PYTHON_TYPES_IMPORTS_TOTAL("python.types.imports.total"),
48+
PYTHON_TYPES_IMPORTS_UNKNOWN("python.types.imports.unknown"),
49+
PYTHON_TYPES_SYMBOLS_UNIQUE("python.types.symbols.unique"),
50+
PYTHON_TYPES_SYMBOLS_UNKNOWN("python.types.symbols.unknown");
4451

4552
private final String key;
4653

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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;
18+
19+
/**
20+
* Telemetry data for type inference quality metrics.
21+
*
22+
* @param totalNames Total number of Name nodes in all analyzed trees
23+
* @param unknownTypeNames Number of Names with the UNKNOWN type
24+
* @param unresolvedImportTypeNames Number of Names with an UnresolvedImportType
25+
* @param totalImports Total number of imported items (from both import and from...import statements)
26+
* @param importsWithUnknownType Number of imports that resolve to UNKNOWN or UnresolvedImportType
27+
* @param uniqueSymbols Number of unique symbols (using symbolsV2)
28+
* @param unknownSymbols Number of unique symbols which are unknown
29+
*/
30+
public record TypeInferenceTelemetry(
31+
long totalNames,
32+
long unknownTypeNames,
33+
long unresolvedImportTypeNames,
34+
long totalImports,
35+
long importsWithUnknownType,
36+
long uniqueSymbols,
37+
long unknownSymbols) {
38+
39+
public static TypeInferenceTelemetry empty() {
40+
return new TypeInferenceTelemetry(0, 0, 0, 0, 0, 0, 0);
41+
}
42+
43+
public TypeInferenceTelemetry add(TypeInferenceTelemetry other) {
44+
return new TypeInferenceTelemetry(
45+
this.totalNames + other.totalNames,
46+
this.unknownTypeNames + other.unknownTypeNames,
47+
this.unresolvedImportTypeNames + other.unresolvedImportTypeNames,
48+
this.totalImports + other.totalImports,
49+
this.importsWithUnknownType + other.importsWithUnknownType,
50+
this.uniqueSymbols + other.uniqueSymbols,
51+
this.unknownSymbols + other.unknownSymbols
52+
);
53+
}
54+
}
55+
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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;
18+
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
import java.util.concurrent.atomic.AtomicLong;
22+
import org.sonar.plugins.python.api.tree.AliasedName;
23+
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
24+
import org.sonar.plugins.python.api.tree.FileInput;
25+
import org.sonar.plugins.python.api.tree.ImportFrom;
26+
import org.sonar.plugins.python.api.tree.ImportName;
27+
import org.sonar.plugins.python.api.tree.Name;
28+
import org.sonar.plugins.python.api.types.v2.PythonType;
29+
import org.sonar.plugins.python.api.types.v2.UnknownType;
30+
import org.sonar.python.semantic.v2.SymbolV2;
31+
import org.sonar.python.semantic.v2.UsageV2;
32+
33+
/**
34+
* Collects telemetry metrics about type inference quality.
35+
* Must be called after type inference has run on each file.
36+
*/
37+
public class TypeInferenceTelemetryCollector {
38+
39+
private final AtomicLong totalNames = new AtomicLong(0);
40+
private final AtomicLong unknownTypeNames = new AtomicLong(0);
41+
private final AtomicLong unresolvedImportTypeNames = new AtomicLong(0);
42+
private final AtomicLong totalImports = new AtomicLong(0);
43+
private final AtomicLong importsWithUnknownType = new AtomicLong(0);
44+
private final AtomicLong uniqueSymbols = new AtomicLong(0);
45+
private final AtomicLong unknownSymbols = new AtomicLong(0);
46+
47+
public void collect(FileInput rootTree) {
48+
var visitor = new CollectorVisitor();
49+
rootTree.accept(visitor);
50+
aggregateMetrics(visitor);
51+
}
52+
53+
private synchronized void aggregateMetrics(CollectorVisitor visitor) {
54+
totalNames.addAndGet(visitor.totalNames);
55+
unknownTypeNames.addAndGet(visitor.unknownTypeNames);
56+
unresolvedImportTypeNames.addAndGet(visitor.unresolvedImportTypeNames);
57+
totalImports.addAndGet(visitor.totalImports);
58+
importsWithUnknownType.addAndGet(visitor.importsWithUnknownType);
59+
uniqueSymbols.addAndGet(visitor.uniqueSymbols.size());
60+
unknownSymbols.addAndGet(visitor.unknownSymbols.size());
61+
}
62+
63+
public TypeInferenceTelemetry getTelemetry() {
64+
return new TypeInferenceTelemetry(
65+
totalNames.get(),
66+
unknownTypeNames.get(),
67+
unresolvedImportTypeNames.get(),
68+
totalImports.get(),
69+
importsWithUnknownType.get(),
70+
uniqueSymbols.get(),
71+
unknownSymbols.get()
72+
);
73+
}
74+
75+
private static class CollectorVisitor extends BaseTreeVisitor {
76+
long totalNames = 0;
77+
long unknownTypeNames = 0;
78+
long unresolvedImportTypeNames = 0;
79+
long totalImports = 0;
80+
long importsWithUnknownType = 0;
81+
Set<SymbolV2> uniqueSymbols = new HashSet<>();
82+
Set<SymbolV2> unknownSymbols = new HashSet<>();
83+
84+
@Override
85+
public void visitName(Name name) {
86+
totalNames++;
87+
88+
PythonType type = name.typeV2();
89+
if (type == PythonType.UNKNOWN) {
90+
unknownTypeNames++;
91+
} else if (type instanceof UnknownType.UnresolvedImportType) {
92+
unresolvedImportTypeNames++;
93+
}
94+
95+
SymbolV2 symbol = name.symbolV2();
96+
if (symbol != null) {
97+
uniqueSymbols.add(symbol);
98+
if (isUnknownSymbol(symbol)) {
99+
unknownSymbols.add(symbol);
100+
}
101+
}
102+
103+
super.visitName(name);
104+
}
105+
106+
@Override
107+
public void visitImportName(ImportName importName) {
108+
for (AliasedName aliasedName : importName.modules()) {
109+
totalImports++;
110+
if (hasUnknownImportType(aliasedName)) {
111+
importsWithUnknownType++;
112+
}
113+
}
114+
super.visitImportName(importName);
115+
}
116+
117+
@Override
118+
public void visitImportFrom(ImportFrom importFrom) {
119+
for (AliasedName aliasedName : importFrom.importedNames()) {
120+
totalImports++;
121+
if (hasUnknownImportType(aliasedName)) {
122+
importsWithUnknownType++;
123+
}
124+
}
125+
super.visitImportFrom(importFrom);
126+
}
127+
128+
private static boolean hasUnknownImportType(AliasedName aliasedName) {
129+
Name nameToCheck = aliasedName.alias();
130+
if (nameToCheck == null) {
131+
var names = aliasedName.dottedName().names();
132+
if (!names.isEmpty()) {
133+
nameToCheck = names.get(names.size() - 1);
134+
}
135+
}
136+
if (nameToCheck == null) {
137+
return false;
138+
}
139+
return isUnknownType(nameToCheck.typeV2());
140+
}
141+
142+
private static boolean isUnknownType(PythonType type) {
143+
return type == PythonType.UNKNOWN || type instanceof UnknownType.UnresolvedImportType;
144+
}
145+
146+
private static boolean isUnknownSymbol(SymbolV2 symbol) {
147+
// A symbol is considered unknown if all its binding usages have unknown types
148+
return symbol.usages().stream()
149+
.filter(UsageV2::isBindingUsage)
150+
.allMatch(usage -> {
151+
if (usage.tree() instanceof Name name) {
152+
return isUnknownType(name.typeV2());
153+
}
154+
return true;
155+
});
156+
}
157+
}
158+
}
159+

0 commit comments

Comments
 (0)