test(coding): add sequence fixtures and contract gate
This commit is contained in:
parent
5c3de786bf
commit
611c079255
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -25,6 +25,7 @@ jobs:
|
||||
run: |
|
||||
bash scripts/check-style.sh
|
||||
bash scripts/check-namespacing.sh
|
||||
bash scripts/check-forbidden-patterns.sh
|
||||
git fetch origin main --depth=1 || true
|
||||
BASE_REF=origin/main bash scripts/check-changelog-touched.sh
|
||||
- name: Format and lint
|
||||
|
||||
@ -62,6 +62,7 @@ Before claiming a change is complete, run and cite:
|
||||
```sh
|
||||
bash scripts/check-style.sh
|
||||
bash scripts/check-namespacing.sh
|
||||
bash scripts/check-forbidden-patterns.sh
|
||||
bash scripts/check-changelog-touched.sh
|
||||
swiftformat . --config .swiftformat --lint
|
||||
swiftlint --config .swiftlint.yml --strict
|
||||
|
||||
@ -56,6 +56,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
deterministically.
|
||||
- Add unkeyed typed Decodable and Encodable conversion for sequences, including
|
||||
nested sequences and sequence properties on keyed structs.
|
||||
- Add a fixture-backed sequence typed-conversion test corpus and a
|
||||
forbidden-pattern verification gate for package, source, and test contracts.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
166
Tests/UnkeyedCodingFixtures.swift
Normal file
166
Tests/UnkeyedCodingFixtures.swift
Normal file
@ -0,0 +1,166 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
struct SequenceArticle: Codable, Equatable {
|
||||
var title: String
|
||||
var tags: [String]
|
||||
var ranks: [Int?]
|
||||
}
|
||||
|
||||
struct SequenceCatalog: Codable, Equatable {
|
||||
var articles: [SequenceArticle]
|
||||
var featured: [String?]
|
||||
var empty: [Int]
|
||||
}
|
||||
|
||||
let sequenceArticle = SequenceArticle(
|
||||
title: "Typed YAML",
|
||||
tags: ["Swift", "YAML"],
|
||||
ranks: [1, nil, 3],
|
||||
)
|
||||
|
||||
let sequenceArticleValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "title", value: .string("Typed YAML")),
|
||||
.init(key: "tags", value: .sequence([
|
||||
.string("Swift"),
|
||||
.string("YAML"),
|
||||
])),
|
||||
.init(key: "ranks", value: .sequence([
|
||||
.int(1),
|
||||
.null,
|
||||
.int(3),
|
||||
])),
|
||||
]))
|
||||
|
||||
let sequenceCatalog = SequenceCatalog(
|
||||
articles: [sequenceArticle],
|
||||
featured: ["Typed YAML", nil],
|
||||
empty: [],
|
||||
)
|
||||
|
||||
let sequenceCatalogValue = PureYAML.Model.Value.mapping(.init([
|
||||
.init(key: "articles", value: .sequence([sequenceArticleValue])),
|
||||
.init(key: "featured", value: .sequence([
|
||||
.string("Typed YAML"),
|
||||
.null,
|
||||
])),
|
||||
.init(key: "empty", value: .sequence([])),
|
||||
]))
|
||||
|
||||
struct SequenceDecodeFixture: CustomStringConvertible {
|
||||
enum Expectation {
|
||||
case integers([Int])
|
||||
case optionalStrings([String?])
|
||||
case nestedIntegers([[Int]])
|
||||
case articles([SequenceArticle])
|
||||
case catalog(SequenceCatalog)
|
||||
}
|
||||
|
||||
var name: String
|
||||
var yaml: String
|
||||
var value: PureYAML.Model.Value
|
||||
var expectation: Expectation
|
||||
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
|
||||
func expectDecodedFromYAML() throws {
|
||||
switch expectation {
|
||||
case let .integers(expected):
|
||||
#expect(try PureYAML.decode([Int].self, from: yaml) == expected)
|
||||
case let .optionalStrings(expected):
|
||||
#expect(try PureYAML.decode([String?].self, from: yaml) == expected)
|
||||
case let .nestedIntegers(expected):
|
||||
#expect(try PureYAML.decode([[Int]].self, from: yaml) == expected)
|
||||
case let .articles(expected):
|
||||
#expect(try PureYAML.decode([SequenceArticle].self, from: yaml) == expected)
|
||||
case let .catalog(expected):
|
||||
#expect(try PureYAML.decode(SequenceCatalog.self, from: yaml) == expected)
|
||||
}
|
||||
}
|
||||
|
||||
func expectDecodedFromValue() throws {
|
||||
switch expectation {
|
||||
case let .integers(expected):
|
||||
#expect(try PureYAML.decode([Int].self, from: value) == expected)
|
||||
case let .optionalStrings(expected):
|
||||
#expect(try PureYAML.decode([String?].self, from: value) == expected)
|
||||
case let .nestedIntegers(expected):
|
||||
#expect(try PureYAML.decode([[Int]].self, from: value) == expected)
|
||||
case let .articles(expected):
|
||||
#expect(try PureYAML.decode([SequenceArticle].self, from: value) == expected)
|
||||
case let .catalog(expected):
|
||||
#expect(try PureYAML.decode(SequenceCatalog.self, from: value) == expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sequenceDecodeFixtures: [SequenceDecodeFixture] = [
|
||||
.init(
|
||||
name: "integer sequence",
|
||||
yaml: """
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
""",
|
||||
value: .sequence([.int(1), .int(2), .int(3)]),
|
||||
expectation: .integers([1, 2, 3]),
|
||||
),
|
||||
.init(
|
||||
name: "optional string sequence",
|
||||
yaml: """
|
||||
- one
|
||||
- null
|
||||
- three
|
||||
""",
|
||||
value: .sequence([
|
||||
.string("one"),
|
||||
.null,
|
||||
.string("three"),
|
||||
]),
|
||||
expectation: .optionalStrings(["one", nil, "three"]),
|
||||
),
|
||||
.init(
|
||||
name: "nested integer sequence",
|
||||
yaml: """
|
||||
- [1, 2]
|
||||
- []
|
||||
""",
|
||||
value: .sequence([
|
||||
.sequence([.int(1), .int(2)]),
|
||||
.sequence([]),
|
||||
]),
|
||||
expectation: .nestedIntegers([[1, 2], []]),
|
||||
),
|
||||
.init(
|
||||
name: "sequence of keyed articles",
|
||||
yaml: """
|
||||
- title: Typed YAML
|
||||
tags:
|
||||
- Swift
|
||||
- YAML
|
||||
ranks:
|
||||
- 1
|
||||
- null
|
||||
- 3
|
||||
""",
|
||||
value: .sequence([sequenceArticleValue]),
|
||||
expectation: .articles([sequenceArticle]),
|
||||
),
|
||||
.init(
|
||||
name: "keyed catalog with sequence properties",
|
||||
yaml: """
|
||||
articles:
|
||||
- title: Typed YAML
|
||||
tags: [Swift, YAML]
|
||||
ranks: [1, null, 3]
|
||||
featured:
|
||||
- Typed YAML
|
||||
- null
|
||||
empty: []
|
||||
""",
|
||||
value: sequenceCatalogValue,
|
||||
expectation: .catalog(sequenceCatalog),
|
||||
),
|
||||
]
|
||||
@ -3,6 +3,29 @@ import Testing
|
||||
|
||||
@Suite("Unkeyed Coding")
|
||||
struct UnkeyedCodingTests {
|
||||
@Test("Fixture corpus decodes YAML and model sequences", arguments: sequenceDecodeFixtures)
|
||||
func test_fixtureCorpusDecodesYAMLAndModelSequences(
|
||||
fixture: SequenceDecodeFixture,
|
||||
) throws {
|
||||
try fixture.expectDecodedFromYAML()
|
||||
try fixture.expectDecodedFromValue()
|
||||
}
|
||||
|
||||
@Test("Fixture corpus encodes exact sequence values and YAML", arguments: sequenceEncodeFixtures)
|
||||
func test_fixtureCorpusEncodesExactSequenceValuesAndYAML(
|
||||
fixture: SequenceEncodeFixture,
|
||||
) throws {
|
||||
try fixture.expectEncodedValue()
|
||||
try fixture.expectEncodedYAML()
|
||||
}
|
||||
|
||||
@Test("Fixture corpus reports exact sequence failures", arguments: sequenceFailureFixtures)
|
||||
func test_fixtureCorpusReportsExactSequenceFailures(
|
||||
fixture: SequenceFailureFixture,
|
||||
) {
|
||||
fixture.expectFailure()
|
||||
}
|
||||
|
||||
@Test("Decodes unkeyed sequences with scalars optionals nesting and keyed elements")
|
||||
func test_decodesUnkeyedSequencesWithScalarsOptionalsNestingAndKeyedElements() throws {
|
||||
#expect(try PureYAML.decode([Int].self, from: .sequence([.int(1), .int(2)])) == [1, 2])
|
||||
|
||||
148
Tests/UnkeyedEncodingFixtures.swift
Normal file
148
Tests/UnkeyedEncodingFixtures.swift
Normal file
@ -0,0 +1,148 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
struct SequenceEncodeFixture: CustomStringConvertible {
|
||||
enum Subject {
|
||||
case integers([Int])
|
||||
case optionalStrings([String?])
|
||||
case nestedIntegers([[Int]])
|
||||
case articles([SequenceArticle])
|
||||
case catalog(SequenceCatalog)
|
||||
}
|
||||
|
||||
var name: String
|
||||
var subject: Subject
|
||||
var expectedValue: PureYAML.Model.Value
|
||||
var expectedYAML: String
|
||||
var forbiddenYAMLFragments: [String]
|
||||
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
|
||||
func expectEncodedValue() throws {
|
||||
switch subject {
|
||||
case let .integers(value):
|
||||
#expect(try PureYAML.encode(value) == expectedValue)
|
||||
case let .optionalStrings(value):
|
||||
#expect(try PureYAML.encode(value) == expectedValue)
|
||||
case let .nestedIntegers(value):
|
||||
#expect(try PureYAML.encode(value) == expectedValue)
|
||||
case let .articles(value):
|
||||
#expect(try PureYAML.encode(value) == expectedValue)
|
||||
case let .catalog(value):
|
||||
#expect(try PureYAML.encode(value) == expectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func expectEncodedYAML() throws {
|
||||
let yaml = try encodedYAML()
|
||||
#expect(yaml == expectedYAML)
|
||||
for fragment in forbiddenYAMLFragments {
|
||||
#expect(!yaml.contains(fragment))
|
||||
}
|
||||
}
|
||||
|
||||
private func encodedYAML() throws -> String {
|
||||
switch subject {
|
||||
case let .integers(value):
|
||||
try PureYAML.encodeToYAML(value)
|
||||
case let .optionalStrings(value):
|
||||
try PureYAML.encodeToYAML(value)
|
||||
case let .nestedIntegers(value):
|
||||
try PureYAML.encodeToYAML(value)
|
||||
case let .articles(value):
|
||||
try PureYAML.encodeToYAML(value)
|
||||
case let .catalog(value):
|
||||
try PureYAML.encodeToYAML(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sequenceEncodeFixtures: [SequenceEncodeFixture] = [
|
||||
.init(
|
||||
name: "integer sequence",
|
||||
subject: .integers([1, 2, 3]),
|
||||
expectedValue: .sequence([.int(1), .int(2), .int(3)]),
|
||||
expectedYAML: """
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
""",
|
||||
forbiddenYAMLFragments: ["null", "title:"],
|
||||
),
|
||||
.init(
|
||||
name: "optional string sequence",
|
||||
subject: .optionalStrings(["one", nil, "three"]),
|
||||
expectedValue: .sequence([
|
||||
.string("one"),
|
||||
.null,
|
||||
.string("three"),
|
||||
]),
|
||||
expectedYAML: """
|
||||
- "one"
|
||||
- null
|
||||
- "three"
|
||||
|
||||
""",
|
||||
forbiddenYAMLFragments: ["- 0"],
|
||||
),
|
||||
.init(
|
||||
name: "nested integer sequence",
|
||||
subject: .nestedIntegers([[1, 2], []]),
|
||||
expectedValue: .sequence([
|
||||
.sequence([.int(1), .int(2)]),
|
||||
.sequence([]),
|
||||
]),
|
||||
expectedYAML: """
|
||||
-
|
||||
- 1
|
||||
- 2
|
||||
-
|
||||
|
||||
""",
|
||||
forbiddenYAMLFragments: ["null"],
|
||||
),
|
||||
.init(
|
||||
name: "sequence of keyed articles",
|
||||
subject: .articles([sequenceArticle]),
|
||||
expectedValue: .sequence([sequenceArticleValue]),
|
||||
expectedYAML: """
|
||||
-
|
||||
title: "Typed YAML"
|
||||
tags:
|
||||
- "Swift"
|
||||
- "YAML"
|
||||
ranks:
|
||||
- 1
|
||||
- null
|
||||
- 3
|
||||
|
||||
""",
|
||||
forbiddenYAMLFragments: ["missing:", "empty:"],
|
||||
),
|
||||
.init(
|
||||
name: "keyed catalog with sequence properties",
|
||||
subject: .catalog(sequenceCatalog),
|
||||
expectedValue: sequenceCatalogValue,
|
||||
expectedYAML: """
|
||||
articles:
|
||||
-
|
||||
title: "Typed YAML"
|
||||
tags:
|
||||
- "Swift"
|
||||
- "YAML"
|
||||
ranks:
|
||||
- 1
|
||||
- null
|
||||
- 3
|
||||
featured:
|
||||
- "Typed YAML"
|
||||
- null
|
||||
empty:
|
||||
|
||||
""",
|
||||
forbiddenYAMLFragments: ["missing:", "- 0"],
|
||||
),
|
||||
]
|
||||
137
Tests/UnkeyedFailureFixtures.swift
Normal file
137
Tests/UnkeyedFailureFixtures.swift
Normal file
@ -0,0 +1,137 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
struct SequenceFailureFixture: CustomStringConvertible {
|
||||
enum Operation {
|
||||
case decodeIntegers
|
||||
case decodeNestedIntegers
|
||||
case decodeArticles
|
||||
case decodeCatalog
|
||||
}
|
||||
|
||||
enum Expected {
|
||||
case decoding(PureYAML.Decoding.Error)
|
||||
case validation([PureYAML.Validation.Issue], String)
|
||||
}
|
||||
|
||||
var name: String
|
||||
var value: PureYAML.Model.Value
|
||||
var operation: Operation
|
||||
var expected: Expected
|
||||
|
||||
var description: String {
|
||||
name
|
||||
}
|
||||
|
||||
func expectFailure() {
|
||||
do {
|
||||
switch operation {
|
||||
case .decodeIntegers:
|
||||
_ = try PureYAML.decode([Int].self, from: value)
|
||||
case .decodeNestedIntegers:
|
||||
_ = try PureYAML.decode([[Int]].self, from: value)
|
||||
case .decodeArticles:
|
||||
_ = try PureYAML.decode([SequenceArticle].self, from: value)
|
||||
case .decodeCatalog:
|
||||
_ = try PureYAML.decode(SequenceCatalog.self, from: value)
|
||||
}
|
||||
recordIssue("expected fixture failure")
|
||||
} catch let error as PureYAML.Decoding.Error {
|
||||
guard case let .decoding(expected) = expected else {
|
||||
recordIssue("unexpected decoding error \(error)")
|
||||
return
|
||||
}
|
||||
#expect(error == expected)
|
||||
#expect(error.description == expected.description)
|
||||
} catch let collection as PureYAML.Validation.Issue.Collection {
|
||||
guard case let .validation(issues, description) = expected else {
|
||||
recordIssue("unexpected validation error \(collection)")
|
||||
return
|
||||
}
|
||||
#expect(collection.issues == issues)
|
||||
#expect(collection.description == description)
|
||||
} catch {
|
||||
recordIssue("unexpected error \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sequenceFailureFixtures: [SequenceFailureFixture] = [
|
||||
.init(
|
||||
name: "sequence root is required",
|
||||
value: .string("not a sequence"),
|
||||
operation: .decodeIntegers,
|
||||
expected: .decoding(.typeMismatch(expected: "sequence", actual: "string", path: .root)),
|
||||
),
|
||||
.init(
|
||||
name: "integer sequence rejects string element",
|
||||
value: .sequence([.int(1), .string("two")]),
|
||||
operation: .decodeIntegers,
|
||||
expected: .decoding(.typeMismatch(expected: "Int", actual: "string", path: .init([.index(1)]))),
|
||||
),
|
||||
.init(
|
||||
name: "nested integer sequence rejects bool element",
|
||||
value: .sequence([
|
||||
.sequence([.int(1), .bool(false)]),
|
||||
]),
|
||||
operation: .decodeNestedIntegers,
|
||||
expected: .decoding(.typeMismatch(
|
||||
expected: "Int",
|
||||
actual: "bool",
|
||||
path: .init([.index(0), .index(1)]),
|
||||
)),
|
||||
),
|
||||
.init(
|
||||
name: "article sequence rejects missing title",
|
||||
value: .sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "tags", value: .sequence([])),
|
||||
.init(key: "ranks", value: .sequence([])),
|
||||
])),
|
||||
]),
|
||||
operation: .decodeArticles,
|
||||
expected: .decoding(.keyNotFound(key: "title", path: .init([.index(0), .key("title")]))),
|
||||
),
|
||||
.init(
|
||||
name: "catalog rejects non-sequence tags",
|
||||
value: .mapping(.init([
|
||||
.init(key: "articles", value: .sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "title", value: .string("Typed YAML")),
|
||||
.init(key: "tags", value: .string("Swift")),
|
||||
.init(key: "ranks", value: .sequence([])),
|
||||
])),
|
||||
])),
|
||||
.init(key: "featured", value: .sequence([])),
|
||||
.init(key: "empty", value: .sequence([])),
|
||||
])),
|
||||
operation: .decodeCatalog,
|
||||
expected: .decoding(.typeMismatch(
|
||||
expected: "sequence",
|
||||
actual: "string",
|
||||
path: .init([.key("articles"), .index(0), .key("tags")]),
|
||||
)),
|
||||
),
|
||||
.init(
|
||||
name: "duplicate keys in sequence are validation failures before decoding",
|
||||
value: .sequence([
|
||||
.mapping(.init([
|
||||
.init(key: "title", value: .string("First")),
|
||||
.init(key: "title", value: .string("Second")),
|
||||
.init(key: "tags", value: .sequence([])),
|
||||
.init(key: "ranks", value: .sequence([])),
|
||||
])),
|
||||
]),
|
||||
operation: .decodeArticles,
|
||||
expected: .validation(
|
||||
[
|
||||
.init(
|
||||
severity: .error,
|
||||
reason: "Duplicate mapping key 'title'",
|
||||
path: .init([.index(0), .key("title")]),
|
||||
),
|
||||
],
|
||||
"error: Duplicate mapping key 'title' at $[0].title",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -55,6 +55,7 @@ Before claiming completion, run:
|
||||
```sh
|
||||
bash scripts/check-style.sh
|
||||
bash scripts/check-namespacing.sh
|
||||
bash scripts/check-forbidden-patterns.sh
|
||||
bash scripts/check-changelog-touched.sh
|
||||
swiftformat . --config .swiftformat --lint
|
||||
swiftlint --config .swiftlint.yml --strict
|
||||
|
||||
@ -10,6 +10,7 @@ Run this before committing or reporting completion:
|
||||
```sh
|
||||
bash scripts/check-style.sh
|
||||
bash scripts/check-namespacing.sh
|
||||
bash scripts/check-forbidden-patterns.sh
|
||||
bash scripts/check-changelog-touched.sh
|
||||
swiftformat . --config .swiftformat --lint
|
||||
swiftlint --config .swiftlint.yml --strict
|
||||
@ -25,6 +26,7 @@ bash scripts/check-wasm.sh
|
||||
|---|---|
|
||||
| Style clean | `scripts/check-style.sh` exits 0 |
|
||||
| Namespace clean | `scripts/check-namespacing.sh` exits 0 |
|
||||
| Forbidden-pattern clean | `scripts/check-forbidden-patterns.sh` exits 0 |
|
||||
| Format clean | `swiftformat . --config .swiftformat --lint` exits 0 |
|
||||
| Lint clean | `swiftlint --config .swiftlint.yml --strict` exits 0 |
|
||||
| Builds on macOS | `swift build` exits 0 |
|
||||
|
||||
@ -15,6 +15,7 @@ run_gate() {
|
||||
|
||||
run_gate "style" bash scripts/check-style.sh
|
||||
run_gate "namespacing" bash scripts/check-namespacing.sh
|
||||
run_gate "forbidden patterns" bash scripts/check-forbidden-patterns.sh
|
||||
run_gate "changelog" bash scripts/check-changelog-touched.sh
|
||||
run_gate "roadmap" bash scripts/check-roadmap.sh
|
||||
run_gate "swiftformat" swiftformat . --config .swiftformat --lint
|
||||
|
||||
85
scripts/check-forbidden-patterns.sh
Normal file
85
scripts/check-forbidden-patterns.sh
Normal file
@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Repo contract gate for PureYAML "never" rules.
|
||||
#
|
||||
# These checks cover patterns that Swift unit tests cannot see reliably:
|
||||
# package dependencies, source imports, C/generated parser files, XCTest-style
|
||||
# tests, platform branches, and singleton/service-locator wiring.
|
||||
|
||||
set -u
|
||||
|
||||
FAIL=0
|
||||
|
||||
fail() {
|
||||
echo "forbidden-patterns: $1" >&2
|
||||
FAIL=1
|
||||
}
|
||||
|
||||
report_matches() {
|
||||
message="$1"
|
||||
matches="$2"
|
||||
if [ -n "$matches" ]; then
|
||||
fail "$message"
|
||||
printf '%s\n' "$matches" | sed 's/^/ /' >&2
|
||||
fi
|
||||
}
|
||||
|
||||
if ! grep -qE '^[[:space:]]*dependencies:[[:space:]]*\[[[:space:]]*\],[[:space:]]*$' Package.swift; then
|
||||
fail "Package.swift must keep dependencies: []"
|
||||
fi
|
||||
|
||||
package_forbidden=$(
|
||||
grep -nE '\.package[[:space:]]*\(|\.binaryTarget[[:space:]]*\(|\.systemLibrary[[:space:]]*\(' Package.swift 2>/dev/null || true
|
||||
)
|
||||
report_matches "Package.swift must not declare external, binary, or system-library targets" "$package_forbidden"
|
||||
|
||||
source_imports=$(
|
||||
grep -RInE '^[[:space:]]*import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' Sources --include '*.swift' 2>/dev/null || true
|
||||
)
|
||||
report_matches "Sources must stay standard-library-only and import no modules" "$source_imports"
|
||||
|
||||
source_platform_branches=$(
|
||||
grep -RInE '^[[:space:]]*#if[[:space:]]+(os|canImport)[[:space:]]*\(' Sources --include '*.swift' 2>/dev/null || true
|
||||
)
|
||||
report_matches "Sources must not add platform-specific branches without an explicit rule change" "$source_platform_branches"
|
||||
|
||||
source_runtimes=$(
|
||||
find Sources -type f \( \
|
||||
-name '*.c' -o \
|
||||
-name '*.cc' -o \
|
||||
-name '*.cpp' -o \
|
||||
-name '*.h' -o \
|
||||
-name '*.hpp' -o \
|
||||
-name '*.m' -o \
|
||||
-name '*.mm' -o \
|
||||
-name '*.js' -o \
|
||||
-name '*.ts' -o \
|
||||
-name '*.wasm' -o \
|
||||
-name '*.y' -o \
|
||||
-name '*.l' -o \
|
||||
-name '*.g4' \
|
||||
\) -print 2>/dev/null
|
||||
)
|
||||
report_matches "Sources must not contain C, JavaScript, WebAssembly blobs, or generated parser runtime files" "$source_runtimes"
|
||||
|
||||
source_singletons=$(
|
||||
grep -RInE 'static[[:space:]]+(let|var)[[:space:]]+shared\b|^[[:space:]]*(public[[:space:]]+)?(final[[:space:]]+)?(class|struct|enum)[[:space:]]+ServiceLocator\b|[A-Za-z0-9_]*(Resolver|Container|Environment)[[:space:]]*\.[[:space:]]*shared\b' Sources --include '*.swift' 2>/dev/null || true
|
||||
)
|
||||
report_matches "Sources must not introduce singleton or service-locator access patterns" "$source_singletons"
|
||||
|
||||
test_imports=$(
|
||||
grep -RInE '^[[:space:]]*(@testable[[:space:]]+)?import[[:space:]]+[A-Za-z_][A-Za-z0-9_]*' Tests --include '*.swift' 2>/dev/null \
|
||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*@testable[[:space:]]+import[[:space:]]+PureYAML[[:space:]]*$' \
|
||||
| grep -vE '^[^:]+:[0-9]+:[[:space:]]*import[[:space:]]+Testing[[:space:]]*$' || true
|
||||
)
|
||||
report_matches "Tests may import only Testing and @testable PureYAML" "$test_imports"
|
||||
|
||||
test_xctest=$(
|
||||
grep -RInE '\bXCTest(Case|Expectation)?\b|\bXCTAssert[A-Za-z]*\b|^[[:space:]]*import[[:space:]]+XCTest[[:space:]]*$' Tests --include '*.swift' 2>/dev/null || true
|
||||
)
|
||||
report_matches "Tests must stay on Swift Testing and avoid XCTest-style APIs" "$test_xctest"
|
||||
|
||||
if [ "$FAIL" -eq 0 ]; then
|
||||
echo "forbidden-patterns: OK"
|
||||
fi
|
||||
|
||||
exit "$FAIL"
|
||||
@ -20,6 +20,18 @@ while IFS= read -r f; do
|
||||
echo "namespacing: $count file-scope types in $f (one per file)" >&2
|
||||
FAIL=1
|
||||
fi
|
||||
|
||||
public_types=$(grep -nE "^public (actor|struct|enum|protocol|class|final class) [A-Z]" "$f" || true)
|
||||
if [ -n "$public_types" ]; then
|
||||
if [ "$f" = "Sources/PureYAML.swift" ] &&
|
||||
[ "$public_types" = "2:public enum PureYAML {" ]; then
|
||||
:
|
||||
else
|
||||
echo "namespacing: public file-scope type in $f (wrap it under PureYAML)" >&2
|
||||
printf '%s\n' "$public_types" | sed 's/^/ /' >&2
|
||||
FAIL=1
|
||||
fi
|
||||
fi
|
||||
done < <(find "$SRC" -name "*.swift")
|
||||
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
|
||||
Loading…
Reference in New Issue
Block a user