Skip to content

Commit 4df13e2

Browse files
SONARPY-707 Add stubs for Django
1 parent 1cf38d4 commit 4df13e2

12 files changed

Lines changed: 77 additions & 26 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "python-frontend/src/main/resources/org/sonar/python/types/typeshed"]
55
path = python-frontend/src/main/resources/org/sonar/python/types/typeshed
66
url = https://github.com/python/typeshed.git
7+
[submodule "python-frontend/src/main/resources/org/sonar/python/types/django-stubs"]
8+
path = python-frontend/src/main/resources/org/sonar/python/types/django-stubs
9+
url = https://github.com/typeddjango/django-stubs.git
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
'project:django-2.2.3/django/http/response.py':[
3+
217,
4+
],
5+
}

its/ruling/src/test/resources/expected/python-S5713.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
'project:django-2.2.3/django/core/management/commands/loaddata.py':[
3+
188,
4+
],
25
'project:django-2.2.3/django/template/defaultfilters.py':[
36
816,
47
],

python-checks/src/main/java/org/sonar/python/checks/hotspots/CorsCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ && isString(subscripts.get(0), ALLOW_ORIGIN) && isString(assignment.assignedValu
147147
}
148148

149149
private static void checkDjangoResponseSetItem(SubscriptionContext ctx) {
150-
reportOnSetMethod(ctx, "django.http.HttpResponse.__setitem__");
150+
reportOnSetMethod(ctx, "django.http.response.HttpResponseBase.__setitem__");
151151
}
152152

153153
private static void reportOnSetMethod(SubscriptionContext ctx, String fqn) {

python-checks/src/main/java/org/sonar/python/checks/hotspots/HttpOnlyCookieCheck.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ String flagName() {
3535

3636
static {
3737
sensitiveArgumentByFQN = new HashMap<>();
38+
sensitiveArgumentByFQN.put("django.http.response.HttpResponseBase.set_cookie", 7);
39+
sensitiveArgumentByFQN.put("django.http.response.HttpResponseBase.set_signed_cookie", 8);
3840
sensitiveArgumentByFQN.put("django.http.HttpResponse.set_cookie", 7);
3941
sensitiveArgumentByFQN.put("django.http.HttpResponse.set_signed_cookie", 8);
4042
sensitiveArgumentByFQN.put("django.http.HttpResponseRedirect.set_cookie", 7);

python-checks/src/main/java/org/sonar/python/checks/hotspots/RegexCheck.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class RegexCheck extends PythonSubscriptionCheck {
4545
private static final String MESSAGE = "Make sure that using a regular expression is safe here.";
4646
private static final int REGEX_ARGUMENT = 0;
4747
private static final Set<String> questionableFunctions = new HashSet<>(Arrays.asList(
48-
"django.core.validators.RegexValidator", "django.urls.re_path",
48+
"django.core.validators.RegexValidator", "django.urls.conf.re_path",
4949
"re.compile", "re.match", "re.search", "re.fullmatch", "re.split", "re.findall", "re.finditer", "re.sub", "re.subn",
5050
"regex.compile", "regex.match", "regex.search", "regex.fullmatch", "regex.split", "regex.findall", "regex.finditer", "regex.sub", "regex.subn",
5151
"regex.subf", "regex.subfn", "regex.splititer"));

python-checks/src/main/java/org/sonar/python/checks/hotspots/SecureCookieCheck.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public class SecureCookieCheck extends AbstractCookieFlagCheck {
3030
private static Map<String, Integer> sensitiveArgumentByFQN;
3131
static {
3232
sensitiveArgumentByFQN = new HashMap<>();
33+
sensitiveArgumentByFQN.put("django.http.response.HttpResponseBase.set_cookie", 6);
34+
sensitiveArgumentByFQN.put("django.http.response.HttpResponseBase.set_signed_cookie", 7);
3335
sensitiveArgumentByFQN.put("django.http.HttpResponse.set_cookie", 6);
3436
sensitiveArgumentByFQN.put("django.http.HttpResponse.set_signed_cookie", 7);
3537
sensitiveArgumentByFQN.put("django.http.HttpResponseRedirect.set_cookie", 6);

python-frontend/src/main/java/org/sonar/python/semantic/SymbolUtils.java

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,6 @@
6262
public class SymbolUtils {
6363

6464
private static final String SEND_MESSAGE = "send_message";
65-
private static final String SET_COOKIE = "set_cookie";
66-
private static final String SET_SIGNED_COOKIE = "set_signed_cookie";
67-
private static final String EQ = "__eq__";
6865
private static final String SET_VERIFY = "set_verify";
6966

7067
private SymbolUtils() {
@@ -217,23 +214,6 @@ public static Map<String, Set<Symbol>> externalModulesSymbols() {
217214
classSymbol("SMTP_SSL", "smtplib.SMTP_SSL", "sendmail", SEND_MESSAGE)
218215
)));
219216

220-
globalSymbols.put("django.http", new HashSet<>(Arrays.asList(
221-
classSymbol("HttpResponse", "django.http.HttpResponse", SET_COOKIE, SET_SIGNED_COOKIE, "__setitem__"),
222-
classSymbol("HttpResponseRedirect", "django.http.HttpResponseRedirect", SET_COOKIE, SET_SIGNED_COOKIE),
223-
classSymbol("HttpResponsePermanentRedirect", "django.http.HttpResponsePermanentRedirect", SET_COOKIE, SET_SIGNED_COOKIE),
224-
classSymbol("HttpResponseNotModified", "django.http.HttpResponseNotModified", SET_COOKIE, SET_SIGNED_COOKIE),
225-
classSymbol("HttpResponseNotFound", "django.http.HttpResponseNotFound", SET_COOKIE, SET_SIGNED_COOKIE),
226-
classSymbol("HttpResponseForbidden", "django.http.HttpResponseForbidden", SET_COOKIE, SET_SIGNED_COOKIE),
227-
classSymbol("HttpResponseNotAllowed", "django.http.HttpResponseNotAllowed", SET_COOKIE, SET_SIGNED_COOKIE),
228-
classSymbol("HttpResponseGone", "django.http.HttpResponseGone", SET_COOKIE, SET_SIGNED_COOKIE),
229-
classSymbol("HttpResponseServerError", "django.http.HttpResponseServerError", SET_COOKIE, SET_SIGNED_COOKIE),
230-
classSymbol("HttpResponseBadRequest", "django.http.HttpResponseBadRequest", SET_COOKIE, SET_SIGNED_COOKIE)
231-
)));
232-
233-
globalSymbols.put("django.http.response", new HashSet<>(Collections.singleton(
234-
classSymbol("HttpResponse", "django.http.response.HttpResponse")
235-
)));
236-
237217
ClassSymbolImpl ldapObject = classSymbol("LDAPObject", "ldap.LDAPObject", "simple_bind", "simple_bind_s", "bind", "bind_s");
238218
FunctionSymbolImpl initialize = new FunctionSymbolImpl(
239219
"initialize", "ldap.initialize", false, false, false, Collections.emptyList(),Collections.emptyList());

python-frontend/src/main/java/org/sonar/python/types/TypeShed.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,19 @@ public class TypeShed {
6262
private static final Map<String, Set<Symbol>> typeShedSymbols = new HashMap<>();
6363
private static final Map<String, Set<Symbol>> builtinGlobalSymbols = new HashMap<>();
6464
private static final Set<String> modulesInProgress = new HashSet<>();
65+
private static final Map<String, String> externalModuleDefinitions = new HashMap<>();
6566

6667
private static final String STDLIB_2AND3 = "typeshed/stdlib/2and3/";
6768
private static final String STDLIB_2 = "typeshed/stdlib/2/";
6869
private static final String STDLIB_3 = "typeshed/stdlib/3/";
6970
private static final String THIRD_PARTY_2AND3 = "typeshed/third_party/2and3/";
7071
private static final String THIRD_PARTY_2 = "typeshed/third_party/2/";
7172
private static final String THIRD_PARTY_3 = "typeshed/third_party/3/";
73+
private static final String DJANGO = "django-stubs/django-stubs/";
74+
75+
static {
76+
externalModuleDefinitions.put("django", DJANGO);
77+
}
7278

7379
private TypeShed() {
7480
}
@@ -201,24 +207,60 @@ private static Set<Symbol> searchTypeShedForModule(String moduleName) {
201207
thirdPartySymbols = commonSymbols(getModuleSymbols(moduleName, THIRD_PARTY_2, builtinGlobalSymbols),
202208
getModuleSymbols(moduleName, THIRD_PARTY_3, builtinGlobalSymbols), moduleName);
203209
}
210+
String moduleCategoryPath = getNonTypeshedModuleCategoryPath(moduleName);
211+
if (thirdPartySymbols.isEmpty() && moduleCategoryPath != null) {
212+
thirdPartySymbols = new HashSet<>(getModuleSymbols(moduleName, moduleCategoryPath, builtinGlobalSymbols).values());
213+
}
204214
modulesInProgress.remove(moduleName);
205215
return thirdPartySymbols;
206216
}
207217

218+
private static @Nullable String getNonTypeshedModuleCategoryPath(String moduleToInspect) {
219+
String targetModule = moduleToInspect;
220+
int dotIndex = targetModule.indexOf('.');
221+
if (dotIndex > 0) {
222+
targetModule = targetModule.substring(0, dotIndex);
223+
}
224+
return externalModuleDefinitions.get(targetModule);
225+
}
226+
208227
@Nullable
209228
private static ModuleDescription getResourceForModule(String moduleName, String categoryPath) {
210-
String[] moduleNameHierarchy = moduleName.split("\\.");
229+
String computedModuleName = moduleName;
230+
String packageNamePrefix = "";
231+
if (categoryPath.equals(getNonTypeshedModuleCategoryPath(computedModuleName))) {
232+
// Non typeshed modules contain folder structure different from the one presented in typeshed
233+
// E.g. for django module the path is django-stubs/__init__.py
234+
// and for django.shortcuts module the path is django-stubs/shortcuts.py
235+
// In this case - we are dropping the first part of the module (e.g. django) and adding it back
236+
// when corresponding file was found
237+
int dotIndex = computedModuleName.indexOf('.');
238+
if (dotIndex > 0) {
239+
packageNamePrefix = computedModuleName.substring(0, dotIndex);
240+
computedModuleName = computedModuleName.substring(dotIndex + 1);
241+
} else {
242+
packageNamePrefix = computedModuleName;
243+
computedModuleName = "";
244+
}
245+
}
246+
String[] moduleNameHierarchy = computedModuleName.split("\\.");
211247
String pathToModule = String.join("/", moduleNameHierarchy);
212248
String moduleFileName = moduleNameHierarchy[moduleNameHierarchy.length - 1];
213249
String packageName = String.join(".", Arrays.copyOfRange(moduleNameHierarchy, 0, moduleNameHierarchy.length - 1));
214250
InputStream resource = TypeShed.class.getResourceAsStream(categoryPath + pathToModule + ".pyi");
215251
if (resource == null) {
216-
resource = TypeShed.class.getResourceAsStream(categoryPath + moduleName + "/__init__.pyi");
252+
resource = TypeShed.class.getResourceAsStream(categoryPath + computedModuleName + "/__init__.pyi");
217253
if (resource == null) {
218254
return null;
219255
}
220256
moduleFileName = "__init__";
221-
packageName = moduleName;
257+
packageName = computedModuleName;
258+
}
259+
// Adding the removed part of package name if there is any
260+
if (!packageNamePrefix.isEmpty() && packageName.isEmpty()) {
261+
packageName = packageNamePrefix;
262+
} else if (!packageNamePrefix.isEmpty()) {
263+
packageName = packageNamePrefix + "." + packageName;
222264
}
223265
return new ModuleDescription(resource, moduleFileName, packageName);
224266
}

0 commit comments

Comments
 (0)