Skip to content

Commit 1d4832c

Browse files
committed
python: allow namespace packages as packages
remove the logic around isPotentialPackage
1 parent 362cf10 commit 1d4832c

File tree

2 files changed

+71
-30
lines changed
  • python/ql
    • lib/semmle/python
    • test/experimental/library-tests/CallGraph-implicit-init/not_root/baz/bar

2 files changed

+71
-30
lines changed

python/ql/lib/semmle/python/Module.qll

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -179,21 +179,6 @@ private predicate legalDottedName(string name) {
179179
bindingset[name]
180180
private predicate legalShortName(string name) { name.regexpMatch("(\\p{L}|_)(\\p{L}|\\d|_)*") }
181181

182-
/**
183-
* Holds if `f` is potentially a source package.
184-
* Does it have an __init__.py file (or --respect-init=False for Python 2) and is it within the source archive?
185-
*/
186-
private predicate isPotentialSourcePackage(Folder f) {
187-
f.getRelativePath() != "" and
188-
isPotentialPackage(f)
189-
}
190-
191-
private predicate isPotentialPackage(Folder f) {
192-
exists(f.getFile("__init__.py"))
193-
or
194-
py_flags_versioned("options.respect_init", "False", _) and major_version() = 2 and exists(f)
195-
}
196-
197182
private string moduleNameFromBase(Container file) {
198183
// We used to also require `isPotentialPackage(f)` to hold in this case,
199184
// but we saw modules not getting resolved because their folder did not
@@ -236,31 +221,87 @@ private predicate transitively_imported_from_entry_point(File file) {
236221
)
237222
}
238223

224+
private predicate isRegularPackage(Folder f, string name) {
225+
legalShortName(name) and
226+
name = f.getStem() and
227+
exists(f.getFile("__init__.py"))
228+
}
229+
230+
private predicate isPotentialModuleFile(File file, string name) {
231+
legalShortName(name) and
232+
name = file.getStem() and
233+
file.getExtension() = ["py", "pyc", "so", "pyd"] and
234+
// it has to be imported in this folder
235+
name =
236+
any(ImportExpr i | i.getLocation().getFile().getParent() = file.getParent())
237+
.getName()
238+
.regexpReplaceAll("\\..*", "") and
239+
name != ""
240+
}
241+
242+
// See https://peps.python.org/pep-0420/#specification
243+
private predicate isNameSpacePackage(Folder f, string name) {
244+
legalShortName(name) and
245+
name = f.getStem() and
246+
not isRegularPackage(f, name) and
247+
// it has to be imported in this folder
248+
name =
249+
any(ImportExpr i | i.getLocation().getFile().getParent() = f)
250+
.getName()
251+
.regexpReplaceAll("\\..*", "") and
252+
name != "" and
253+
// no siblibling regular package
254+
// no sibling module
255+
not exists(Folder sibling | sibling.getParent() = f.getParent() |
256+
isRegularPackage(sibling.getFolder(name), name)
257+
or
258+
isPotentialModuleFile(sibling.getAFile(), name)
259+
)
260+
}
261+
262+
private predicate isPackage(Folder f, string name) {
263+
isRegularPackage(f, name)
264+
or
265+
isNameSpacePackage(f, name)
266+
}
267+
268+
private predicate isModuleFile(File file, string name) {
269+
isPotentialModuleFile(file, name) and
270+
not isPackage(file.getParent(), _)
271+
}
272+
273+
private predicate isOutermostPackage(Folder f, string name) {
274+
isPackage(f, name) and
275+
not isPackage(f.getParent(), _)
276+
}
277+
239278
cached
240-
string moduleNameFromFile(Container file) {
279+
string moduleNameFromFile(Container c) {
280+
// package
281+
isOutermostPackage(c, result)
282+
or
283+
// module
284+
isModuleFile(c, result)
285+
or
241286
Stages::AST::ref() and
242287
exists(string basename |
243-
basename = moduleNameFromBase(file) and
288+
basename = moduleNameFromBase(c) and
244289
legalShortName(basename)
245290
|
246-
result = moduleNameFromFile(file.getParent()) + "." + basename
291+
// recursive case
292+
result = moduleNameFromFile(c.getParent()) + "." + basename
247293
or
248294
// If `file` is a transitive import of a file that's executed directly, we allow references
249295
// to it by its `basename`.
250-
transitively_imported_from_entry_point(file) and
296+
transitively_imported_from_entry_point(c) and
251297
result = basename
252298
)
253299
or
254-
isPotentialSourcePackage(file) and
255-
result = file.getStem() and
256-
(
257-
not isPotentialSourcePackage(file.getParent()) or
258-
not legalShortName(file.getParent().getBaseName())
259-
)
260-
or
261-
result = file.getStem() and file.getParent() = file.getImportRoot()
300+
//
301+
// standard library
302+
result = c.getStem() and c.getParent() = c.getImportRoot()
262303
or
263-
result = file.getStem() and isStubRoot(file.getParent())
304+
result = c.getStem() and isStubRoot(c.getParent())
264305
}
265306

266307
private predicate isStubRoot(Folder f) {

python/ql/test/experimental/library-tests/CallGraph-implicit-init/not_root/baz/bar/a.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ def afunc():
22
print("afunc called")
33
return 1
44

5-
from baz.foo import foo_func
6-
foo_func() # $ MISSING: pt,tt="baz/foo.py:foo_func"
5+
from not_root.baz.foo import foo_func
6+
foo_func() # $ pt,tt="not_root/baz/foo.py:foo_func"

0 commit comments

Comments
 (0)