followup #18362: make UnusedImport work robustly (#18366)

* warnDuplicateModuleImport => hintDuplicateModuleImport
* improve DuplicateModuleImport msg, add test
This commit is contained in:
Timothee Cour 2021-06-27 11:39:16 -07:00 • committed by GitHub
commit 0b7361e938
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 186 additions and 25 deletions

View file

@ -24,8 +24,6 @@ import strutils except `%` # collides with ropes.`%`
from ic / ic import ModuleBackendFlag from ic / ic import ModuleBackendFlag
from modulegraphs import ModuleGraph, PPassContext from modulegraphs import ModuleGraph, PPassContext
from lineinfos import
warnGcMem, errXMustBeCompileTime, hintDependency, errGenerated, errCannotOpenFile
import dynlib import dynlib
when not declared(dynlib.libCandidates): when not declared(dynlib.libCandidates):

View file

@ -29,7 +29,7 @@
## "A Graph–Free Approach to Data–Flow Analysis" by Markus Mohnen. ## "A Graph–Free Approach to Data–Flow Analysis" by Markus Mohnen.
## https://link.springer.com/content/pdf/10.1007/3-540-45937-5_6.pdf ## https://link.springer.com/content/pdf/10.1007/3-540-45937-5_6.pdf
import ast, types, intsets, lineinfos, renderer import ast, intsets, lineinfos, renderer
import std/private/asciitables import std/private/asciitables
type type

View file

@ -16,7 +16,7 @@ import
packages/docutils/rst, packages/docutils/rstgen, packages/docutils/rst, packages/docutils/rstgen,
json, xmltree, trees, types, json, xmltree, trees, types,
typesrenderer, astalgo, lineinfos, intsets, typesrenderer, astalgo, lineinfos, intsets,
pathutils, trees, tables, nimpaths, renderverbatim, osproc pathutils, tables, nimpaths, renderverbatim, osproc
from uri import encodeUrl from uri import encodeUrl
from std/private/globs import nativeToUnixPath from std/private/globs import nativeToUnixPath

View file

@ -12,7 +12,7 @@
import import
intsets, ast, astalgo, msgs, options, idents, lookups, intsets, ast, astalgo, msgs, options, idents, lookups,
semdata, modulepaths, sigmatch, lineinfos, sets, semdata, modulepaths, sigmatch, lineinfos, sets,
modulegraphs, wordrecg modulegraphs, wordrecg, tables
from strutils import `%` from strutils import `%`
proc readExceptSet*(c: PContext, n: PNode): IntSet = proc readExceptSet*(c: PContext, n: PNode): IntSet =
@ -239,6 +239,7 @@ proc importModuleAs(c: PContext; n: PNode, realModule: PSym, importHidden: bool)
if importHidden: if importHidden:
result.options.incl optImportHidden result.options.incl optImportHidden
c.unusedImports.add((result, n.info)) c.unusedImports.add((result, n.info))
c.importModuleMap[result.id] = realModule.id
proc transformImportAs(c: PContext; n: PNode): tuple[node: PNode, importHidden: bool] = proc transformImportAs(c: PContext; n: PNode): tuple[node: PNode, importHidden: bool] =
var ret: typeof(result) var ret: typeof(result)
@ -296,6 +297,13 @@ proc myImportModule(c: PContext, n: var PNode, importStmtResult: PNode): PSym =
importStmtResult.add newSymNode(result, n.info) importStmtResult.add newSymNode(result, n.info)
#newStrNode(toFullPath(c.config, f), n.info) #newStrNode(toFullPath(c.config, f), n.info)
proc afterImport(c: PContext, m: PSym) =
# fixes bug #17510, for re-exported symbols
let realModuleId = c.importModuleMap[m.id]
for s in allSyms(c.graph, m):
if s.owner.id != realModuleId:
c.exportIndirections.incl((m.id, s.id))
proc impMod(c: PContext; it: PNode; importStmtResult: PNode) = proc impMod(c: PContext; it: PNode; importStmtResult: PNode) =
var it = it var it = it
let m = myImportModule(c, it, importStmtResult) let m = myImportModule(c, it, importStmtResult)
@ -304,9 +312,7 @@ proc impMod(c: PContext; it: PNode; importStmtResult: PNode) =
addDecl(c, m, it.info) # add symbol to symbol table of module addDecl(c, m, it.info) # add symbol to symbol table of module
importAllSymbols(c, m) importAllSymbols(c, m)
#importForwarded(c, m.ast, emptySet, m) #importForwarded(c, m.ast, emptySet, m)
for s in allSyms(c.graph, m): # fixes bug #17510, for re-exported symbols afterImport(c, m)
if s.owner != m:
c.exportIndirections.incl((m.id, s.id))
proc evalImport*(c: PContext, n: PNode): PNode = proc evalImport*(c: PContext, n: PNode): PNode =
result = newNodeI(nkImportStmt, n.info) result = newNodeI(nkImportStmt, n.info)
@ -345,6 +351,7 @@ proc evalFrom*(c: PContext, n: PNode): PNode =
if n[i].kind != nkNilLit: if n[i].kind != nkNilLit:
importSymbol(c, n[i], m, im.imported) importSymbol(c, n[i], m, im.imported)
c.addImport im c.addImport im
afterImport(c, m)
proc evalImportExcept*(c: PContext, n: PNode): PNode = proc evalImportExcept*(c: PContext, n: PNode): PNode =
result = newNodeI(nkImportStmt, n.info) result = newNodeI(nkImportStmt, n.info)
@ -355,3 +362,4 @@ proc evalImportExcept*(c: PContext, n: PNode): PNode =
addDecl(c, m, n.info) # add symbol to symbol table of module addDecl(c, m, n.info) # add symbol to symbol table of module
importAllSymbolsExcept(c, m, readExceptSet(c, n)) importAllSymbolsExcept(c, m, readExceptSet(c, n))
#importForwarded(c, m.ast, exceptSet, m) #importForwarded(c, m.ast, exceptSet, m)
afterImport(c, m)

View file

@ -15,7 +15,7 @@
import import
intsets, strtabs, ast, astalgo, msgs, renderer, magicsys, types, idents, intsets, strtabs, ast, astalgo, msgs, renderer, magicsys, types, idents,
strutils, options, dfa, lowerings, tables, modulegraphs, msgs, strutils, options, dfa, lowerings, tables, modulegraphs,
lineinfos, parampatterns, sighashes, liftdestructors, optimizer, lineinfos, parampatterns, sighashes, liftdestructors, optimizer,
varpartitions varpartitions
@ -73,7 +73,7 @@ proc nestedScope(parent: var Scope): Scope =
proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode
proc moveOrCopy(dest, ri: PNode; c: var Con; s: var Scope; isDecl = false): PNode proc moveOrCopy(dest, ri: PNode; c: var Con; s: var Scope; isDecl = false): PNode
import sets, hashes, tables import sets, hashes
proc hash(n: PNode): Hash = hash(cast[pointer](n)) proc hash(n: PNode): Hash = hash(cast[pointer](n))
@ -245,8 +245,6 @@ template isUnpackedTuple(n: PNode): bool =
## hence unpacked tuples themselves don't need to be destroyed ## hence unpacked tuples themselves don't need to be destroyed
(n.kind == nkSym and n.sym.kind == skTemp and n.sym.typ.kind == tyTuple) (n.kind == nkSym and n.sym.kind == skTemp and n.sym.typ.kind == tyTuple)
from strutils import parseInt
proc checkForErrorPragma(c: Con; t: PType; ri: PNode; opname: string) = proc checkForErrorPragma(c: Con; t: PType; ri: PNode; opname: string) =
var m = "'" & opname & "' is not available for type <" & typeToString(t) & ">" var m = "'" & opname & "' is not available for type <" & typeToString(t) & ">"
if (opname == "=" or opname == "=copy") and ri != nil: if (opname == "=" or opname == "=copy") and ri != nil:

View file

@ -37,8 +37,6 @@ import
import json, sets, math, tables, intsets, strutils import json, sets, math, tables, intsets, strutils
from modulegraphs import ModuleGraph, PPassContext
type type
TJSGen = object of PPassContext TJSGen = object of PPassContext
module: PSym module: PSym

View file

@ -67,12 +67,12 @@ type
warnResultUsed = "ResultUsed", warnResultUsed = "ResultUsed",
warnCannotOpen = "CannotOpen", warnCannotOpen = "CannotOpen",
warnFileChanged = "FileChanged", warnFileChanged = "FileChanged",
warnDuplicateModuleImport = "DuplicateModuleImport",
warnUser = "User", warnUser = "User",
# hints # hints
hintSuccess = "Success", hintSuccessX = "SuccessX", hintSuccess = "Success", hintSuccessX = "SuccessX",
hintCC = "CC", hintCC = "CC",
hintLineTooLong = "LineTooLong", hintXDeclaredButNotUsed = "XDeclaredButNotUsed", hintLineTooLong = "LineTooLong",
hintXDeclaredButNotUsed = "XDeclaredButNotUsed", hintDuplicateModuleImport = "DuplicateModuleImport",
hintXCannotRaiseY = "XCannotRaiseY", hintConvToBaseNotNeeded = "ConvToBaseNotNeeded", hintXCannotRaiseY = "XCannotRaiseY", hintConvToBaseNotNeeded = "ConvToBaseNotNeeded",
hintConvFromXtoItselfNotNeeded = "ConvFromXtoItselfNotNeeded", hintExprAlwaysX = "ExprAlwaysX", hintConvFromXtoItselfNotNeeded = "ConvFromXtoItselfNotNeeded", hintExprAlwaysX = "ExprAlwaysX",
hintQuitCalled = "QuitCalled", hintProcessing = "Processing", hintCodeBegin = "CodeBegin", hintQuitCalled = "QuitCalled", hintProcessing = "Processing", hintCodeBegin = "CodeBegin",
@ -149,7 +149,6 @@ const
warnResultUsed: "used 'result' variable", warnResultUsed: "used 'result' variable",
warnCannotOpen: "cannot open: $1", warnCannotOpen: "cannot open: $1",
warnFileChanged: "file changed: $1", warnFileChanged: "file changed: $1",
warnDuplicateModuleImport: "$1",
warnUser: "$1", warnUser: "$1",
hintSuccess: "operation successful: $#", hintSuccess: "operation successful: $#",
# keep in sync with `testament.isSuccess` # keep in sync with `testament.isSuccess`
@ -157,6 +156,7 @@ const
hintCC: "CC: $1", hintCC: "CC: $1",
hintLineTooLong: "line too long", hintLineTooLong: "line too long",
hintXDeclaredButNotUsed: "'$1' is declared but not used", hintXDeclaredButNotUsed: "'$1' is declared but not used",
hintDuplicateModuleImport: "$1",
hintXCannotRaiseY: "$1", hintXCannotRaiseY: "$1",
hintConvToBaseNotNeeded: "conversion to base object is not needed", hintConvToBaseNotNeeded: "conversion to base object is not needed",
hintConvFromXtoItselfNotNeeded: "conversion from $1 to itself is pointless", hintConvFromXtoItselfNotNeeded: "conversion from $1 to itself is pointless",

View file

@ -300,11 +300,15 @@ proc wrongRedefinition*(c: PContext; info: TLineInfo, s: string;
proc addDeclAt*(c: PContext; scope: PScope, sym: PSym, info: TLineInfo) = proc addDeclAt*(c: PContext; scope: PScope, sym: PSym, info: TLineInfo) =
let conflict = scope.addUniqueSym(sym, onConflictKeepOld = true) let conflict = scope.addUniqueSym(sym, onConflictKeepOld = true)
if conflict != nil: if conflict != nil:
var note = errGenerated
if sym.kind == skModule and conflict.kind == skModule and sym.owner == conflict.owner: if sym.kind == skModule and conflict.kind == skModule and sym.owner == conflict.owner:
# import foo; import foo # e.g.: import foo; import foo
note = warnDuplicateModuleImport # xxx we could refine this by issuing a different hint for the case
wrongRedefinition(c, info, sym.name.s, conflict.info, note) # where a duplicate import happens inside an include.
localError(c.config, info, hintDuplicateModuleImport,
"duplicate import of '$1'; previous import here: $2" %
[sym.name.s, c.config $ conflict.info])
else:
wrongRedefinition(c, info, sym.name.s, conflict.info, errGenerated)
proc addDeclAt*(c: PContext; scope: PScope, sym: PSym) {.inline.} = proc addDeclAt*(c: PContext; scope: PScope, sym: PSym) {.inline.} =
addDeclAt(c, scope, sym, sym.info) addDeclAt(c, scope, sym, sym.info)

View file

@ -156,7 +156,8 @@ type
features*: set[Feature] features*: set[Feature]
inTypeContext*, inConceptDecl*: int inTypeContext*, inConceptDecl*: int
unusedImports*: seq[(PSym, TLineInfo)] unusedImports*: seq[(PSym, TLineInfo)]
exportIndirections*: HashSet[(int, int)] exportIndirections*: HashSet[(int, int)] # (module.id, symbol.id)
importModuleMap*: Table[int, int] # (module.id, module.id)
lastTLineInfo*: TLineInfo lastTLineInfo*: TLineInfo
template config*(c: PContext): ConfigRef = c.graph.config template config*(c: PContext): ConfigRef = c.graph.config

View file

@ -32,7 +32,7 @@
# included from sigmatch.nim # included from sigmatch.nim
import algorithm, sets, prefixmatches, lineinfos, parseutils, linter import algorithm, sets, prefixmatches, lineinfos, parseutils, linter, tables
from wordrecg import wDeprecated, wError, wAddr, wYield from wordrecg import wDeprecated, wError, wAddr, wYield
when defined(nimsuggest): when defined(nimsuggest):
@ -572,7 +572,8 @@ proc markOwnerModuleAsUsed(c: PContext; s: PSym) =
var i = 0 var i = 0
while i <= high(c.unusedImports): while i <= high(c.unusedImports):
let candidate = c.unusedImports[i][0] let candidate = c.unusedImports[i][0]
if candidate == module or c.exportIndirections.contains((candidate.id, s.id)): if candidate == module or c.importModuleMap.getOrDefault(candidate.id, int.low) == module.id or
c.exportIndirections.contains((candidate.id, s.id)):
# mark it as used: # mark it as used:
c.unusedImports.del(i) c.unusedImports.del(i)
else: else:

View file

@ -31,7 +31,7 @@ const
proc runNimCmd(file, options = "", rtarg = ""): auto = proc runNimCmd(file, options = "", rtarg = ""): auto =
let fileabs = testsDir / file.unixToNativePath let fileabs = testsDir / file.unixToNativePath
# doAssert fileabs.fileExists, fileabs # disabled because this allows passing `nim r --eval:code fakefile` # doAssert fileabs.fileExists, fileabs # disabled because this allows passing `nim r --eval:code fakefile`
let cmd = fmt"{nim} {mode} {options} --hints:off {fileabs} {rtarg}" let cmd = fmt"{nim} {mode} --hint:all:off {options} {fileabs} {rtarg}"
result = execCmdEx(cmd) result = execCmdEx(cmd)
when false: # for debugging when false: # for debugging
echo cmd echo cmd
@ -352,5 +352,29 @@ running: v2
doAssert outp2 == "12345\n", outp2 doAssert outp2 == "12345\n", outp2
doAssert status2 == 0 doAssert status2 == 0
block: # UnusedImport
proc fn(opt: string, expected: string) =
let output = runNimCmdChk("pragmas/mused3.nim", fmt"--warning:all:off --warning:UnusedImport --hint:DuplicateModuleImport {opt}")
doAssert output == expected, opt & "\noutput:\n" & output & "expected:\n" & expected
fn("-d:case1"): """
mused3.nim(13, 8) Warning: imported and not used: 'mused3b' [UnusedImport]
"""
fn("-d:case2"): ""
fn("-d:case3"): ""
fn("-d:case4"): ""
fn("-d:case5"): ""
fn("-d:case6"): ""
fn("-d:case7"): ""
fn("-d:case8"): ""
fn("-d:case9"): ""
fn("-d:case10"): ""
when false:
fn("-d:case11"): """
Warning: imported and not used: 'm2' [UnusedImport]
"""
fn("-d:case12"): """
mused3.nim(75, 10) Hint: duplicate import of 'mused3a'; previous import here: mused3.nim(74, 10) [DuplicateModuleImport]
"""
else: else:
discard # only during debugging, tests added here will run with `-d:nimTestsTrunnerDebugging` enabled discard # only during debugging, tests added here will run with `-d:nimTestsTrunnerDebugging` enabled

76
tests/pragmas/mused3.nim Normal file
View file

@ -0,0 +1,76 @@
#[
ran from trunner
]#
# line 10
when defined case1:
from mused3a import nil
from mused3b import nil
mused3a.fn1()
when defined case2:
from mused3a as m1 import nil
m1.fn1()
when defined case3:
from mused3a import fn1
fn1()
when defined case4:
from mused3a as m1 import fn1
m1.fn1()
when defined case5:
import mused3a as m1
fn1()
when defined case6:
import mused3a except nonexistent
fn1()
when defined case7:
import mused3a
mused3a.fn1()
when defined case8:
# re-export test
import mused3a except nonexistent
gn1()
when defined case9:
# re-export test
import mused3a
gn1()
when defined case10:
#[
edge case which happens a lot in compiler code:
don't report UnusedImport for mused3b here even though it works without `import mused3b`,
because `a.b0.f0` is accessible from both mused3a and mused3b (fields are given implicit access)
]#
import mused3a
import mused3b
var a: Bar
discard a.b0.f0
when false:
when defined case11:
#[
xxx minor bug: this should give:
Warning: imported and not used: 'm2' [UnusedImport]
but doesn't, because currently implementation in `markOwnerModuleAsUsed`
only looks at `fn1`, not fully qualified call `m1.fn1()
]#
from mused3a as m1 import nil
from mused3a as m2 import nil
m1.fn1()
when defined case12:
import mused3a
import mused3a
fn1()

41
tests/pragmas/mused3a.nim Normal file
View file

@ -0,0 +1,41 @@
when defined case1:
proc fn1*() = discard
when defined case2:
proc fn1*() = discard
when defined case3:
proc fn1*() = discard
proc fn2*() = discard
when defined case4:
proc fn1*() = discard
proc fn2*() = discard
when defined case5:
proc fn1*() = discard
when defined case6:
proc fn1*() = discard
when defined case7:
proc fn1*() = discard
when defined case8:
import mused3b
export mused3b
when defined case9:
import mused3b
export mused3b
when defined case10:
import mused3b
type Bar* = object
b0*: Foo
when defined case11:
proc fn1*() = discard
when defined case12:
proc fn1*() = discard

12
tests/pragmas/mused3b.nim Normal file
View file

@ -0,0 +1,12 @@
when defined case1:
proc gn1*()=discard
when defined case8:
proc gn1*()=discard
when defined case9:
proc gn1*()=discard
when defined case10:
type Foo* = object
f0*: int