Skip to content

Commit 4a4c0f6

Browse files
committed
Teach the "unused variable / value" diagnostics to use SwiftIfConfig
Rather than walking into the inactive regions of IfConfigDecls looking for references to a declaration before we diagnose it, go to the syntax tree and look through inactive *and unparsed* regions for identifier tokens that match. If we find one, suppress the diagnostic. This reduces our dependency on IfConfigDecl in the AST, and also makes the same suppression work with code in unparsed regions that had no representation in IfConfigDecl.
1 parent 66dba4a commit 4a4c0f6

File tree

3 files changed

+152
-57
lines changed

3 files changed

+152
-57
lines changed

lib/ASTGen/Sources/ASTGen/CompilerBuildConfiguration.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,83 @@ public func freeConfiguredRegions(
334334
UnsafeMutableBufferPointer(start: regions, count: numRegions).deallocate()
335335
}
336336

337+
/// Cache used when checking for inactive code that might contain a reference
338+
/// to specific names.
339+
private struct InactiveCodeContainsReferenceCache {
340+
let syntax: SourceFileSyntax
341+
let configuredRegions: ConfiguredRegions
342+
}
343+
344+
/// Determine whether the inactive code within the given search range
345+
/// contains a token referring to the given name.
346+
@_cdecl("swift_ASTGen_inactiveCodeContainsReference")
347+
public func inactiveCodeContainsReference(
348+
astContext: BridgedASTContext,
349+
sourceFileBuffer: BridgedStringRef,
350+
searchRange: BridgedStringRef,
351+
bridgedName: BridgedStringRef,
352+
untypedCachePtr: UnsafeMutablePointer<UnsafeMutableRawPointer?>
353+
) -> Bool {
354+
let syntax: SourceFileSyntax
355+
let configuredRegions: ConfiguredRegions
356+
if let untypedCachePtr = untypedCachePtr.pointee {
357+
// Use the cache.
358+
let cache = untypedCachePtr.assumingMemoryBound(to: InactiveCodeContainsReferenceCache.self)
359+
syntax = cache.pointee.syntax
360+
configuredRegions = cache.pointee.configuredRegions
361+
} else {
362+
// Parse the region.
363+
364+
// FIXME: Use 'ExportedSourceFile' when C++ parser is replaced.
365+
let searchRangeBuffer = UnsafeBufferPointer<UInt8>(start: searchRange.data, count: searchRange.count)
366+
syntax = Parser.parse(source: searchRangeBuffer)
367+
368+
// Compute the configured regions within the search range.
369+
let configuration = CompilerBuildConfiguration(
370+
ctx: astContext,
371+
sourceBuffer: searchRangeBuffer
372+
)
373+
configuredRegions = syntax.configuredRegions(in: configuration)
374+
375+
let cache = UnsafeMutablePointer<InactiveCodeContainsReferenceCache>.allocate(capacity: 1)
376+
cache.initialize(to: .init(syntax: syntax, configuredRegions: configuredRegions))
377+
378+
untypedCachePtr.pointee = .init(cache)
379+
}
380+
381+
// If there are no regions, everything is active. This is the common case.
382+
if configuredRegions.isEmpty {
383+
return false
384+
}
385+
386+
// Walk all of the tokens in the search range looking for one that references
387+
// the given name.
388+
let name = String(bridged: bridgedName)
389+
for token in syntax.tokens(viewMode: .sourceAccurate) {
390+
guard let identifier = token.identifier else {
391+
continue
392+
}
393+
394+
if identifier.name == name && configuredRegions.isActive(token) != .active {
395+
// Found it in a non-active region.
396+
return true
397+
}
398+
}
399+
400+
return false
401+
}
402+
403+
@_cdecl("swift_ASTGen_freeInactiveCodeContainsReferenceCache")
404+
public func freeInactiveCodeContainsReferenceCache(pointer: UnsafeMutableRawPointer?) {
405+
guard let pointer else {
406+
return
407+
}
408+
409+
let cache = pointer.assumingMemoryBound(to: InactiveCodeContainsReferenceCache.self)
410+
cache.deinitialize(count: 1)
411+
cache.deallocate()
412+
}
413+
337414
/// Evaluate the #if condition at ifClauseLocationPtr.
338415
@_cdecl("swift_ASTGen_evaluatePoundIfCondition")
339416
public func evaluatePoundIfCondition(

lib/Sema/MiscDiagnostics.cpp

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "TypeCheckConcurrency.h"
2020
#include "TypeCheckInvertible.h"
2121
#include "TypeChecker.h"
22+
#include "swift/AST/ASTBridging.h"
2223
#include "swift/AST/ASTWalker.h"
2324
#include "swift/AST/ConformanceLookup.h"
2425
#include "swift/AST/DiagnosticsSema.h"
@@ -3160,6 +3161,9 @@ class VarDeclUsageChecker : public ASTWalker {
31603161
DeclContext *DC;
31613162

31623163
DiagnosticEngine &Diags;
3164+
3165+
SourceRange FullRange;
3166+
31633167
// Keep track of some information about a variable.
31643168
enum {
31653169
RK_Defined = 1, ///< Whether it was ever defined in this scope.
@@ -3195,8 +3199,9 @@ class VarDeclUsageChecker : public ASTWalker {
31953199
void operator=(const VarDeclUsageChecker &) = delete;
31963200

31973201
public:
3198-
VarDeclUsageChecker(DeclContext *DC,
3199-
DiagnosticEngine &Diags) : DC(DC), Diags(Diags) {}
3202+
VarDeclUsageChecker(DeclContext *DC, DiagnosticEngine &Diags,
3203+
SourceRange range)
3204+
: DC(DC), Diags(Diags), FullRange(range) {}
32003205

32013206
// After we have scanned the entire region, diagnose variables that could be
32023207
// declared with a narrower usage kind.
@@ -3266,11 +3271,6 @@ class VarDeclUsageChecker : public ASTWalker {
32663271
if (isa<TypeDecl>(D))
32673272
return Action::SkipNode();
32683273

3269-
// The body of #if clauses are not walked into, we need custom processing
3270-
// for them.
3271-
if (auto *ICD = dyn_cast<IfConfigDecl>(D))
3272-
handleIfConfig(ICD);
3273-
32743274
// If this is a VarDecl, then add it to our list of things to track.
32753275
if (auto *vd = dyn_cast<VarDecl>(D)) {
32763276
if (shouldTrackVarDecl(vd)) {
@@ -3354,9 +3354,6 @@ class VarDeclUsageChecker : public ASTWalker {
33543354
/// The heavy lifting happens when visiting expressions.
33553355
PreWalkResult<Expr *> walkToExprPre(Expr *E) override;
33563356

3357-
/// handle #if directives.
3358-
void handleIfConfig(IfConfigDecl *ICD);
3359-
33603357
/// Custom handling for statements.
33613358
PreWalkResult<Stmt *> walkToStmtPre(Stmt *S) override {
33623359
// Keep track of an association between vardecls and the StmtCondition that
@@ -3864,6 +3861,16 @@ SourceLoc swift::getFixItLocForVarToLet(VarDecl *var) {
38643861
return SourceLoc();
38653862
}
38663863

3864+
extern "C" bool swift_ASTGen_inactiveCodeContainsReference(
3865+
BridgedASTContext ctx,
3866+
BridgedStringRef sourceFileBuffer,
3867+
BridgedStringRef searchRange,
3868+
BridgedStringRef name,
3869+
void **cache
3870+
);
3871+
3872+
extern "C" void swift_ASTGen_freeInactiveCodeContainsReferenceCache(void *cache);
3873+
38673874
// After we have scanned the entire region, diagnose variables that could be
38683875
// declared with a narrower usage kind.
38693876
VarDeclUsageChecker::~VarDeclUsageChecker() {
@@ -3873,6 +3880,35 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
38733880
if (sawError)
38743881
return;
38753882

3883+
/// Check whether the given variable might have been referenced from an
3884+
/// inactive region of the source code.
3885+
void *usedInInactiveCache = nullptr;
3886+
auto isUsedInInactive = [&](VarDecl *var) -> bool {
3887+
#if SWIFT_BUILD_SWIFT_SYNTAX
3888+
// Extract the full buffer containing the source range.
3889+
ASTContext &ctx = DC->getASTContext();
3890+
SourceManager &sourceMgr = ctx.SourceMgr;
3891+
auto bufferID = sourceMgr.findBufferContainingLoc(FullRange.Start);
3892+
StringRef sourceFileText = sourceMgr.getEntireTextForBuffer(bufferID);
3893+
3894+
// Extract the search text from that buffer.
3895+
auto searchTextCharRange = Lexer::getCharSourceRangeFromSourceRange(
3896+
sourceMgr, FullRange);
3897+
StringRef searchText = sourceMgr.extractText(searchTextCharRange, bufferID);
3898+
3899+
return swift_ASTGen_inactiveCodeContainsReference(
3900+
ctx, sourceFileText, searchText, var->getName().str(),
3901+
&usedInInactiveCache);
3902+
#else
3903+
return false;
3904+
#endif
3905+
};
3906+
SWIFT_DEFER {
3907+
#if SWIFT_BUILD_SWIFT_SYNTAX
3908+
swift_ASTGen_freeInactiveCodeContainsReferenceCache(usedInInactiveCache);
3909+
#endif
3910+
};
3911+
38763912
for (auto p : VarDecls) {
38773913
VarDecl *var;
38783914
unsigned access;
@@ -3907,7 +3943,7 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
39073943
auto VD = dyn_cast<VarDecl>(FD->getStorage());
39083944
if ((access & RK_Read) == 0) {
39093945
auto found = AssociatedGetterRefExpr.find(VD);
3910-
if (found != AssociatedGetterRefExpr.end()) {
3946+
if (found != AssociatedGetterRefExpr.end() && !isUsedInInactive(VD)) {
39113947
auto *DRE = found->second;
39123948
Diags.diagnose(DRE->getLoc(), diag::unused_setter_parameter,
39133949
var->getName());
@@ -3941,6 +3977,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
39413977
// produce a fixit hint with a parent map, but this is a lot of effort for
39423978
// a narrow case.
39433979
if (access & RK_CaptureList) {
3980+
if (isUsedInInactive(var))
3981+
continue;
3982+
39443983
Diags.diagnose(var->getLoc(), diag::capture_never_used,
39453984
var->getName());
39463985
continue;
@@ -3954,6 +3993,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
39543993
if (auto *pbd = var->getParentPatternBinding()) {
39553994
if (pbd->getSingleVar() == var && pbd->getInit(0) != nullptr &&
39563995
!isa<TypedPattern>(pbd->getPattern(0))) {
3996+
if (isUsedInInactive(var))
3997+
continue;
3998+
39573999
unsigned varKind = var->isLet();
39584000
SourceRange replaceRange(
39594001
pbd->getStartLoc(),
@@ -3984,6 +4026,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
39844026
if (isa<NamedPattern>(LP->getSubPattern())) {
39854027
auto initExpr = SC->getCond()[0].getInitializer();
39864028
if (initExpr->getStartLoc().isValid()) {
4029+
if (isUsedInInactive(var))
4030+
continue;
4031+
39874032
unsigned noParens = initExpr->canAppendPostfixExpression();
39884033

39894034
// If the subexpr is an "as?" cast, we can rewrite it to
@@ -4051,6 +4096,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
40514096
});
40524097

40534098
if (foundVP) {
4099+
if (isUsedInInactive(var))
4100+
continue;
4101+
40544102
unsigned varKind = var->isLet();
40554103
Diags
40564104
.diagnose(var->getLoc(), diag::variable_never_used,
@@ -4062,6 +4110,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
40624110

40634111
// Otherwise, this is something more complex, perhaps
40644112
// let (a,b) = foo()
4113+
if (isUsedInInactive(var))
4114+
continue;
4115+
40654116
if (isWrittenLet) {
40664117
Diags.diagnose(var->getLoc(),
40674118
diag::immutable_value_never_used_but_assigned,
@@ -4085,6 +4136,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
40854136
!isVarDeclPartOfPBDThatHadSomeMutation(var)) {
40864137
SourceLoc FixItLoc = getFixItLocForVarToLet(var);
40874138

4139+
if (isUsedInInactive(var))
4140+
continue;
4141+
40884142
// If this is a parameter explicitly marked 'var', remove it.
40894143
if (FixItLoc.isInvalid()) {
40904144
Diags.diagnose(var->getLoc(), diag::variable_never_mutated,
@@ -4113,6 +4167,9 @@ VarDeclUsageChecker::~VarDeclUsageChecker() {
41134167

41144168
// If this is a variable that was only written to, emit a warning.
41154169
if ((access & RK_Read) == 0) {
4170+
if (isUsedInInactive(var))
4171+
continue;
4172+
41164173
Diags.diagnose(var->getLoc(), diag::variable_never_read, var->getName());
41174174
continue;
41184175
}
@@ -4334,56 +4391,12 @@ ASTWalker::PreWalkResult<Expr *> VarDeclUsageChecker::walkToExprPre(Expr *E) {
43344391
return Action::Continue(E);
43354392
}
43364393

4337-
/// handle #if directives. All of the active clauses are already walked by the
4338-
/// AST walker, but we also want to handle the inactive ones to avoid false
4339-
/// positives.
4340-
void VarDeclUsageChecker::handleIfConfig(IfConfigDecl *ICD) {
4341-
struct ConservativeDeclMarker : public ASTWalker {
4342-
VarDeclUsageChecker &VDUC;
4343-
SourceFile *SF;
4344-
4345-
ConservativeDeclMarker(VarDeclUsageChecker &VDUC)
4346-
: VDUC(VDUC), SF(VDUC.DC->getParentSourceFile()) {}
4347-
4348-
MacroWalking getMacroWalkingBehavior() const override {
4349-
return MacroWalking::Arguments;
4350-
}
4351-
4352-
PostWalkResult<Expr *> walkToExprPost(Expr *E) override {
4353-
// If we see a bound reference to a decl in an inactive #if block, then
4354-
// conservatively mark it read and written. This will silence "variable
4355-
// unused" and "could be marked let" warnings for it.
4356-
if (auto *DRE = dyn_cast<DeclRefExpr>(E))
4357-
VDUC.addMark(DRE->getDecl(), RK_Read | RK_Written);
4358-
else if (auto *declRef = dyn_cast<UnresolvedDeclRefExpr>(E)) {
4359-
auto name = declRef->getName();
4360-
auto loc = declRef->getLoc();
4361-
if (name.isSimpleName() && loc.isValid()) {
4362-
auto *varDecl = dyn_cast_or_null<VarDecl>(
4363-
ASTScope::lookupSingleLocalDecl(SF, name.getFullName(), loc));
4364-
if (varDecl)
4365-
VDUC.addMark(varDecl, RK_Read|RK_Written);
4366-
}
4367-
}
4368-
return Action::Continue(E);
4369-
}
4370-
};
4371-
4372-
for (auto &clause : ICD->getClauses()) {
4373-
// Active clauses are handled by the normal AST walk.
4374-
if (clause.isActive) continue;
4375-
4376-
for (auto elt : clause.Elements)
4377-
elt.walk(ConservativeDeclMarker(*this));
4378-
}
4379-
}
4380-
43814394
/// Apply the warnings managed by VarDeclUsageChecker to the top level
43824395
/// code declarations that haven't been checked yet.
43834396
void swift::
43844397
performTopLevelDeclDiagnostics(TopLevelCodeDecl *TLCD) {
43854398
auto &ctx = TLCD->getDeclContext()->getASTContext();
4386-
VarDeclUsageChecker checker(TLCD, ctx.Diags);
4399+
VarDeclUsageChecker checker(TLCD, ctx.Diags, TLCD->getSourceRange());
43874400
TLCD->walk(checker);
43884401
}
43894402

@@ -4398,7 +4411,7 @@ void swift::performAbstractFuncDeclDiagnostics(AbstractFunctionDecl *AFD) {
43984411
// declared as constants. Skip local functions though, since they will
43994412
// be checked as part of their parent function or TopLevelCodeDecl.
44004413
auto &ctx = AFD->getDeclContext()->getASTContext();
4401-
VarDeclUsageChecker checker(AFD, ctx.Diags);
4414+
VarDeclUsageChecker checker(AFD, ctx.Diags, AFD->getBody()->getSourceRange());
44024415
AFD->walk(checker);
44034416
}
44044417

test/decl/var/usage.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,14 @@ func testForceValueExpr() {
237237
func testBuildConfigs() {
238238
let abc = 42 // no warning.
239239
var mut = 18 // no warning.
240+
let other = 15 // no warning?
241+
var othermut = 15 // no warning
240242
#if false
241243
mut = abc // These uses prevent abc/mut from being unused/unmutated.
242244
#endif
245+
#if compiler(>=10.0)
246+
othermut = other // This use prevents other/othermut from being unused/unmutated
247+
#endif
243248
}
244249

245250
// same as above, but with a guard statement

0 commit comments

Comments
 (0)