Skip to content

Commit

Permalink
Fix read-only subscripts (#285)
Browse files Browse the repository at this point in the history
## Overview

Read-only subscripts were incorrectly generating setter thunks which
weren’t caught by the test suite due to the generic `Equatable`
overload. This adds the `readonly` attribute as needed when parsing
subscript methods using similar logic as properties.

Closes #281

## Test Plan

Fixed read-only subscript tests and added an additional case for setters
marked as (file)private.
  • Loading branch information
andrewchang-bird committed Jan 28, 2022
1 parent f6e080c commit 816fa74
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,16 @@ class SubscriptMethodTemplate: MethodTemplate {
},
invocationArguments: setterInvocationArguments).render())

let accessors = method.attributes.contains(.readonly) ? [getterDefinition.render()] : [
getterDefinition.render(),
setterDefinition.render(),
]

return String(lines: [
"// MARK: Mocked \(fullNameForMocking)",
VariableDefinitionTemplate(attributes: method.attributes.safeDeclarations,
declaration: "public \(overridableModifiers)\(uniqueDeclaration)",
body: String(lines: [getterDefinition.render(),
setterDefinition.render()])).render(),
body: String(lines: accessors)).render(),
])
}

Expand Down
29 changes: 29 additions & 0 deletions Sources/MockingbirdGenerator/Parser/Models/Method.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ struct Method {
rawParametersDeclaration) = Method.parseDeclaration(from: dictionary,
source: source,
isInitializer: isInitializer,
kind: kind,
rootKind: rootKind,
attributes: attributes)

// Parse return type.
Expand Down Expand Up @@ -121,6 +123,8 @@ struct Method {
private static func parseDeclaration(from dictionary: StructureDictionary,
source: Data?,
isInitializer: Bool,
kind: SwiftDeclarationKind,
rootKind: SwiftDeclarationKind,
attributes: Attributes) -> (Attributes, Substring?) {
guard let declaration = SourceSubstring.key.extract(from: dictionary, contents: source)
else { return (attributes, nil) }
Expand Down Expand Up @@ -159,9 +163,34 @@ struct Method {
fullAttributes.insert(.throws)
}

// Parse kind-specific attributes.
switch kind {
case .functionSubscript:
if isReadOnlySubscript(from: dictionary, rootKind: rootKind, source: source) {
fullAttributes.insert(.readonly)
}
default:
break
}

return (fullAttributes, rawParametersDeclaration)
}

private static func isReadOnlySubscript(from dictionary: StructureDictionary,
rootKind: SwiftDeclarationKind,
source: Data?) -> Bool {
let setterAccessLevel = AccessLevel(setter: dictionary)
if rootKind == .class {
guard setterAccessLevel != .fileprivate,
setterAccessLevel != .private
else { return true }
let body = SourceSubstring.body.extract(from: dictionary, contents: source) ?? ""
return !body.contains("set", excluding: ["{": "}"])
} else {
return setterAccessLevel == nil
}
}

private static func parseArgumentLabels(name: String, parameters: Substring?)
-> (shortName: String, labels: [String?]) {
let (shortName, labels) = name.extractArgumentLabels()
Expand Down
10 changes: 8 additions & 2 deletions Sources/MockingbirdTestsHost/Subscripts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ protocol SubscriptedProtocol {
subscript(index: Int) -> String { get set }
subscript(index: Int) -> Bool { get set } // Overloaded parameter type
subscript(index: String) -> String { get set } // Overloaded return type
subscript(index: Int) -> Int { get } // Only getter
subscript(object: AnyObject) -> Int { get } // Only getter
subscript(row: Int, column: Int) -> String { get set } // Multiple parameters
subscript(indexes: String...) -> String { get set } // Variadic parameter
subscript<IndexType: Equatable, ReturnType: Hashable>(index: IndexType) -> ReturnType { get set }
Expand All @@ -29,10 +29,16 @@ class SubscriptedClass {
}

// Only getter
subscript(index: Int) -> Int {
subscript(object: AnyObject) -> Int {
get { fatalError() }
}

// Private setter
private(set) subscript(object: NSObject) -> Int {
get { fatalError() }
set { fatalError() }
}

// Multiple parameters
subscript(row: Int, column: Int) -> String {
get { fatalError() }
Expand Down
16 changes: 10 additions & 6 deletions Tests/MockingbirdTests/Framework/SubscriptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,28 @@ class SubscriptTests: BaseTestCase {
classMock = mock(SubscriptedClass.self)
}

private class MyObject {}

// MARK: - Protocol mock

// MARK: Getter

func testSubscriptProtocol_handlesBasicSingleParameterGetter() {
let object = MyObject()
given(protocolMock.getSubscript(42)) ~> "bar"
given(protocolMock.getSubscript(42)) ~> true
given(protocolMock.getSubscript("foo")) ~> "bar"
given(protocolMock.getSubscript(42)) ~> 99
given(protocolMock.getSubscript(object)) ~> 99

XCTAssertEqual(protocolInstance[42], "bar")
XCTAssertEqual(protocolInstance[42], true)
XCTAssertEqual(protocolInstance["foo"], "bar")
XCTAssertEqual(protocolInstance[42], 99)
XCTAssertEqual(protocolInstance[object], 99)

verify(protocolMock.getSubscript(42)).returning(String.self).wasCalled()
verify(protocolMock.getSubscript(42)).returning(Bool.self).wasCalled()
verify(protocolMock.getSubscript("foo")).returning(String.self).wasCalled()
verify(protocolMock.getSubscript(42)).returning(Int.self).wasCalled()
verify(protocolMock.getSubscript(object)).returning(Int.self).wasCalled()
}

func testSubscriptProtocol_handlesMultipleParameterGetter() {
Expand Down Expand Up @@ -109,20 +112,21 @@ class SubscriptTests: BaseTestCase {
// MARK: - Class mock

func testSubscriptClass_handlesBasicSingleParameterCalls() {
let object = MyObject()
given(classMock.getSubscript(42)) ~> "bar"
given(classMock.getSubscript(42)) ~> true
given(classMock.getSubscript("foo")) ~> "bar"
given(classMock.getSubscript(42)) ~> 99
given(classMock.getSubscript(object)) ~> 99

XCTAssertEqual(classInstance[42], "bar")
XCTAssertEqual(classInstance[42], true)
XCTAssertEqual(classInstance["foo"], "bar")
XCTAssertEqual(classInstance[42], 99)
XCTAssertEqual(classInstance[object], 99)

verify(classMock.getSubscript(42)).returning(String.self).wasCalled()
verify(classMock.getSubscript(42)).returning(Bool.self).wasCalled()
verify(classMock.getSubscript("foo")).returning(String.self).wasCalled()
verify(classMock.getSubscript(42)).returning(Int.self).wasCalled()
verify(classMock.getSubscript(object)).returning(Int.self).wasCalled()
}

func testSubscriptClass_handlesMultipleParameterCalls() {
Expand Down

0 comments on commit 816fa74

Please sign in to comment.