Repository: apple/swift-docc (swift)

Issue:
AvailabilityParser marks `iOSApplicationExtension, unavailable`-annotated properties as universally deprecated
### Description

When generating our SDK docs with DocC, classes marked as unavailable in Application Extensions are marked as deprecated in the generated docs.

<img width="541" alt="CleanShot 2023-01-04 at 15 08 43@2x" src="https://user-images.githubusercontent.com/52758633/210667061-0b79c3b1-f8de-476c-9d90-8648d5fbe634.png">

### Steps To Reproduce

[Generated DocC page for PaymentSheet.FlowController](https://davidme-stripe.github.io/docc-test/stripe-ios/stripepaymentsheet/documentation/stripepaymentsheet/paymentsheet)
[Class definition for PaymentSheet.FlowController](https://github.com/stripe/stripe-ios/blob/2a04cc683c4fa02d35f4017d282e51d7538dab79/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift#L41)

If we define our class:
```
@available(iOSApplicationExtension, unavailable)
@available(macCatalystApplicationExtension, unavailable)
public class MyClass: NSObject { }
```

The Swift compiler will output this in the symbol graph JSON:
```
"availability":[{"domain":"macCatalystAppExtension","isUnconditionallyUnavailable":true},{"domain":"iOSAppExtension","isUnconditionallyUnavailable":true}]
```

And Swift-DocC will mark it as deprecated in the docs data JSON:
```
{"deprecated":true,"role":"symbol","title":"MyClass","fragments":[...]}
```

I think the issue is that the AvailabilityParser marks a symbol as deprecated if every Availability entry for a symbol is marked as `isUnconditionallyUnavailable`, even if the symbol is implicitly available in other domains. [This final check](https://github.com/apple/swift-docc/blob/d94139a5e64e9ecf158214b1cded2a2880fc1b02/Sources/SwiftDocC/Model/AvailabilityParser.swift#L41) seems incorrect:
```
        // Check if the symbol is unconditionally deprecated
        return availability.availability
            .allSatisfy { $0.isUnconditionallyDeprecated || $0.isUnconditionallyUnavailable || $0.deprecatedVersion != nil }
```
DocC already checks if a "universal deprecation" exists (a deprecation where `domain == nil`) [just above that line](https://github.com/apple/swift-docc/blob/d94139a5e64e9ecf158214b1cded2a2880fc1b02/Sources/SwiftDocC/Model/AvailabilityParser.swift#L36), though I think it should also check if the domain is `*`.

### Checklist

- [ ] If possible, I've reproduced the issue using the `main` branch of this package.
- [X] This issue hasn't been addressed in an [existing GitHub issue](https://github.com/apple/swift-docc/issues).

### Expected Behavior

DocC marks it as available, or maybe marks it as "not available in Application Extensions".

### Actual behavior

DocC marks the iOSApplicationExtension-unavailable class as Deprecated with a strikethrough effect.
<img width="541" alt="CleanShot 2023-01-04 at 15 08 43@2x" src="https://user-images.githubusercontent.com/52758633/210667061-0b79c3b1-f8de-476c-9d90-8648d5fbe634.png">

### Swift-DocC Version Information

Bundled in Xcode 14.1

### Swift Compiler Version Information

```shell
swift-driver version: 1.62.15 Apple Swift version 5.7.1 (swiftlang-5.7.1.135.3 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0
```

Pull request:
Bug/issue #, if applicable: rdar://112197357

#450 

## Summary

Currently, DocC checks whether a symbol is either deprecated or unavailable for all platforms it is aware of to determine whether a symbol is unconditionally deprecated.

However, if a symbol is marked unavailable on all the platforms DocC has availability information about, this check causes DocC to mark the symbol unconditionally deprecated.

This PR adds a check that the symbol is deprecated on at least one platform to prevent symbols that are marked _unavailable_ from being displayed as unconditionally _deprecated_.

## Testing

Steps:
1. Create a project that does not give DocC any default platform availability information.
2. Mark a symbol as unavailable.
3. Build documentation and check that the symbol is not deprecated.

## Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

- [x] Added tests
- [x] Ran the `./bin/test` script and it succeeded
- [ ] Updated documentation if necessary (N/A)

Patch:
diff --git a/Sources/SwiftDocC/Model/AvailabilityParser.swift b/Sources/SwiftDocC/Model/AvailabilityParser.swift
index fe41acc44b..06b4453a19 100644
--- a/Sources/SwiftDocC/Model/AvailabilityParser.swift
+++ b/Sources/SwiftDocC/Model/AvailabilityParser.swift
@@ -39,8 +39,11 @@ struct AvailabilityParser {
         }
         
         // Check if the symbol is unconditionally deprecated
-        return availability.availability
+        let isUnconditionallyDeprecated = availability.availability
             .allSatisfy { $0.isUnconditionallyDeprecated || $0.isUnconditionallyUnavailable || $0.deprecatedVersion != nil }
+            // If a symbol is unavailable on all known platforms, it should not be marked unconditionally deprecated.
+            && availability.availability.contains(where: { $0.isUnconditionallyDeprecated || $0.deprecatedVersion != nil })
+        return isUnconditionallyDeprecated
     }
     
     /// Determines a symbol's deprecation message that either applies to a given platform or that applies to all platforms.


Test patch:
diff --git a/Tests/SwiftDocCTests/Model/AvailabilityParserTests.swift b/Tests/SwiftDocCTests/Model/AvailabilityParserTests.swift
index b6d30949c9..e76181111a 100644
--- a/Tests/SwiftDocCTests/Model/AvailabilityParserTests.swift
+++ b/Tests/SwiftDocCTests/Model/AvailabilityParserTests.swift
@@ -154,4 +154,52 @@ class AvailabilityParserTests: XCTestCase {
         XCTAssertTrue(compiler.isDeprecated())
         XCTAssertEqual(compiler.deprecationMessage(), "deprecated")
     }
+    
+    func testUnavailableIsNotDeprecated() throws {
+        let json = """
+        [
+            {
+                "domain": "tvOS",
+                "isUnconditionallyUnavailable" : true
+            }
+        ]
+        """
+        let availability = try JSONDecoder().decode(Availability.self, from: json.data(using: .utf8)!)
+        
+        /// Test all platforms
+        let compiler = AvailabilityParser(availability)
+        XCTAssertFalse(compiler.isDeprecated())
+    }
+    
+    func testAllPlatformsUnavailableOrDeprecatedIsMarkedDeprecated() throws {
+        let json = """
+        [
+            {
+                "domain": "tvOS",
+                "isUnconditionallyUnavailable" : true
+            },
+            {
+                "domain": "macOS",
+                "message" : "deprecated",
+                "deprecated": { "major": 10, "minor": 17 }
+            },
+            {
+                "domain": "iOS",
+                "message" : "deprecated",
+                "deprecated": { "major": 10, "minor": 17 }
+            },
+            {
+                "domain": "watchOS",
+                "message" : "deprecated",
+                "deprecated": { "major": 10, "minor": 17 }
+            }
+        ]
+        """
+        let availability = try JSONDecoder().decode(Availability.self, from: json.data(using: .utf8)!)
+        
+        /// Test all platforms
+        let compiler = AvailabilityParser(availability)
+        XCTAssertTrue(compiler.isDeprecated())
+        XCTAssertEqual(compiler.deprecationMessage(), "deprecated")
+    }
 }
diff --git a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift
index ab816b3d9d..5be19eb254 100644
--- a/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift
+++ b/Tests/SwiftDocCTests/Rendering/PlatformAvailabilityTests.swift
@@ -132,4 +132,31 @@ class PlatformAvailabilityTests: XCTestCase {
             item.name == "My Package" && item.introduced == "2.0"
         }))
     }
+    
+    // Test that the Info.plist default availability does not affect the deprecated/unavailable availabilities provided by the symbol graph.
+    func testAvailabilityParserWithInfoPlistDefaultAvailability() throws {
+        let (bundle, context) = try testBundleAndContext(named: "AvailabilityOverrideBundle")
+
+        let reference = ResolvedTopicReference(
+            bundleIdentifier: bundle.identifier,
+            path: "/documentation/MyKit/MyClass",
+            sourceLanguage: .swift
+        )
+        let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
+        var translator = RenderNodeTranslator(
+            context: context,
+            bundle: bundle,
+            identifier: reference,
+            source: nil
+        )
+        let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
+        let availability = try XCTUnwrap(renderNode.metadata.platformsVariants.defaultValue)
+        XCTAssertEqual(availability.count, 4)
+        availability.forEach { platform in
+            XCTAssertTrue(platform.deprecated != nil)
+        }
+        XCTAssertFalse(availability.contains(where: { platform in
+            platform.name == "tvOS"
+        }))
+    }
 }
diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityOverrideBundle.docc/Info.plist b/Tests/SwiftDocCTests/Test Bundles/AvailabilityOverrideBundle.docc/Info.plist
new file mode 100644
index 0000000000..2d1656c9a1
--- /dev/null
+++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityOverrideBundle.docc/Info.plist
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDisplayName</key>
+	<string>AvailabilityBundle</string>
+	<key>CFBundleIdentifier</key>
+	<string>org.swift.docc.availability</string>
+	<key>CFBundleVersion</key>
+	<string>0.1.0</string>
+	<key>CDAppleDefaultAvailability</key>
+	<dict>
+		<key>MyKit</key>
+		<array>
+			<dict>
+				<key>name</key>
+				<string>Mac Catalyst</string>
+				<key>version</key>
+				<string>13.0</string>
+			</dict>
+			<dict>
+				<key>name</key>
+				<string>iOS</string>
+				<key>version</key>
+				<string>13.0</string>
+			</dict>
+			<dict>
+				<key>name</key>
+				<string>tvOS</string>
+				<key>version</key>
+				<string>13.0</string>
+			</dict>
+			<dict>
+				<key>name</key>
+				<string>watchOS</string>
+				<key>version</key>
+				<string>6.0</string>
+			</dict>
+			<dict>
+				<key>name</key>
+				<string>macOS</string>
+				<key>version</key>
+				<string>10.15</string>
+			</dict>
+		</array>
+	</dict>
+</dict>
+</plist>
diff --git a/Tests/SwiftDocCTests/Test Bundles/AvailabilityOverrideBundle.docc/mykit-iOS.symbols.json b/Tests/SwiftDocCTests/Test Bundles/AvailabilityOverrideBundle.docc/mykit-iOS.symbols.json
new file mode 100644
index 0000000000..0aa422c310
--- /dev/null
+++ b/Tests/SwiftDocCTests/Test Bundles/AvailabilityOverrideBundle.docc/mykit-iOS.symbols.json
@@ -0,0 +1,559 @@
+{
+  "metadata": {
+      "formatVersion" : {
+          "major" : 1
+      },
+      "generator" : "app/1.0"
+  },
+  "module" : {
+    "name" : "MyKit",
+    "platform" : {
+      "architecture" : "x86_64",
+      "vendor" : "apple",
+      "operatingSystem" : {
+        "name" : "ios",
+        "minimumVersion" : {
+          "major" : 13,
+          "minor" : 0,
+          "patch" : 0
+        }
+      }
+    }
+  },
+  "symbols" : [
+    {
+      "accessLevel" : "public",
+      "kind" : {
+        "identifier" : "swift.class",
+        "displayName" : "Class"
+      },
+      "names" : {
+        "title" : "MyClass",
+        "subHeading" : [
+            {
+              "kind" : "keyword",
+              "spelling" : "class"
+            },
+            {
+              "kind" : "text",
+              "spelling" : " "
+            },
+            {
+              "kind" : "identifier",
+              "spelling" : "MyClass"
+            }
+        ],
+        "navigator" : [
+            {
+              "kind" : "identifier",
+              "spelling" : "MyClassNavigator"
+            }
+        ]
+      },
+      "availability" : [
+        {
+          "domain": "macOS",
+          "deprecated": {
+            "major": 10,
+            "minor": 0
+          }
+        },
+        {
+          "domain": "watchOS",
+          "deprecated": {
+            "major": 6,
+            "minor": 0
+          }
+        },
+        {
+          "domain": "tvOS",
+          "isUnconditionallyUnavailable": true
+        },
+        {
+          "domain": "iOS",
+          "deprecated": {
+            "major": 13,
+            "minor": 0
+          }
+        }
+      ],
+      "pathComponents" : [
+        "MyClass"
+      ],
+      "identifier" : {
+        "precise" : "s:5MyKit0A5ClassC",
+        "interfaceLanguage": "swift"
+      },
+      "declarationFragments" : [
+        {
+          "kind" : "keyword",
+          "spelling" : "class"
+        },
+        {
+          "kind" : "text",
+          "spelling" : " "
+        },
+        {
+          "kind" : "identifier",
+          "spelling" : "MyClass"
+        }
+      ]
+    },
+    {
+      "accessLevel" : "public",
+      "kind" : {
+        "identifier" : "swift.method",
+        "displayName" : "Instance Method"
+      },
+      "names" : {
+        "title" : "myFunction()"
+      },
+      "pathComponents" : [
+        "MyClass",
+        "myFunction()"
+      ],
+      "identifier" : {
+        "precise" : "s:5MyKit0A5ClassC10myFunctionyyF",
+        "interfaceLanguage": "swift"
+      },
+      "docComment" : {
+        "lines" : [
+          {
+            "text" : "A cool API to call."
+          },
+          {
+            "text" : ""
+          },
+          {
+            "text" : "- Parameters:"
+          },
+          {
+            "text" : "  - name: A parameter"
+          },
+          {
+            "text" : "- Returns: Return value"
+          }
+        ]
+      },
+      "declarationFragments" : [
+        {
+          "kind" : "keyword",
+          "spelling" : "func"
+        },
+        {
+          "kind" : "text",
+          "spelling" : " "
+        },
+        {
+          "kind" : "identifier",
+          "spelling" : "myFunction"
+        },
+        {
+          "kind" : "text",
+          "spelling" : "("
+        },
+        {
+          "kind" : "externalParam",
+          "spelling" : "for"
+        },
+        {
+          "kind" : "text",
+          "spelling" : " "
+        },
+        {
+          "kind" : "internalParam",
+          "spelling" : "name"
+        },
+        {
+          "kind" : "unhandledTokenKind",
+          "spelling" : "..."
+        },
+        {
+          "kind" : "text",
+          "spelling" : ")"
+        }
+      ],
+      "swiftExtension" : {
+        "extendedModule": "MyKit",
+        "constraints" : [
+          {
+            "kind" : "sameType",
+            "lhs" : "Label",
+            "rhs" : "Text"
+          },
+          {
+            "kind" : "superclass",
+            "lhs" : "Observer",
+            "rhs" : "NSObject"
+          },
+          {
+            "kind" : "conformance",
+            "lhs" : "S",
+            "rhs" : "StringProtocol"
+          }
+        ]
+      }
+    },
+    {
+      "accessLevel" : "public",
+      "declarationFragments" : [
+        {
+          "kind" : "keyword",
+          "spelling" : "func"
+        },
+        {
+          "kind" : "text",
+          "spelling" : " "
+        },
+        {
+          "kind" : "identifier",
+          "spelling" : "globalFunction"
+        },
+        {
+          "kind" : "text",
+          "spelling" : "("
+        },
+        {
+          "kind" : "identifier",
+          "spelling" : "_"
+        },
+        {
+          "kind" : "text",
+          "spelling" : ": "
+        },
+        {
+          "kind" : "typeIdentifier",
+          "preciseIdentifier" : "s:10Foundation4DataV",
+          "spelling" : "Data"
+        },
+        {
+          "kind" : "text",
+          "spelling" : ", "
+        },
+        {
+          "kind" : "identifier",
+          "spelling" : "considering"
+        },
+        {
+          "kind" : "text",
+          "spelling" : ": "
+        },
+        {
+          "kind" : "typeIdentifier",
+          "preciseIdentifier" : "s:Si",
+          "spelling" : "Int"
+        },
+        {
+          "kind" : "text",
+          "spelling" : ")"
+        }
+      ],
+      "docComment" : {
+        "lines" : [
+
+        ]
+      },
+      "functionSignature" : {
+        "parameters" : [
+          {
+            "declarationFragments" : [
+              {
+                "kind" : "identifier",
+                "spelling" : "data"
+              },
+              {
+                "kind" : "text",
+                "spelling" : ": "
+              },
+              {
+                "kind" : "typeIdentifier",
+                "preciseIdentifier" : "s:10Foundation4DataV",
+                "spelling" : "Data"
+              }
+            ],
+            "name" : "data"
+          },
+          {
+            "declarationFragments" : [
+              {
+                "kind" : "identifier",
+                "spelling" : "considering"
+              },
+              {
+                "kind" : "text",
+                "spelling" : ": "
+              },
+              {
+                "kind" : "typeIdentifier",
+                "preciseIdentifier" : "s:Si",
+                "spelling" : "Int"
+              }
+            ],
+            "name" : "considering"
+          }
+        ],
+        "returns" : [
+          {
+            "kind" : "text",
+            "spelling" : "()"
+          }
+        ]
+      },
+      "identifier" : {
+        "interfaceLanguage" : "swift",
+        "precise" : "s:5MyKit14globalFunction_11consideringy10Foundation4DataV_SitF"
+      },
+      "kind" : {
+        "displayName" : "Function",
+        "identifier" : "swift.func"
+      },
+      "names" : {
+        "navigator" : [
+          {
+            "kind" : "keyword",
+            "spelling" : "func"
+          },
+          {
+            "kind" : "text",
+            "spelling" : " "
+          },
+          {
+            "kind" : "identifier",
+            "spelling" : "globalFunction"
+          },
+          {
+            "kind" : "text",
+            "spelling" : "("
+          },
+          {
+            "kind" : "typeIdentifier",
+            "preciseIdentifier" : "s:10Foundation4DataV",
+            "spelling" : "Data"
+          },
+          {
+            "kind" : "text",
+            "spelling" : ", "
+          },
+          {
+            "kind" : "identifier",
+            "spelling" : "considering"
+          },
+          {
+            "kind" : "text",
+            "spelling" : ": "
+          },
+          {
+            "kind" : "typeIdentifier",
+            "preciseIdentifier" : "s:Si",
+            "spelling" : "Int"
+          },
+          {
+            "kind" : "text",
+            "spelling" : ")"
+          },
+          {
+            "kind" : "text",
+            "spelling" : "\n"
+          }
+        ],
+        "subHeading" : [
+          {
+            "kind" : "keyword",
+            "spelling" : "func"
+          },
+          {
+            "kind" : "text",
+            "spelling" : " "
+          },
+          {
+            "kind" : "identifier",
+            "spelling" : "globalFunction"
+          },
+          {
+            "kind" : "text",
+            "spelling" : "("
+          },
+          {
+            "kind" : "typeIdentifier",
+            "preciseIdentifier" : "s:10Foundation4DataV",
+            "spelling" : "Data"
+          },
+          {
+            "kind" : "text",
+            "spelling" : ", "
+          },
+          {
+            "kind" : "identifier",
+            "spelling" : "considering"
+          },
+          {
+            "kind" : "text",
+            "spelling" : ": "
+          },
+          {
+            "kind" : "typeIdentifier",
+            "preciseIdentifier" : "s:Si",
+            "spelling" : "Int"
+          },
+          {
+            "kind" : "text",
+            "spelling" : ")"
+          },
+          {
+            "kind" : "text",
+            "spelling" : "\n"
+          }
+        ],
+        "title" : "globalFunction(_:considering:)"
+      },
+      "pathComponents" : [
+        "globalFunction(_:considering:)"
+      ]
+    },
+    {
+      "accessLevel" : "internal",
+      "kind" : {
+        "identifier" : "swift.init",
+        "displayName" : "Initializer"
+      },
+      "names" : {
+        "title" : "init()"
+      },
+      "pathComponents" : [
+        "MyClass",
+        "init()"
+      ],
+      "identifier" : {
+        "precise" : "s:5MyKit0A5ClassCACycfc",
+        "interfaceLanguage": "swift"
+      }
+    },
+    {
+      "accessLevel" : "internal",
+      "kind" : {
+        "identifier" : "swift.init",
+        "displayName" : "Initializer"
+      },
+      "names" : {
+        "title" : "init()"
+      },
+      "pathComponents" : [
+        "MyClass",
+        "init()"
+      ],
+      "identifier" : {
+        "precise" : "s:5MyKit0A5ClassCACycfcDUPLICATE",
+        "interfaceLanguage" : "swift"
+      }
+    },
+    {
+      "accessLevel" : "public",
+      "kind" : {
+        "identifier" : "swift.protocol",
+        "displayName" : "Protocol"
+      },
+      "names" : {
+        "title" : "MyProtocol",
+        "navigator": [
+          {
+            "kind" : "identifier",
+            "spelling" : "MyProtocol"
+          }
+        ],
+        "subHeading": [
+          {
+            "kind" : "keyword",
+            "spelling" : "protocol"
+          },
+          {
+            "kind" : "text",
+            "spelling" : " "
+          },
+          {
+            "kind" : "identifier",
+            "spelling" : "MyProtocol"
+          },
+          {
+            "kind" : "text",
+            "spelling" : " : "
+          },
+          {
+            "kind" : "typeIdentifier",
+            "spelling" : "Hashable",
+            "preciseIdentifier" : "p:hPP"
+          }
+        ]
+      },
+      "pathComponents" : [
+        "MyProtocol"
+      ],
+      "identifier" : {
+        "precise" : "s:5MyKit0A5ProtocolP",
+        "interfaceLanguage": "swift"
+      },
+      "declarationFragments" : [
+        {
+          "kind" : "keyword",
+          "spelling" : "protocol"
+        },
+        {
+          "kind" : "text",
+          "spelling" : " "
+        },
+        {
+          "kind" : "identifier",
+          "spelling" : "MyProtocol"
+        },
+        {
+          "kind" : "text",
+          "spelling" : " : "
+        },
+        {
+          "kind" : "typeIdentifier",
+          "spelling" : "Hashable",
+          "preciseIdentifier" : "p:hPP"
+        }
+      ]
+    }
+  ],
+  "relationships" : [
+    {
+      "source" : "s:5MyKit0A5ProtocolP",
+      "target" : "s:5Foundation0A5NSCodableP",
+      "kind" : "conformsTo",
+      "targetFallback": "Foundation.NSCodable"
+    },
+    {
+      "source" : "s:5MyKit0A5ProtocolP",
+      "target" : "s:5Foundation0A5EarhartP",
+      "kind" : "conformsTo"
+    },
+    {
+      "source" : "s:5MyKit0A5ClassC",
+      "target" : "s:5MyKit0A5ProtocolP",
+      "kind" : "conformsTo",
+      "swiftConstraints" : [
+        {
+            "kind" : "conformance",
+            "lhs" : "Element",
+            "rhs" : "Equatable"
+        }
+      ]
+    },
+    {
+      "source" : "s:5MyKit0A5ClassC10myFunctionyyF",
+      "target" : "s:5MyKit0A5ClassC",
+      "kind" : "memberOf"
+    },
+    {
+      "source" : "s:5MyKit0A5ClassCACycfc",
+      "target" : "s:5MyKit0A5ClassC",
+      "kind" : "memberOf"
+    },
+    {
+      "source" : "s:5MyKit0A5ClassCACycfcDUPLICATE",
+      "target" : "s:5MyKit0A5ClassC",
+      "kind" : "memberOf"
+    }
+  ]
+}