Skip to content

Swift 6 #318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: macOS Swift



on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
name: Swift on ${{ matrix.os }}
strategy:
matrix:
os: [macos-15]

runs-on: ${{ matrix.os }}

steps:
- name: Print Swift version
run: swift --version
- uses: actions/checkout@v4
- name: Build
run: swift build -v
- name: Run tests
run: swift test -v
12 changes: 6 additions & 6 deletions .github/workflows/swift.yml → .github/workflows/ubuntu.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Swift
name: Ubuntu Swift



Expand All @@ -13,15 +13,15 @@ jobs:
name: Swift ${{ matrix.swift }} on ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, macos-latest]
swift: ["5", "5.9"]
os: [ubuntu-22.04]
swift: ["6.1", "6.0"]

runs-on: ${{ matrix.os }}

container:
image: swift:${{ matrix.swift }}

steps:
- uses: swift-actions/setup-swift@v2.3.0
with:
swift-version: ${{ matrix.swift }}
- uses: actions/checkout@v4
- name: Build
run: swift build -v
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

import PackageDescription

Expand Down
2 changes: 1 addition & 1 deletion Sources/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Foundation
*
*/
open class Attributes: NSCopying {
public static var dataPrefix: [UInt8] = "data-".utf8Array
public static let dataPrefix: [UInt8] = "data-".utf8Array

// Stored by lowercased key, but key case is checked against the copy inside
// the Attribute on retrieval.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Comment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/**
A comment node.
*/
public class Comment: Node {
public class Comment: Node, @unchecked Sendable {
private static let COMMENT_KEY: [UInt8] = UTF8Arrays.comment

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/DataNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/**
A data node, for contents of style, script tags etc, where contents should not show in text().
*/
open class DataNode: Node {
open class DataNode: Node, @unchecked Sendable {
private static let DATA_KEY = "data".utf8Array

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

open class Document: Element {
open class Document: Element, @unchecked Sendable {
public enum QuirksMode {
case noQuirks, quirks, limitedQuirks
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/DocumentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/**
* A {@code <!DOCTYPE>} node.
*/
public class DocumentType: Node {
public class DocumentType: Node, @unchecked Sendable {
static let PUBLIC_KEY = "PUBLIC".utf8Array
static let SYSTEM_KEY = "SYSTEM".utf8Array
private static let NAME = "name".utf8Array
Expand Down
2 changes: 1 addition & 1 deletion Sources/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

open class Element: Node {
open class Element: Node, @unchecked Sendable {
var _tag: Tag

private static let classString = "class".utf8Array
Expand Down
21 changes: 16 additions & 5 deletions Sources/Entities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class Entities {

private static let spaceString: [UInt8] = [0x20]

public class EscapeMode: Equatable {
public class EscapeMode: Equatable, @unchecked Sendable {

/** Restricted entities suitable for XHTML output: lt, gt, amp, and quot only. */
public static let xhtml: EscapeMode = EscapeMode(string: Entities.xhtml, size: 4, id: 0)
Expand Down Expand Up @@ -146,9 +146,20 @@ public class Entities {
}
}

private static var multipoints: [ArraySlice<UInt8>: [UnicodeScalar]] = [:] // name -> multiple character references
private static var multipointsLock = MutexLock()

// Singleton for thread-safe multipoints
private final class MultipointsRegistry: @unchecked Sendable {
static let shared = MultipointsRegistry()
let multipointsLock = MutexLock()
var multipoints: [ArraySlice<UInt8>: [UnicodeScalar]] = [:]
private init() {}
}

private static var multipointsLock: MutexLock { MultipointsRegistry.shared.multipointsLock }
private static var multipoints: [ArraySlice<UInt8>: [UnicodeScalar]] {
get { MultipointsRegistry.shared.multipoints }
set { MultipointsRegistry.shared.multipoints = newValue }
}

/**
* Check if the input is a known named entity
* @param name the possible entity name (e.g. "lt" or "amp")
Expand Down Expand Up @@ -183,7 +194,7 @@ public class Entities {
}
return nil
}

public static func codepointsForName(_ name: ArraySlice<UInt8>) -> [UnicodeScalar]? {
multipointsLock.lock()
if let scalars = multipoints[name] {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Exception.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public enum ExceptionType {
public enum ExceptionType: Sendable {
case IllegalArgumentException
case IOException
case XmlDeclaration
Expand Down
2 changes: 1 addition & 1 deletion Sources/FormElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
* A HTML Form Element provides ready access to the form fields/controls that are associated with it. It also allows a
* form to easily be submitted.
*/
public class FormElement: Element {
public class FormElement: Element, @unchecked Sendable {
private let _elements: Elements = Elements()

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/Node.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal final class Weak<T: AnyObject> {
}
}

open class Node: Equatable, Hashable {
open class Node: Equatable, Hashable, @unchecked Sendable {
var baseUri: [UInt8]?
var attributes: Attributes?

Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

open class ParseSettings {
open class ParseSettings: @unchecked Sendable {
/**
* HTML default settings: both tag and attribute names are lower-cased during parsing.
*/
Expand Down
3 changes: 2 additions & 1 deletion Sources/ParsingStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ final class TrieNode {
var isTerminal: Bool = false
}

public struct ParsingStrings: Hashable, Equatable {
public struct ParsingStrings: Hashable, Equatable, @unchecked Sendable {
// root is not Sendable, so we must mark it as @unchecked Sendable
let multiByteChars: [[UInt8]]
let multiByteCharLengths: [Int]
public let multiByteByteLookups: [(UInt64, UInt64, UInt64, UInt64)]
Expand Down
2 changes: 1 addition & 1 deletion Sources/Pattern.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Foundation

public struct Pattern {
public struct Pattern: Sendable {
public static let CASE_INSENSITIVE: Int = 0x02
let pattern: String

Expand Down
47 changes: 35 additions & 12 deletions Sources/Tag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@

import Foundation

open class Tag: Hashable {
// map of known tags
static var tags: Dictionary<[UInt8], Tag> = {
do {
return try Tag.initializeMaps()
} catch {
preconditionFailure("This method must be overridden")
open class Tag: Hashable, @unchecked Sendable {
// Removed duplicate == and hash(into:) to fix redeclaration errors
// Singleton for thread-safe tag map
private final class TagRegistry: @unchecked Sendable {
static let shared = TagRegistry()
let tagsLock = NSLock()
var tags: Dictionary<[UInt8], Tag>
private init() {
do {
self.tags = try Tag.initializeMaps()
} catch {
preconditionFailure("This method must be overridden")
}
}
return Dictionary<[UInt8], Tag>()
}()
}

// Helper to access the singleton
private static var tagsLock: NSLock { TagRegistry.shared.tagsLock }
private static var tags: Dictionary<[UInt8], Tag> {
get { TagRegistry.shared.tags }
set { TagRegistry.shared.tags = newValue }
}

fileprivate var _tagName: [UInt8]
fileprivate var _tagNameNormal: [UInt8]
Expand Down Expand Up @@ -73,12 +85,17 @@ open class Tag: Hashable {

public static func valueOf(_ tagName: [UInt8], _ settings: ParseSettings) throws -> Tag {
var tagName = tagName
var tag: Tag? = Tag.tags[tagName]
var tag: Tag?
Tag.tagsLock.lock()
tag = Tag.tags[tagName]
Tag.tagsLock.unlock()

if (tag == nil) {
tagName = settings.normalizeTag(tagName)
try Validate.notEmpty(string: tagName)
Tag.tagsLock.lock()
tag = Tag.tags[tagName]
Tag.tagsLock.unlock()

if (tag == nil) {
// not defined: create default; go anywhere, do anything! (incl be inside a <p>)
Expand Down Expand Up @@ -186,7 +203,10 @@ open class Tag: Hashable {
*/
@inline(__always)
open func isKnownTag() -> Bool {
return Tag.tags[_tagName] != nil
Tag.tagsLock.lock()
let result = Tag.tags[_tagName] != nil
Tag.tagsLock.unlock()
return result
}

/**
Expand All @@ -197,7 +217,10 @@ open class Tag: Hashable {
*/
@inline(__always)
public static func isKnownTag(_ tagName: [UInt8]) -> Bool {
return Tag.tags[tagName] != nil
Tag.tagsLock.lock()
let result2 = Tag.tags[tagName] != nil
Tag.tagsLock.unlock()
return result2
}

/**
Expand Down
2 changes: 1 addition & 1 deletion Sources/TextNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/**
A text node.
*/
open class TextNode: Node {
open class TextNode: Node, @unchecked Sendable {
/*
TextNode is a node, and so by default comes with attributes and children. The attributes are seldom used, but use
memory, and the child nodes are never used. So we don't have them, and override accessors to attributes to create
Expand Down
2 changes: 1 addition & 1 deletion Sources/Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ open class Token {
@inline(__always)
public override func toString() throws -> String {
try Validate.notNull(obj: data)
return String(decoding: getData()!, as: UTF8.self) ?? ""
return String(decoding: getData()!, as: UTF8.self)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/XmlDeclaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
/**
An XML Declaration.
*/
public class XmlDeclaration: Node {
public class XmlDeclaration: Node, @unchecked Sendable {
private let _name: [UInt8]
private let isProcessingInstruction: Bool // <! if true, <? if false, declaration (and last data char should be ?)

Expand Down
34 changes: 0 additions & 34 deletions Tests/LinuxMain.swift

This file was deleted.

25 changes: 0 additions & 25 deletions Tests/SwiftSoupTests/AttributeParseTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,6 @@ import XCTest
import SwiftSoup

class AttributeParseTest: XCTestCase {

func testLinuxTestSuiteIncludesAllTests() {
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
let thisClass = type(of: self)
let linuxCount = thisClass.allTests.count
let darwinCount = Int(thisClass.defaultTestSuite.testCaseCount)
XCTAssertEqual(linuxCount, darwinCount, "\(darwinCount - linuxCount) tests are missing from allTests")
#endif
}

func testparsesRoughAttributeString() throws {
let html: String = "<a id=\"123\" class=\"baz = 'bar'\" style = 'border: 2px'qux zim foo = 12 mux=18 />"
// should be: <id=123>, <class=baz = 'bar'>, <qux=>, <zim=>, <foo=12>, <mux.=18>
Expand Down Expand Up @@ -103,19 +93,4 @@ class AttributeParseTest: XCTestCase {
doc = try SwiftSoup.parse(html, "", Parser.xmlParser())
XCTAssertEqual("<img onerror=\"doMyJob\" />", try doc.html())
}

static var allTests = {
return [
("testLinuxTestSuiteIncludesAllTests", testLinuxTestSuiteIncludesAllTests),
("testparsesRoughAttributeString", testparsesRoughAttributeString),
("testhandlesNewLinesAndReturns", testhandlesNewLinesAndReturns),
("testparsesEmptyString", testparsesEmptyString),
("testcanStartWithEq", testcanStartWithEq),
("teststrictAttributeUnescapes", teststrictAttributeUnescapes),
("testmoreAttributeUnescapes", testmoreAttributeUnescapes),
("testparsesBooleanAttributes", testparsesBooleanAttributes),
("testretainsSlashFromAttributeName", testretainsSlashFromAttributeName)
]
}()

}
Loading