Skip to content

Add "Toggle disabled test" code action#2541

Open
xlmtvv wants to merge 1 commit intoswiftlang:mainfrom
xlmtvv:feat/toggle-disabled-test
Open

Add "Toggle disabled test" code action#2541
xlmtvv wants to merge 1 commit intoswiftlang:mainfrom
xlmtvv:feat/toggle-disabled-test

Conversation

@xlmtvv
Copy link
Copy Markdown

@xlmtvv xlmtvv commented Mar 6, 2026

Add a syntactic code action to toggle test functions between enabled and disabled states.

For Swift Testing, activating the action on a @Test function adds .disabled() to the attribute (and removes it when the test is already disabled). For XCTest, it inserts a leading throw XCTSkip("Disabled") statement and adds throws to the signature (and reverses both when enabling).

Conditional disables such as .disabled(if: condition) are intentionally not treated as unconditional - the action will not offer "Enable test" for those.

Resolves #2524

Add a syntactic code action that toggles test functions between enabled
and disabled states.

For Swift Testing, this adds or removes the .disabled() trait on the
@test attribute. For XCTest, this inserts or removes a leading
throw XCTSkip("Disabled") statement and adjusts throws accordingly.

Resolves swiftlang#2524
@rintaro
Copy link
Copy Markdown
Member

rintaro commented Mar 16, 2026

@swift-ci Please test

return declRef.baseName.text == "XCTSkip"
}
if let memberAccess = functionCall.calledExpression.as(MemberAccessExprSyntax.self) {
return memberAccess.declName.baseName.text == "XCTSkip"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this handling module qualified calls i.e. XCTests.XCTSkip(...). If so, could you add test cases?

Comment on lines +277 to +291
// Walk up to find the enclosing MemberBlockSyntax, then check its parent.
var node: Syntax? = Syntax(funcDecl).parent
while let current = node {
if let memberBlock = current.as(MemberBlockSyntax.self) {
let parent = memberBlock.parent
return parent?.is(ClassDeclSyntax.self) == true
|| parent?.is(ExtensionDeclSyntax.self) == true
}
// Stop at code blocks to avoid matching nested functions.
if current.is(CodeBlockSyntax.self) {
return false
}
node = current.parent
}
return false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Walk up to find the enclosing MemberBlockSyntax, then check its parent.
var node: Syntax? = Syntax(funcDecl).parent
while let current = node {
if let memberBlock = current.as(MemberBlockSyntax.self) {
let parent = memberBlock.parent
return parent?.is(ClassDeclSyntax.self) == true
|| parent?.is(ExtensionDeclSyntax.self) == true
}
// Stop at code blocks to avoid matching nested functions.
if current.is(CodeBlockSyntax.self) {
return false
}
node = current.parent
}
return false
funcDecl.findParentOfSelf(
ofType: Syntax.self,
stoppingIf: { $0.is(CodeBlockSyntax.self) },
matching: { $0.is(ClassDeclSyntax.self) || $0.is(ExtensionDeclSyntax.self) }
) != nil

Comment on lines +252 to +260
let name = funcDecl.name.text
guard name.hasPrefix("test") else {
return nil
}

// XCTest methods must be inside a class or extension.
guard isDirectClassOrExtensionMember(funcDecl) else {
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit unfortunate that this is essentially duplicating the logic in SyntacticSwiftXCTestScanner, but I don't feel it's worth to reuse that for this simple task... fine for now


/// Infer the indentation used inside a code block by examining the first
/// statement's leading trivia, or falling back to 4 spaces.
private static func inferIndentation(of body: CodeBlockSyntax) -> String {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use BasicFormat.inferIndentation(of:) from SwiftBasicFormat module?

return true
}
// Don't descend into nested functions or closures.
if node.is(FunctionDeclSyntax.self) || node.is(ClosureExprSyntax.self) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel we can skip any DeclSyntax

Suggested change
if node.is(FunctionDeclSyntax.self) || node.is(ClosureExprSyntax.self) {
if node.is(DeclSyntax.self) || node.is(ClosureExprSyntax.self) {

guard
let funcDecl = scope.innermostNodeContainingRange?.findParentOfSelf(
ofType: FunctionDeclSyntax.self,
stoppingIf: { $0.is(CodeBlockSyntax.self) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This action is active at anywhere in the function signature including the attributes, but not in the body.
Maybe we might want to adjust it in the future, but I think it's fine for now. Could you add comment to clarify the cursor positions it's intended to be active?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Toggle disabled test

2 participants