├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── documentation.yml │ ├── feature_request.yml │ ├── maintenance.yml │ └── question.yml ├── pull_request_template.md ├── scripts │ └── validate-pr-title.js └── workflows │ ├── ci.yml │ ├── pr-labels.yml │ └── pr-title.yml ├── .gitignore ├── .swiftformat ├── Brewfile ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftSyntaxSugar │ ├── AccessLevelSyntax │ └── AccessLevelSyntax.swift │ ├── AccessorBlockSyntax │ └── AccessorBlockSyntax+Accessors.swift │ ├── AccessorDeclSyntax │ └── AccessorDeclSyntax+Effects.swift │ ├── ActorDeclSyntax │ └── ActorDeclSyntax+AccessLevel.swift │ ├── ClassDeclSyntax │ └── ClassDeclSyntax+AccessLevel.swift │ ├── DeclModifierListSyntax │ └── DeclModifierListSyntax+AccessLevel.swift │ ├── DeclModifierSyntax │ └── DeclModifierSyntax+AccessLevel.swift │ ├── FunctionDeclSyntax │ ├── FunctionDeclSyntax+AccessLevel.swift │ └── FunctionDeclSyntax+Effects.swift │ ├── FunctionParameterSyntax │ └── FunctionParameterSyntax+IsVariadic.swift │ ├── FunctionTypeSyntax │ └── FunctionTypeSyntax+Escaping.swift │ ├── InheritanceClauseSyntax │ └── InheritanceClauseSyntax+InheritedTypes.swift │ ├── InitializerDeclSyntax │ └── InitializerDeclSyntax+AccessLevel.swift │ ├── MemberBlockSyntax │ └── MemberBlockSyntax+MemberDeclarations.swift │ ├── ProtocolDeclSyntax │ ├── ProtocolDeclSyntax+AccessLevel.swift │ ├── ProtocolDeclSyntax+Actor.swift │ ├── ProtocolDeclSyntax+GenericWhereClauses.swift │ └── ProtocolDeclSyntax+Type.swift │ ├── StructDeclSyntax │ └── StructDeclSyntax+AccessLevel.swift │ ├── SyntaxProtocol │ ├── SyntaxProtocol+WithAttributeListSyntax.swift │ ├── SyntaxProtocol+WithCodeBlockSyntax.swift │ └── SyntaxProtocol+WithDeclModifierListSyntax.swift │ ├── TypeSyntax │ └── TypeSyntax+Describing.swift │ └── VariableDeclSyntax │ └── VariableDeclSyntax+AccessLevel.swift ├── Tests └── SwiftSyntaxSugarTests │ ├── AccessLevelSyntax │ └── AccessLevelSyntaxTests.swift │ ├── AccessorBlockSyntax │ └── AccessorBlockSyntax_AccessorsTests.swift │ ├── AccessorDeclSyntax │ └── AccessorDeclSyntax_EffectsTests.swift │ ├── ActorDeclSyntax │ └── ActorDeclSyntax_AccessLevelTests.swift │ ├── ClassDeclSyntax │ └── ClassDeclSyntax_AccessLevelTests.swift │ ├── DeclModifierListSyntax │ └── DeclModifierListSyntax_AccessLevelTests.swift │ ├── DeclModifierSyntax │ └── DeclModifierSyntax_AccessLevelTests.swift │ ├── FunctionDeclSyntax │ ├── FunctionDeclSyntax_AccessLevelTests.swift │ └── FunctionDeclSyntax_EffectsTests.swift │ ├── FunctionParameterSyntax │ └── FunctionParameterSyntax_IsVariadicTests.swift │ ├── FunctionTypeSyntax │ └── FunctionTypeSyntax_EscapingTests.swift │ ├── InheritanceClauseSyntax │ └── InheritanceClauseSyntax_InheritedTypesTests.swift │ ├── InitializerDeclSyntax │ └── InitializerDeclSyntax_AccessLevelTests.swift │ ├── MemberBlockSyntax │ └── MemberBlockSyntax_MemberDeclarationsTests.swift │ ├── ProtocolDeclSyntax │ ├── ProtocolDeclSyntax_AccessLevelTests.swift │ ├── ProtocolDeclSyntax_ActorTests.swift │ ├── ProtocolDeclSyntax_GenericWhereClausesTests.swift │ └── ProtocolDeclSyntax_TypeTests.swift │ ├── StructDeclSyntax │ └── StructDeclSyntax_AccessLevelTests.swift │ ├── SyntaxProtocol │ ├── SyntaxProtocol_WithAttributeListSyntaxTests.swift │ ├── SyntaxProtocol_WithCodeBlockSyntaxTests.swift │ └── SyntaxProtocol_WithDeclModifierListSyntaxTests.swift │ ├── TypeSyntax │ └── TypeSyntax_DescribingTests.swift │ └── VariableDeclSyntax │ └── VariableDeclSyntax_AccessLevelTests.swift └── codecov.yml /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug or unexpected behavior 3 | title: "[Bug] " 4 | labels: [bug] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ### Thanks for reporting a bug! 12 | Please fill out the form below with as much detail as possible. 13 | 14 | - type: input 15 | id: swift_version 16 | attributes: 17 | label: Swift Version 18 | placeholder: "e.g. 6.1" 19 | validations: 20 | required: true 21 | 22 | - type: input 23 | id: package_version 24 | attributes: 25 | label: Package Version 26 | placeholder: "e.g. 1.2.0 or main" 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: description 32 | attributes: 33 | label: Bug Description 34 | description: A clear and concise description of the bug. 35 | placeholder: "What happened?" 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: steps 41 | attributes: 42 | label: Steps to Reproduce 43 | description: Describe the steps needed to reproduce the issue. 44 | placeholder: "If describing an issue related to code, please provide an isolated code example that demonstrates the issue using as little code as possible." 45 | validations: 46 | required: true 47 | 48 | - type: textarea 49 | id: expected_behavior 50 | attributes: 51 | label: Expected Behavior 52 | description: What did you expect to happen? 53 | validations: 54 | required: false 55 | 56 | - type: textarea 57 | id: actual_behavior 58 | attributes: 59 | label: Actual Behavior 60 | description: What actually happened? 61 | validations: 62 | required: false 63 | 64 | - type: textarea 65 | id: logs 66 | attributes: 67 | label: Stack Trace / Logs 68 | description: Paste any error messages or logs. 69 | render: shell 70 | validations: 71 | required: false 72 | 73 | - type: textarea 74 | id: additional_context 75 | attributes: 76 | label: Additional Context 77 | description: Add screenshots, links to related issues, or any other useful info. 78 | validations: 79 | required: false 80 | 81 | - type: checkboxes 82 | id: terms 83 | attributes: 84 | label: Code of Conduct 85 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 86 | options: 87 | - label: I agree to follow this project's Code of Conduct 88 | required: true 89 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Request changes to documentation 3 | title: "[Documentation] " 4 | labels: [documentation] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | id: location 10 | attributes: 11 | label: Where is the documentation issue? 12 | description: Please provide a link or describe the section of the documentation that needs attention. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: problem 18 | attributes: 19 | label: What is the problem? 20 | description: Explain what's wrong (typo, unclear instructions, outdated info, etc.). 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: suggested_change 26 | attributes: 27 | label: Suggested fix or improvement 28 | description: If you have a suggestion for how to fix the issue, please describe it here. 29 | validations: 30 | required: false 31 | 32 | - type: checkboxes 33 | id: terms 34 | attributes: 35 | label: Code of Conduct 36 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 37 | options: 38 | - label: I agree to follow this project's Code of Conduct 39 | required: true 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Propose a new feature or enhancement 3 | title: "[Feature] " 4 | labels: [enhancement] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ### Got an idea? 12 | Share your feature request or enhancement so we can consider adding it. 13 | 14 | - type: input 15 | id: use_case 16 | attributes: 17 | label: Use Case 18 | description: What problem does this feature solve? 19 | placeholder: "I'm trying to..." 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: proposal 25 | attributes: 26 | label: Feature Proposal 27 | description: Describe the feature you’d like to see. 28 | placeholder: "I would like to see..." 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: alternatives 34 | attributes: 35 | label: Alternatives Considered 36 | description: Have you considered any alternatives or workarounds? 37 | placeholder: "I've tried..." 38 | validations: 39 | required: false 40 | 41 | - type: textarea 42 | id: additional_context 43 | attributes: 44 | label: Additional Context 45 | description: Add screenshots, examples, or any other helpful information. 46 | validations: 47 | required: false 48 | 49 | - type: checkboxes 50 | id: terms 51 | attributes: 52 | label: Code of Conduct 53 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 54 | options: 55 | - label: I agree to follow this project's Code of Conduct 56 | required: true 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/maintenance.yml: -------------------------------------------------------------------------------- 1 | name: Maintenance 2 | description: Suggest internal improvements unrelated to features or bugs 3 | title: "[Maintenance] " 4 | labels: [] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | id: area 10 | attributes: 11 | label: What area needs maintenance? 12 | description: Specify what part of the project you want to improve. 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: reason 18 | attributes: 19 | label: Why is this improvement needed? 20 | description: Explain the benefit (e.g. performance, test coverage, code quality, build speed). 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: proposed_solution 26 | attributes: 27 | label: Proposed solution 28 | description: If you have a suggestion for how to fix the issue, please describe it here. 29 | validations: 30 | required: false 31 | 32 | - type: checkboxes 33 | id: terms 34 | attributes: 35 | label: Code of Conduct 36 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 37 | options: 38 | - label: I agree to follow this project's Code of Conduct 39 | required: true 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.yml: -------------------------------------------------------------------------------- 1 | name: Question 2 | description: Ask a question or request clarification 3 | title: "[Question] " 4 | labels: [question] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ### Thanks for asking a question! 12 | > [!Important] 13 | > Please consider using [Discussions](https://github.com/fetch-rewards/SwiftSyntaxSugar/discussions) to ask your question. There, you can receive answers from maintainers as well as community members. While we do provide this form for submitting questions, we prefer to reserve issues for things that need to be fixed, changed, or added. If you are sharing feedback, asking for help or opinions, or looking to brainstorm ideas with other members of the community, please use [Discussions](https://github.com/fetch-rewards/SwiftSyntaxSugar/discussions). 14 | 15 | - type: textarea 16 | id: question 17 | attributes: 18 | label: What is your question? 19 | description: Please be clear and specific. 20 | validations: 21 | required: true 22 | 23 | - type: checkboxes 24 | id: checklist 25 | attributes: 26 | label: Checklist 27 | options: 28 | - label: I have searched existing [Issues](https://github.com/fetch-rewards/SwiftSyntaxSugar/issues) and [Discussions](https://github.com/fetch-rewards/SwiftSyntaxSugar/discussions). 29 | required: true 30 | - label: This is not a bug report or feature request. 31 | required: true 32 | 33 | - type: checkboxes 34 | id: terms 35 | attributes: 36 | label: Code of Conduct 37 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 38 | options: 39 | - label: I agree to follow this project's Code of Conduct 40 | required: true 41 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📝 Summary 2 | 3 | 7 | 8 | ## 🛠️ Type of Change 9 | 10 | 11 | 12 | - [ ] Bug fix (change that fixes an issue) 13 | - [ ] New feature (change that adds functionality) 14 | - [ ] Breaking change (bug fix or feature that is not backwards compatible) 15 | - [ ] Documentation (DocC, API docs, markdown files, templates, etc.) 16 | - [ ] Testing (new tests, updated tests, etc.) 17 | - [ ] Refactoring or code formatting (no logic changes) 18 | - [ ] Updating dependencies (Swift packages, Homebrew, etc.) 19 | - [ ] CI/CD (change to automated workflows) 20 | 21 | ## 🧪 How Has This Been Tested? 22 | 23 | 28 | 29 | ## 🔗 Related PRs or Issues 30 | 31 | 37 | 38 | 45 | 46 | ## ✅ Checklist 47 | 48 | 49 | 50 | - [ ] I have added relevant tests 51 | - [ ] I have verified all tests pass 52 | - [ ] I have formatted my code using `SwiftFormat` 53 | - [ ] I have updated documentation (if needed) 54 | - [ ] I have added the appropriate label to my PR 55 | - [ ] I have read the [contributing guidelines](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CONTRIBUTING.md) 56 | - [ ] I agree to follow this project's [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md) 57 | -------------------------------------------------------------------------------- /.github/scripts/validate-pr-title.js: -------------------------------------------------------------------------------- 1 | const nlp = require('compromise'); 2 | const title = process.env.PR_TITLE || ''; 3 | 4 | let isValidTitle = true; 5 | 6 | function logSuccess(message) { 7 | console.log(`✅ ${message}`); 8 | } 9 | 10 | function logFailure(message) { 11 | isValidTitle = false; 12 | console.error(`❌ ${message}`); 13 | } 14 | 15 | function capitalized(string) { 16 | if (!string) return ''; 17 | return string[0].toUpperCase() + string.substring(1); 18 | } 19 | 20 | // Rule 1: PR title must not be empty 21 | if (title) { 22 | logSuccess(`PR title is not empty`); 23 | } else { 24 | logFailure(`PR title must not be empty`); 25 | } 26 | 27 | // Rule 2: PR title must be 72 characters or less 28 | if (title.length <= 72) { 29 | logSuccess(`PR title is ${title.length} characters`); 30 | } else { 31 | logFailure(`PR title must be 72 characters or less (currently ${title.length} characters)`); 32 | } 33 | 34 | // Rule 3: PR title must begin with a capital letter 35 | if (/^[A-Z]/.test(title)) { 36 | logSuccess(`PR title begins with a capital letter`); 37 | } else { 38 | logFailure('PR title must begin with a capital letter'); 39 | } 40 | 41 | // Rule 4: PR title must end with a letter or number 42 | if (/[A-Za-z0-9]$/.test(title)) { 43 | logSuccess(`PR title ends with a letter or number`); 44 | } else { 45 | logFailure('PR title must end with a letter or number'); 46 | } 47 | 48 | // Rule 5: PR title must be written in the imperative 49 | const firstWord = title.split(' ')[0]; 50 | const firstWordLowercased = firstWord.toLowerCase(); 51 | const firstWordCapitalized = capitalized(firstWord); 52 | const firstWordAsImperativeVerb = nlp(firstWord).verbs().toInfinitive().out('text'); 53 | const firstWordAsImperativeVerbLowercased = firstWordAsImperativeVerb.toLowerCase(); 54 | const firstWordAsImperativeVerbCapitalized = capitalized(firstWordAsImperativeVerb); 55 | 56 | if (firstWordLowercased === firstWordAsImperativeVerbLowercased) { 57 | logSuccess(`PR title is written in the imperative`); 58 | } else if (firstWordAsImperativeVerb) { 59 | logFailure(`PR title must be written in the imperative ("${firstWordAsImperativeVerbCapitalized}" instead of "${firstWordCapitalized}")`); 60 | } else { 61 | logFailure(`PR title must begin with a verb and be written in the imperative`); 62 | } 63 | 64 | if (!isValidTitle) { 65 | process.exit(1); 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: [opened, reopened, synchronize] 10 | branches: 11 | - '*' 12 | 13 | jobs: 14 | build-and-test: 15 | name: Build, Test, & Report Coverage 16 | runs-on: macos-latest 17 | 18 | steps: 19 | - name: Checkout source code 20 | uses: actions/checkout@v4 21 | 22 | - name: Set up Xcode 23 | uses: maxim-lobanov/setup-xcode@v1 24 | with: 25 | xcode-version: latest 26 | 27 | - name: Install Brewfile dependencies 28 | run: | 29 | brew update --quiet 30 | brew bundle install 31 | 32 | - name: Run SwiftFormat in lint mode 33 | run: | 34 | swiftformat --version 35 | swiftformat --lint . 36 | 37 | - name: Install package dependencies 38 | run: swift package resolve 39 | 40 | - name: Build the package 41 | run: swift build 42 | 43 | - name: Run tests with code coverage 44 | run: swift test --enable-code-coverage 45 | 46 | - name: Gather code coverage 47 | run: xcrun llvm-cov export -format="lcov" .build/debug/SwiftSyntaxSugarPackageTests.xctest/Contents/MacOS/SwiftSyntaxSugarPackageTests -instr-profile .build/debug/codecov/default.profdata > coverage_report.lcov 48 | 49 | - name: Upload coverage to Codecov 50 | uses: codecov/codecov-action@v5 51 | with: 52 | files: coverage_report.lcov 53 | token: ${{ secrets.CODECOV_TOKEN }} 54 | slug: fetch-rewards/SwiftSyntaxSugar 55 | verbose: true 56 | fail_ci_if_error: true 57 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yml: -------------------------------------------------------------------------------- 1 | name: PR Labels 2 | 3 | on: 4 | pull_request: 5 | types: [opened, labeled, unlabeled, reopened, synchronize] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: read 10 | 11 | jobs: 12 | check-for-required-labels: 13 | name: Check For Required Labels 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout source code 18 | uses: actions/checkout@v4 19 | 20 | - name: Validate PR has required labels 21 | env: 22 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | run: | 24 | PR_NUMBER=${{ github.event.pull_request.number }} 25 | REPO=${{ github.repository }} 26 | 27 | REQUIRED_LABELS=("bug" "ci/cd" "dependencies" "documentation" "enhancement" "formatting" "refactoring" "testing") 28 | LABELS=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name') 29 | 30 | echo "PR labels:" 31 | echo "$LABELS" 32 | 33 | for required in "${REQUIRED_LABELS[@]}"; do 34 | if echo "$LABELS" | grep -q "^$required$"; then 35 | echo "✅ Found required label: $required" 36 | exit 0 37 | fi 38 | done 39 | 40 | echo "❌ PR is missing a required label." 41 | echo "At least one of the following labels is required:" 42 | printf '%s\n' "${REQUIRED_LABELS[@]}" 43 | exit 1 44 | shell: bash 45 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: PR Title 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, reopened, synchronize] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: read 10 | 11 | jobs: 12 | validate-pr-title: 13 | name: Validate PR Title 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout source code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: latest 23 | 24 | - name: Install dependencies 25 | run: npm install compromise 26 | 27 | - name: Validate PR title 28 | run: node .github/scripts/validate-pr-title.js 29 | env: 30 | PR_TITLE: ${{ github.event.pull_request.title }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | ## User settings 4 | xcuserdata/ 5 | 6 | ## Obj-C/Swift specific 7 | *.hmap 8 | 9 | ## App Packaging 10 | *.ipa 11 | *.dSYM.zip 12 | *.dSYM 13 | 14 | ## Playgrounds 15 | timeline.xctimeline 16 | playground.xcworkspace 17 | 18 | # Swift Package Manager 19 | Packages/ 20 | Package.pins 21 | Package.resolved 22 | *.xcodeproj 23 | .swiftpm 24 | .build/ 25 | 26 | # Homebrew 27 | Brewfile.lock.json 28 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # Swift Version 2 | --swiftversion 6.0 3 | 4 | # SwiftFormat Version 5 | --minversion 0.56.2 6 | 7 | # Options 8 | 9 | ## Acronyms to auto-capitalize. Defaults to ID,URL,UUID. 10 | --acronyms ID,URL,UUID,API 11 | 12 | ## Date format to use in file headers: system (default), iso, dmy, mdy, or custom. 13 | --dateformat mdy 14 | 15 | ## Change type with only static members to enum: always (default) or structs-only. 16 | --enumnamespaces structs-only 17 | 18 | ## Place ACL on-extension (default) or on-declarations. 19 | --extensionacl on-declarations 20 | 21 | ## Function @attributes: preserve, prev-line, or same-line. 22 | --funcattributes prev-line 23 | 24 | ## Mark for extension grouped with extended type: "MARK: %c" (default). 25 | --groupedextension "MARK: - %c" 26 | 27 | ## Header comments: strip, ignore, or the text you wish use. 28 | --header \n {file}\n\n Copyright © {year} Fetch.\n 29 | 30 | ## #if indenting: indent (default), no-indent, or outdent. 31 | --ifdef no-indent 32 | 33 | ## testable-first/last, alpha (default), or length. 34 | --importgrouping testable-last 35 | 36 | ## Number of spaces to indent, or tab to use tabs. 37 | --indent 4 38 | 39 | ## Mark extensions always (default), never, if-not-empty 40 | --markextensions never 41 | 42 | ## Mark types always (default), never, or if-not-empty 43 | --marktypes never 44 | 45 | ## Maximum length of a line before wrapping or none (default). 46 | --maxwidth 100 47 | 48 | ## Spacing for ranges: spaced (default) or no-space. 49 | --ranges no-space 50 | 51 | ## Redundant type: inferred, explicit, or infer-locals-only (default). 52 | --redundanttype inferred 53 | 54 | ## Explicit self: insert, remove (default), or init-only. 55 | --self insert 56 | 57 | ## Allow semicolons: never or inline (default). 58 | --semicolons never 59 | 60 | ## Mark unused function arguments with _: closure-only, unnamed-only, or always (default). 61 | --stripunusedargs closure-only 62 | 63 | ## The width of a tab character. Defaults to unspecified. 64 | --tabwidth 4 65 | 66 | ## Trim trailing space: always (default) or nonblank-lines. 67 | --trimwhitespace always 68 | 69 | ## Type @attributes: preserve, prev-line, or same-line. 70 | --typeattributes prev-line 71 | 72 | ## Blank lines from types: remove (default) or preserve. 73 | --typeblanklines preserve 74 | 75 | ## Property @attributes: preserve, prev-line, or same-line. 76 | --varattributes preserve 77 | 78 | ## Wrap all arguments: before-first, after-first, preserve. 79 | --wraparguments before-first 80 | 81 | ## Wrap array/dict: before-first, after-first, preserve. 82 | --wrapcollections before-first 83 | 84 | ## Wrap conditions: before-first, after-first, preserve. 85 | --wrapconditions preserve 86 | 87 | ## Wrap effects: if-multiline, never, preserve. 88 | --wrapeffects never 89 | 90 | ## Wrap func params: before-first, after-first, preserve 91 | --wrapparameters before-first 92 | 93 | ## Wrap ternary operators: default, before-operators 94 | --wrapternary before-operators 95 | 96 | ## Wrap typealiases: before-first, after-first, preserve 97 | --wraptypealiases before-first 98 | 99 | # Rules 100 | 101 | ## Capitalize acronyms when the first character is capitalized. 102 | ### Options: 103 | ### --acronyms (default: ID,URL,UUID) 104 | --rules acronyms 105 | 106 | ## Prefer comma over && in if, guard or while conditions. 107 | --rules andOperator 108 | 109 | ## Prefer AnyObject over class in protocol definitions. 110 | --rules anyObjectProtocol 111 | 112 | ## Replace obsolete @UIApplicationMain and @NSApplicationMain attributes 113 | ## with @main for Swift 5.3 and above. 114 | --rules applicationMain 115 | 116 | ## Changes all instances of assert(false, ...) to assertionFailure(...) 117 | ## and precondition(false, ...) to preconditionFailure(...). 118 | --rules assertionFailures 119 | 120 | ## Insert blank line after import statements. 121 | --rules blankLineAfterImports 122 | 123 | ## Insert blank line before and after MARK: comments. 124 | ### Options: 125 | ### --lineaftermarks (default: true) 126 | --rules blankLinesAroundMark 127 | 128 | ## Remove trailing blank line at the end of a scope. 129 | --rules blankLinesAtEndOfScope 130 | 131 | ## Remove leading blank line at the start of a scope. 132 | ### Options: 133 | ### --typeblanklines (default: remove) 134 | --rules blankLinesAtStartOfScope 135 | 136 | ## Remove blank lines between chained functions but keep the linebreaks. 137 | --rules blankLinesBetweenChainedFunctions 138 | 139 | ## Remove blank lines between import statements. 140 | --rules blankLinesBetweenImports 141 | 142 | ## Insert blank line before class, struct, enum, extension, protocol or function declarations. 143 | --rules blankLinesBetweenScopes 144 | 145 | ## Convert block comments to consecutive single line comments. 146 | --rules blockComments 147 | 148 | ## Wrap braces in accordance with selected style (K&R or Allman). 149 | ### Options: 150 | ### --allman (default: false) 151 | --rules braces 152 | 153 | ## Assign properties using if / switch expressions. 154 | --rules conditionalAssignment 155 | 156 | ## Replace consecutive blank lines with a single blank line. 157 | --rules consecutiveBlankLines 158 | 159 | ## Replace consecutive spaces with a single space. 160 | --rules consecutiveSpaces 161 | 162 | ## Use doc comments for API declarations, otherwise use regular comments. 163 | --rules docComments 164 | 165 | ## Remove duplicate import statements. 166 | --rules duplicateImports 167 | 168 | ## Place else, catch, or while keyword in accordance with current style (same or next line). 169 | ### Options: 170 | ### --elseposition (default: same-line) 171 | ### --guardelse (default: auto) 172 | --rules elseOnSameLine 173 | 174 | ## Remove whitespace inside empty braces. 175 | ### Options: 176 | ### --emptybraces (default: no-space) 177 | --rules emptyBraces 178 | 179 | ## Convert types used for hosting only static members into enums 180 | ## (an empty enum is the canonical way to create a namespace in Swift as it can't be instantiated). 181 | ### Options: 182 | ### --enumnamespaces (default: always) 183 | --rules enumNamespaces 184 | 185 | ## Configure the placement of an extension's access control keyword. 186 | ### Options: 187 | ### --extensionacl (default: on-extension) 188 | --rules extensionAccessControl 189 | 190 | ## Use specified source file header template for all files. 191 | ### Options: 192 | ### --header 193 | --rules fileHeader 194 | 195 | ## Use angle brackets (extension Array) for generic type extensions 196 | ## instead of type constraints (extension Array where Element == Foo). 197 | ### Options: 198 | ### --generictypes 199 | --rules genericExtensions 200 | 201 | ## Ensure file name in header comment matches the actual file name. 202 | --rules headerFileName 203 | 204 | ## Reposition let or var bindings within pattern. 205 | ### Options: 206 | ### --patternlet (default: hoist) 207 | --rules hoistPatternLet 208 | 209 | ## Indent code in accordance with the scope level. 210 | ### Options: 211 | ### --indent 212 | ### --tabwidth (default: unspecified) 213 | ### --smarttabs (default: enabled) 214 | ### --indentcase (default: false) 215 | ### --ifdef (default: indent) 216 | ### --xcodeindentation (default: disabled) 217 | ### --indentstrings (default: false) 218 | --rules indent 219 | 220 | ## Add @available(*, unavailable) attribute to required init(coder:) when it hasn't been implemented. 221 | --rules initCoderUnavailable 222 | 223 | ## Prefer isEmpty over comparing count against zero. 224 | --rules isEmpty 225 | 226 | ## Move leading delimiters to the end of the previous line. 227 | --rules leadingDelimiters 228 | 229 | ## Add empty blank line at end of file. 230 | --rules linebreakAtEndOfFile 231 | 232 | ## Use specified linebreak character for all linebreaks (CR, LF or CRLF). 233 | ### Options: 234 | ### --linebreaks (default: lf) 235 | --rules linebreaks 236 | 237 | ## Add a MARK comment before top-level types and extensions. 238 | ### Options: 239 | ### --marktypes (default: always) 240 | ### --typemark (default: MARK: - %t) 241 | ### --markextensions (default: always) 242 | ### --extensionmark (default: MARK: - %t + %c) 243 | ### --groupedextension (default: MARK: %c) 244 | --rules markTypes 245 | 246 | ## Use consistent ordering for member modifiers. 247 | ### Options: 248 | ### --modifierorder 249 | --rules modifierOrder 250 | 251 | ## Use consistent grouping for numeric literals. 252 | ## Groups will be separated by _ delimiters to improve readability. 253 | ## For each numeric type you can specify a group size (the number of digits in each group) 254 | ## and a threshold (the minimum number of digits in a number before grouping is applied). 255 | ### Options: 256 | ### --decimalgrouping (default: 3,6) 257 | ### --binarygrouping (default: 4,8) 258 | ### --octalgrouping (default: 4,8) 259 | ### --hexgrouping (default: 4,8) 260 | ### --fractiongrouping (default: disabled) 261 | ### --exponentgrouping (default: disabled) 262 | ### --hexliteralcase (default: uppercase) 263 | ### --exponentcase (default: uppercase) 264 | --rules numberFormatting 265 | 266 | ## Use opaque generic parameters (some Protocol) 267 | ## instead of generic parameters with constraints (T where T: Protocol, etc) where equivalent. 268 | ## Also supports primary associated types for common standard library types, 269 | ## so definitions like T where T: Collection, T.Element == Foo are updated to some Collection. 270 | ### Options: 271 | ### --someany (default: true) 272 | --rules opaqueGenericParameters 273 | 274 | ## Convert trivial map { $0.foo } closures to keyPath-based syntax. 275 | --rules preferKeyPath 276 | 277 | ## Remove redundant backticks around identifiers. 278 | --rules redundantBackticks 279 | 280 | ## Remove redundant break in switch case. 281 | --rules redundantBreak 282 | 283 | ## Removes redundant closures bodies, containing a single statement, which are called immediately. 284 | --rules redundantClosure 285 | 286 | ## Remove redundant access control modifiers. 287 | --rules redundantExtensionACL 288 | 289 | ## Prefer private over fileprivate where equivalent. 290 | --rules redundantFileprivate 291 | 292 | ## Remove unneeded get clause inside computed properties. 293 | --rules redundantGet 294 | 295 | ## Remove explicit init if not required. 296 | --rules redundantInit 297 | 298 | ## Remove redundant internal access control. 299 | --rules redundantInternal 300 | 301 | ## Remove redundant let/var from ignored variables. 302 | --rules redundantLet 303 | 304 | ## Remove redundant let error from catch clause. 305 | --rules redundantLetError 306 | 307 | ## Remove redundant nil default value (Optional vars are nil by default). 308 | --rules redundantNilInit 309 | 310 | ## Remove redundant @objc annotations. 311 | --rules redundantObjc 312 | 313 | ## Remove redundant identifiers in optional binding conditions. 314 | --rules redundantOptionalBinding 315 | 316 | ## Remove redundant parentheses. 317 | --rules redundantParens 318 | 319 | ## Remove redundant parentheses. 320 | --rules redundantPattern 321 | 322 | ## Remove redundant raw string values for enum cases. 323 | --rules redundantRawValues 324 | 325 | ## Remove unneeded return keyword. 326 | --rules redundantReturn 327 | 328 | ## Insert/remove explicit self where applicable. 329 | ### Options: 330 | ### --self (default: remove) 331 | ### --selfrequired 332 | --rules redundantSelf 333 | 334 | ## Remove redundant type from variable declarations. 335 | --rules redundantType 336 | 337 | ## Remove explicit Void return type. 338 | ### Options: 339 | ### --closurevoid (default: remove) 340 | --rules redundantVoidReturnType 341 | 342 | ## Remove semicolons. 343 | ### Options: 344 | ### --semicolons (default: inline). 345 | --rules semicolons 346 | 347 | ## Sorts the body of declarations with // swiftformat:sort 348 | ## and declarations between // swiftformat:sort:begin 349 | ## and // swiftformat:sort:end comments. 350 | --rules sortDeclarations 351 | 352 | ## Sort import statements alphabetically. 353 | ### Options: 354 | ### --importgrouping (default: alpha) 355 | --rules sortImports 356 | 357 | ## Sort switch cases alphabetically. 358 | --rules sortSwitchCases 359 | 360 | ## Sort protocol composition typealiases alphabetically. 361 | --rules sortTypealiases 362 | 363 | ## Add or remove space around curly braces. 364 | --rules spaceAroundBraces 365 | 366 | ## Add or remove space around square brackets. 367 | --rules spaceAroundBrackets 368 | 369 | ## Add space before and/or after comments. 370 | --rules spaceAroundComments 371 | 372 | ## Remove space around angle brackets. 373 | --rules spaceAroundGenerics 374 | 375 | ## Add or remove space around operators or delimiters. 376 | ### Options: 377 | ### --operatorfunc (default: spaced) 378 | ### --nospaceoperators 379 | ### --ranges (default: spaced) 380 | --rules spaceAroundOperators 381 | 382 | ## Add or remove space around parentheses. 383 | --rules spaceAroundParens 384 | 385 | ## Add space inside curly braces. 386 | --rules spaceInsideBraces 387 | 388 | ## Remove space inside square brackets. 389 | --rules spaceInsideBrackets 390 | 391 | ## Add leading and/or trailing space inside comments. 392 | --rules spaceInsideComments 393 | 394 | ## Remove space inside angle brackets. 395 | --rules spaceInsideGenerics 396 | 397 | ## Remove space inside parentheses. 398 | --rules spaceInsideParens 399 | 400 | ## Remove weak modifier from @IBOutlet properties. 401 | --rules strongOutlets 402 | 403 | ## Remove backticks around self in Optional unwrap expressions. 404 | --rules strongifiedSelf 405 | 406 | ## Use correct formatting for TODO:, MARK: or FIXME: comments. 407 | --rules todos 408 | 409 | ## Use trailing closure syntax where applicable. 410 | ### Options: 411 | ### --trailingclosures 412 | ### --nevertrailing 413 | --rules trailingClosures 414 | 415 | ## Add or remove trailing comma from the last item in a collection literal. 416 | ### Options: 417 | ### --commas (default: always) 418 | --rules trailingCommas 419 | 420 | ## Remove trailing space at end of a line. 421 | ### Options: 422 | ### --trimwhitespace (default: always) 423 | --rules trailingSpace 424 | 425 | ## Prefer shorthand syntax for Arrays, Dictionaries and Optionals. 426 | ### Options: 427 | ### --shortoptionals (default: always) 428 | --rules typeSugar 429 | 430 | ## Mark unused function arguments with _. 431 | ### Options: 432 | ### --stripunusedargs (default: always). 433 | --rules unusedArguments 434 | 435 | ## Use Void for type declarations and () for values. 436 | ### Options: 437 | ### --voidtype (default: void) 438 | --rules void 439 | 440 | ## Wrap lines that exceed the specified maximum width. 441 | ### Options: 442 | ### --maxwidth (default: none) 443 | ### --nowrapoperators 444 | ### --assetliterals 445 | ### --wrapternary (default: default) 446 | --rules wrap 447 | 448 | ## Align wrapped function arguments or collection elements. 449 | ### Options: 450 | ### --wraparguments 451 | ### --wrapparameters 452 | ### --wrapcollections 453 | ### --closingparen (default: balanced) 454 | ### --wrapreturntype (default: preserve) 455 | ### --wrapconditions 456 | ### --wraptypealiases 457 | ### --wrapeffects 458 | --rules wrapArguments 459 | 460 | ## Wrap @attributes onto a separate line, or keep them on the same line. 461 | ### Options: 462 | ### --funcattributes 463 | ### --typeattributes 464 | ### --varattributes 465 | --rules wrapAttributes 466 | 467 | ## Wrap the bodies of inline conditional statements onto a new line. 468 | --rules wrapConditionalBodies 469 | 470 | ## Rewrite comma-delimited enum cases to one case per line. 471 | ### Options: 472 | ### --wrapenumcases (default: always) 473 | --rules wrapEnumCases 474 | 475 | ## Wrap the opening brace of multiline statements. 476 | --rules wrapMultilineStatementBraces 477 | 478 | ## Prefer constant values to be on the right-hand-side of expressions. 479 | ### Options: 480 | ### --yodaswap (default: always) 481 | --rules yodaConditions 482 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "swiftformat" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org). 6 | 7 | ## 📦 [Version 0.1.0](https://github.com/fetch-rewards/SwiftSyntaxSugar/releases/tag/0.1.0) - April 21, 2025 ([Full Changelog](https://github.com/fetch-rewards/SwiftSyntaxSugar/commits/0.1.0)) 8 | 9 | ### 🚀 Initial Release 10 | 11 | This is the first public release of `SwiftSyntaxSugar`, a library that extends and simplifies interaction with [`SwiftSyntax`](https://github.com/swiftlang/swift-syntax), 12 | reducing boilerplate and improving ergonomics for developers building Swift tooling. 13 | 14 | This initial release includes: 15 | 16 | - Expressive syntax that improves the ergonomics of working with `SwiftSyntax` types 17 | - Powerful, convenient extensions that reduce boilerplate when parsing Swift syntax trees 18 | - A well-tested foundation on which to build Swift tooling 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lead maintainer 2 | * @graycampbell 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | oss+codeofconduct@fetch.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > [!IMPORTANT] 4 | > PRs are merged at the discretion of the project's maintainers. If you would like to contribute code to this project, 5 | > please first [open an issue](https://github.com/fetch-rewards/SwiftSyntaxSugar/issues/new) with a detailed description 6 | > of your proposed changes. This allows us to discuss implementation, alternatives, etc. and avoid wasting time dealing 7 | > with the inefficient back and forth that can arise when PRs are created without prior discussion. 8 | > 9 | > After you've created an issue, please read through the guidelines in this document carefully before implementing any 10 | > changes. 11 | > 12 | > By opening an issue or contributing code to this project, you agree to follow our 13 | > [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 14 | > 15 | > Thank you for helping us make this project the best it can be! 16 | 17 | - [Homebrew](#homebrew) 18 | - [Code Formatting](#code-formatting) 19 | - [Signed Commits Required](#signed-commits-required) 20 | - [Commit Messages & PR Titles](#commit-messages--pr-titles) 21 | 22 | ## Homebrew 23 | 24 | This project uses [Homebrew](https://brew.sh) to manage certain dependencies required for development. These 25 | dependencies are defined in the [`Brewfile`](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/Brewfile) 26 | located at the repository's root. 27 | 28 | To install these dependencies, use the command line to navigate to the repository’s root and run the following command: 29 | ``` 30 | brew bundle install 31 | ``` 32 | 33 | ## Code Formatting 34 | 35 | This project uses [`SwiftFormat`](https://github.com/nicklockwood/SwiftFormat) to maintain consistent code formatting. 36 | We do not currently automate the process of formatting code, but our CI workflow does use `SwiftFormat` as a linter to 37 | validate that all code changes adhere to our formatting rules. Before creating a PR, please run `swiftformat` on all new 38 | or updated files and commit the changes. 39 | 40 | ## Signed Commits Required 41 | 42 | All contributions to this project must use **signed commits**. This is an important part of our commitment to security, 43 | authenticity, and trust in the software we maintain. Signed commits prove that a commit actually came from you, not just 44 | someone who knows your name and email. Without signed commits, it’s possible for malicious actors to impersonate contributors 45 | and inject malicious code into the project. 46 | 47 | > [!IMPORTANT] 48 | > Unsigned commits will be automatically rejected by our CI. If you forget to sign a commit, you can amend and re-push. 49 | 50 | To learn more about commit signature verification and to make sure you're using signed commits, please read this 51 | [guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). 52 | 53 | Thank you for helping us make the open-source community safer! 54 | 55 | ## Commit Messages & PR Titles 56 | 57 | Descriptive, well-formatted commit messages and PR titles help create a consistent experience for maintainers and 58 | contributors. 59 | 60 | **All commits and PR titles should follow these guidelines:** 61 | 62 | 1. The subject is written using sentence case, not title case, and acronyms are written in all caps: 63 | ``` 64 | ✅ Make public API changes 65 | ❌ Make public api changes 66 | ❌ Make Public API Changes 67 | ❌ MAKE PUBLIC API CHANGES 68 | ``` 69 | 1. The subject is written in the imperative: 70 | ``` 71 | ✅ Add new file 72 | ❌ Adds new file 73 | ❌ Added new file 74 | ❌ Adding new file 75 | ``` 76 | 1. The subject does not end with a period or include unnecessary punctuation: 77 | ``` 78 | ✅ Refactor networking layer 79 | ❌ Refactor networking layer. 80 | ``` 81 | 1. The subject is ideally 50 characters or less (otherwise, 72 characters or less). 82 | 1. The subject is separated from the body with a blank line (critical unless the body is omitted entirely). 83 | 1. The subject and body are free of whitespace errors and typos. 84 | 1. The body uses proper punctuation and capitalization. 85 | 1. The body has a line length of 72 characters or less. 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2025 Fetch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftSyntaxSugar", 7 | platforms: [ 8 | .macOS(.v10_15), 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v6), 12 | .macCatalyst(.v13), 13 | ], 14 | products: [ 15 | .library( 16 | name: "SwiftSyntaxSugar", 17 | targets: ["SwiftSyntaxSugar"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package( 22 | url: "https://github.com/apple/swift-syntax.git", 23 | exact: "600.0.0" 24 | ), 25 | ], 26 | targets: [ 27 | .target( 28 | name: "SwiftSyntaxSugar", 29 | dependencies: [ 30 | .product(name: "SwiftSyntax", package: "swift-syntax"), 31 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 32 | ], 33 | swiftSettings: .default 34 | ), 35 | .testTarget( 36 | name: "SwiftSyntaxSugarTests", 37 | dependencies: ["SwiftSyntaxSugar"], 38 | swiftSettings: .default 39 | ), 40 | ] 41 | ) 42 | 43 | // MARK: - Swift Settings 44 | 45 | extension SwiftSetting { 46 | static let internalImportsByDefault: SwiftSetting = .enableUpcomingFeature( 47 | "InternalImportsByDefault" 48 | ) 49 | static let existentialAny: SwiftSetting = .enableUpcomingFeature( 50 | "ExistentialAny" 51 | ) 52 | } 53 | 54 | extension [SwiftSetting] { 55 | 56 | /// Default Swift settings to enable for targets. 57 | static let `default`: [SwiftSetting] = [ 58 | .existentialAny, 59 | .internalImportsByDefault, 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftSyntaxSugar 2 | 3 | [![ci](https://github.com/fetch-rewards/SwiftSyntaxSugar/actions/workflows/ci.yml/badge.svg)](https://github.com/fetch-rewards/SwiftSyntaxSugar/actions/workflows/ci.yml?query=branch%3Amain) 4 | [![codecov](https://codecov.io/gh/fetch-rewards/SwiftSyntaxSugar/graph/badge.svg?token=gTqOi09vx5)](https://codecov.io/gh/fetch-rewards/SwiftSyntaxSugar) 5 | [![swift](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffetch-rewards%2FSwiftSyntaxSugar%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/fetch-rewards/SwiftSyntaxSugar) 6 | [![platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ffetch-rewards%2FSwiftSyntaxSugar%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/fetch-rewards/SwiftSyntaxSugar) 7 | [![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/LICENSE) 8 | 9 | `SwiftSyntaxSugar` is a library that provides syntactic sugar and helpful extensions for [`SwiftSyntax`](https://github.com/swiftlang/swift-syntax). 10 | The purpose of this library is to improve the readability and maintainability of code written using `SwiftSyntax`. 11 | 12 | - [Example](#example) 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Contributing](#contributing) 16 | - [License](#license) 17 | 18 | ## Example 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 43 | 44 |
With SwiftSyntaxSugar Without SwiftSyntaxSugar
27 | 28 | ```swift 29 | protocolDeclaration.isActorConstrained 30 | ``` 31 | 32 | 34 | 35 | ```swift 36 | let isProtocolActorConstrained = protocolDeclaration.inheritanceClause?.inheritedTypes.contains { inheritedType in 37 | let identifierType = inheritedType.type.as(IdentifierTypeSyntax.self) 38 | return identifierType.name.tokenKind == .identifier("Actor") 39 | } ?? false 40 | ``` 41 | 42 |
45 | 46 | ## Installation 47 | 48 | To add `SwiftSyntaxSugar` to a Swift package manifest file: 49 | - Add the `SwiftSyntaxSugar` package to your package's `dependencies`: 50 | ```swift 51 | .package( 52 | url: "https://github.com/fetch-rewards/SwiftSyntaxSugar.git", 53 | from: "<#latest SwiftSyntaxSugar tag#>" 54 | ) 55 | ``` 56 | - Add the `SwiftSyntaxSugar` product to your target's `dependencies`: 57 | ```swift 58 | .product(name: "SwiftSyntaxSugar", package: "SwiftSyntaxSugar") 59 | ``` 60 | 61 | ## Usage 62 | 63 | - Import `SwiftSyntaxSugar`: 64 | ```swift 65 | import SwiftSyntaxSugar 66 | ``` 67 | - Use `SwiftSyntax` exactly how you normally would! 68 | 69 | ## Contributing 70 | 71 | The simplest way to contribute to this project is by [opening an issue](https://github.com/fetch-rewards/SwiftSyntaxSugar/issues/new). 72 | 73 | If you would like to contribute code to this project, please read our [Contributing Guidelines](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CONTRIBUTING.md). 74 | 75 | By opening an issue or contributing code to this project, you agree to follow our [Code of Conduct](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/CODE_OF_CONDUCT.md). 76 | 77 | ## License 78 | 79 | This library is released under the MIT license. See [LICENSE](https://github.com/fetch-rewards/SwiftSyntaxSugar/blob/main/LICENSE) for details. 80 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/AccessLevelSyntax/AccessLevelSyntax.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessLevelSyntax.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | /// An access level. 10 | public enum AccessLevelSyntax: String, CaseIterable, Hashable, Sendable { 11 | 12 | // MARK: Cases 13 | 14 | /// The `fileprivate` access level. 15 | case `fileprivate` 16 | 17 | /// The `internal` access level. 18 | case `internal` 19 | 20 | /// The `open` access level. 21 | case open 22 | 23 | /// The `package` access level. 24 | case package 25 | 26 | /// The `private` access level. 27 | case `private` 28 | 29 | /// The `public` access level. 30 | case `public` 31 | 32 | // MARK: Properties 33 | 34 | /// The access level's raw value. 35 | public var rawValue: String { 36 | self.token.text 37 | } 38 | 39 | /// The access level as a keyword. 40 | public var keyword: Keyword { 41 | switch self { 42 | case .fileprivate: .fileprivate 43 | case .internal: .internal 44 | case .open: .open 45 | case .package: .package 46 | case .private: .private 47 | case .public: .public 48 | } 49 | } 50 | 51 | /// The access level's token. 52 | public var token: TokenSyntax { 53 | .keyword(self.keyword) 54 | } 55 | 56 | /// The access level's token kind. 57 | public var tokenKind: TokenKind { 58 | .keyword(self.keyword) 59 | } 60 | 61 | /// The access level as a modifier. 62 | public var modifier: DeclModifierSyntax { 63 | DeclModifierSyntax(name: self.token) 64 | } 65 | 66 | // MARK: Initializers 67 | 68 | /// Creates an access level from the specified raw value. 69 | /// 70 | /// If there is no access level that corresponds to the specified raw value, 71 | /// this initializer returns `nil`. 72 | /// 73 | /// - Parameter rawValue: The access level's raw value. 74 | public init?(rawValue: String) { 75 | let accessLevel = Self.allCases.first { $0.rawValue == rawValue } 76 | 77 | guard let accessLevel else { 78 | return nil 79 | } 80 | 81 | self = accessLevel 82 | } 83 | 84 | /// Creates an access level from the specified token. 85 | /// 86 | /// If there is no access level that corresponds to the specified token, 87 | /// this initializer returns `nil`. 88 | /// 89 | /// - Parameter token: The token from which to create the access level. 90 | public init?(token: TokenSyntax) { 91 | self.init(rawValue: token.text) 92 | } 93 | 94 | /// Creates an access level from the specified modifier. 95 | /// 96 | /// If there is no access level that corresponds to the specified modifier, 97 | /// this initializer returns `nil`. 98 | /// 99 | /// - Parameter modifier: The modifier from which to create the access 100 | /// level. 101 | public init?(modifier: DeclModifierSyntax) { 102 | self.init(token: modifier.name) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/AccessorBlockSyntax/AccessorBlockSyntax+Accessors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessorBlockSyntax+Accessors.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension AccessorBlockSyntax { 10 | 11 | // MARK: Accessors 12 | 13 | /// The accessor block's `get` accessor declaration, if there is one. 14 | public var getAccessorDeclaration: AccessorDeclSyntax? { 15 | guard case let .accessors(accessorDeclarations) = self.accessors else { 16 | return nil 17 | } 18 | 19 | return accessorDeclarations.first { accessorDeclaration in 20 | accessorDeclaration.accessorSpecifier.tokenKind == .keyword(.get) 21 | } 22 | } 23 | 24 | /// The accessor block's `set` accessor declaration, if there is one. 25 | public var setAccessorDeclaration: AccessorDeclSyntax? { 26 | guard case let .accessors(accessorDeclarations) = self.accessors else { 27 | return nil 28 | } 29 | 30 | return accessorDeclarations.first { accessorDeclaration in 31 | accessorDeclaration.accessorSpecifier.tokenKind == .keyword(.set) 32 | } 33 | } 34 | 35 | // MARK: Contains 36 | 37 | /// A boolean indicating if the accessor block contains a `get` accessor 38 | /// declaration. 39 | public var containsGetAccessor: Bool { 40 | self.getAccessorDeclaration != nil 41 | } 42 | 43 | /// A boolean indicating if the accessor block contains a `set` accessor 44 | /// declaration. 45 | public var containsSetAccessor: Bool { 46 | self.setAccessorDeclaration != nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/AccessorDeclSyntax/AccessorDeclSyntax+Effects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessorDeclSyntax+Effects.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension AccessorDeclSyntax { 10 | 11 | /// A boolean indicating if the accessor is asynchronous. 12 | public var isAsync: Bool { 13 | self.effectSpecifiers?.asyncSpecifier != nil 14 | } 15 | 16 | /// A boolean indicating if the accessor is throwing. 17 | public var isThrowing: Bool { 18 | self.effectSpecifiers?.throwsClause != nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/ActorDeclSyntax/ActorDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension ActorDeclSyntax { 10 | 11 | /// The actor declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/ClassDeclSyntax/ClassDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension ClassDeclSyntax { 10 | 11 | /// The class declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/DeclModifierListSyntax/DeclModifierListSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclModifierListSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension DeclModifierListSyntax { 10 | 11 | /// The first access level modifier found in the modifier list, or 12 | /// `.internal` if none exists. 13 | public var accessLevel: AccessLevelSyntax { 14 | guard 15 | let accessLevelModifier = self.first(where: \.isAccessLevel), 16 | let accessLevel = AccessLevelSyntax(modifier: accessLevelModifier) 17 | else { 18 | return .internal 19 | } 20 | 21 | return accessLevel 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/DeclModifierSyntax/DeclModifierSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclModifierSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension DeclModifierSyntax { 10 | 11 | /// A boolean indicating if the modifier is an access level modifier. 12 | public var isAccessLevel: Bool { 13 | AccessLevelSyntax.allCases.contains { accessLevel in 14 | self.name.tokenKind == accessLevel.tokenKind 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/FunctionDeclSyntax/FunctionDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension FunctionDeclSyntax { 10 | 11 | /// The function declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | 16 | /// Returns a copy of the function declaration with the provided access 17 | /// level. 18 | /// 19 | /// - Parameter accessLevel: The access level of the new function 20 | /// declaration. 21 | /// - Returns: A copy of the function declaration with the provided access 22 | /// level. 23 | public func withAccessLevel( 24 | _ accessLevel: AccessLevelSyntax 25 | ) throws -> FunctionDeclSyntax { 26 | try self.with(\.modifiers) { modifiers in 27 | if accessLevel != .internal { 28 | accessLevel.modifier 29 | } 30 | 31 | for modifier in modifiers where !modifier.isAccessLevel { 32 | modifier 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/FunctionDeclSyntax/FunctionDeclSyntax+Effects.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionDeclSyntax+Effects.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension FunctionDeclSyntax { 10 | 11 | /// A boolean indicating if the function is asynchronous. 12 | public var isAsync: Bool { 13 | self.signature.effectSpecifiers?.asyncSpecifier != nil 14 | } 15 | 16 | /// A boolean indicating if the function is throwing. 17 | public var isThrowing: Bool { 18 | self.signature.effectSpecifiers?.throwsClause != nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/FunctionParameterSyntax/FunctionParameterSyntax+IsVariadic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionParameterSyntax+IsVariadic.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension FunctionParameterSyntax { 10 | 11 | /// A Boolean value indicating whether the parameter is a variadic 12 | /// parameter. 13 | public var isVariadic: Bool { 14 | self.ellipsis != nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/FunctionTypeSyntax/FunctionTypeSyntax+Escaping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionTypeSyntax+Escaping.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension FunctionTypeSyntax { 10 | 11 | /// Returns an escaping version of the function type. 12 | /// 13 | /// - Returns: An escaping version of the function type. 14 | public func escaping() -> AttributedTypeSyntax { 15 | // TODO: Remove specifiers argument when deprecated init is removed. 16 | AttributedTypeSyntax( 17 | specifiers: [], 18 | attributes: AttributeListSyntax { 19 | AttributeSyntax( 20 | atSign: .atSignToken(), 21 | attributeName: IdentifierTypeSyntax( 22 | name: .keyword(.escaping) 23 | ) 24 | ) 25 | }, 26 | baseType: self 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/InheritanceClauseSyntax/InheritanceClauseSyntax+InheritedTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InheritanceClauseSyntax+InheritedTypes.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension InheritanceClauseSyntax { 10 | 11 | /// Returns the inherited types of the provided `type` that are contained in 12 | /// the inheritance clause. 13 | /// 14 | /// - Parameter type: The type of inherited type to return. 15 | /// - Returns: The inherited types of the provided `type` that are contained 16 | /// in the inheritance clause. 17 | public func inheritedTypes( 18 | ofType type: Syntax.Type 19 | ) -> [Syntax] { 20 | self.inheritedTypes.compactMap { inheritedType in 21 | inheritedType.type.as(type) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/InitializerDeclSyntax/InitializerDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitializerDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension InitializerDeclSyntax { 10 | 11 | /// The initializer declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | 16 | /// Returns a copy of the initializer declaration with the provided access 17 | /// level. 18 | /// 19 | /// - Parameter accessLevel: The access level of the new initializer 20 | /// declaration. 21 | /// - Returns: A copy of the initializer declaration with the provided 22 | /// access level. 23 | public func withAccessLevel( 24 | _ accessLevel: AccessLevelSyntax 25 | ) throws -> InitializerDeclSyntax { 26 | try self.with(\.modifiers) { modifiers in 27 | if accessLevel != .internal { 28 | accessLevel.modifier 29 | } 30 | 31 | for modifier in modifiers where !modifier.isAccessLevel { 32 | modifier 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/MemberBlockSyntax/MemberBlockSyntax+MemberDeclarations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberBlockSyntax+MemberDeclarations.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension MemberBlockSyntax { 10 | 11 | /// Returns the member declarations of the provided `type` that are 12 | /// contained in the member block. 13 | /// 14 | /// - Parameter type: The type of member declarations to return. 15 | /// - Returns: The member declarations of the provided `type` that are 16 | /// contained in the member block. 17 | public func memberDeclarations( 18 | ofType type: Syntax.Type 19 | ) -> [Syntax] { 20 | self.members.compactMap { member in 21 | member.decl.as(type) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/ProtocolDeclSyntax/ProtocolDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension ProtocolDeclSyntax { 10 | 11 | /// The protocol declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | 16 | /// The minimum access level required for types conforming to the protocol. 17 | public var minimumConformingAccessLevel: AccessLevelSyntax { 18 | let accessLevel = self.accessLevel 19 | 20 | return accessLevel == .private ? .fileprivate : accessLevel 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/ProtocolDeclSyntax/ProtocolDeclSyntax+Actor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax+Actor.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension ProtocolDeclSyntax { 10 | 11 | /// A boolean indicating if the protocol requires conforming types to be 12 | /// actors. 13 | public var isActorConstrained: Bool { 14 | guard let inheritanceClause = self.inheritanceClause else { 15 | return false 16 | } 17 | 18 | let identifierTypes = inheritanceClause.inheritedTypes( 19 | ofType: IdentifierTypeSyntax.self 20 | ) 21 | 22 | return identifierTypes.contains { type in 23 | type.name.tokenKind == .identifier(String(describing: (any Actor).self)) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/ProtocolDeclSyntax/ProtocolDeclSyntax+GenericWhereClauses.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax+GenericWhereClauses.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension ProtocolDeclSyntax { 10 | 11 | /// The protocol's generic where clauses (the clause from the protocol 12 | /// declaration and the clauses from the associated type declarations). 13 | public var genericWhereClauses: [GenericWhereClauseSyntax] { 14 | var genericWhereClauses: [GenericWhereClauseSyntax] = [] 15 | 16 | if let genericWhereClause = self.genericWhereClause { 17 | genericWhereClauses.append(genericWhereClause) 18 | } 19 | 20 | for member in self.memberBlock.members { 21 | guard 22 | let associatedTypeDeclaration = member.decl.as( 23 | AssociatedTypeDeclSyntax.self 24 | ), 25 | let genericWhereClause = associatedTypeDeclaration.genericWhereClause 26 | else { 27 | continue 28 | } 29 | 30 | genericWhereClauses.append(genericWhereClause) 31 | } 32 | 33 | return genericWhereClauses 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/ProtocolDeclSyntax/ProtocolDeclSyntax+Type.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax+Type.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension ProtocolDeclSyntax { 10 | 11 | /// The protocol as a type. 12 | public var type: TypeSyntax { 13 | TypeSyntax( 14 | IdentifierTypeSyntax(name: self.name) 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/StructDeclSyntax/StructDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StructDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension StructDeclSyntax { 10 | 11 | /// The struct declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/SyntaxProtocol/SyntaxProtocol+WithAttributeListSyntax.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxProtocol+WithAttributeListSyntax.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | 10 | extension SyntaxProtocol { 11 | 12 | /// Returns a new syntax node that has the child at `keyPath` replaced by an 13 | /// `AttributeListSyntax` built using `itemsBuilder`. 14 | /// 15 | /// - Parameters: 16 | /// - keyPath: The key path of the child to replace. 17 | /// - itemsBuilder: The items builder with which to build the 18 | /// `AttributeListSyntax` value with which to replace the child. 19 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 20 | /// an `AttributeListSyntax` built using `itemsBuilder`. 21 | public func with( 22 | _ keyPath: WritableKeyPath, 23 | @AttributeListBuilder itemsBuilder: @escaping () throws -> AttributeListSyntax 24 | ) throws -> Self { 25 | self.with( 26 | keyPath, 27 | try AttributeListSyntax(itemsBuilder: itemsBuilder) 28 | ) 29 | } 30 | 31 | /// Returns a new syntax node that has the child at `keyPath` replaced by an 32 | /// `AttributeListSyntax` built using `itemsBuilder`. 33 | /// 34 | /// - Parameters: 35 | /// - keyPath: The key path of the child to replace. 36 | /// - itemsBuilder: The items builder with which to build the 37 | /// `AttributeListSyntax` value with which to replace the child. 38 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 39 | /// an `AttributeListSyntax` built using `itemsBuilder`. 40 | public func with( 41 | _ keyPath: WritableKeyPath, 42 | @AttributeListBuilder itemsBuilder: @escaping ( 43 | AttributeListSyntax 44 | ) throws -> AttributeListSyntax 45 | ) throws -> Self { 46 | try self.with(keyPath) { 47 | try itemsBuilder(self[keyPath: keyPath]) 48 | } 49 | } 50 | 51 | /// Returns a new syntax node that has the child at `keyPath` replaced by an 52 | /// `AttributeListSyntax` built using `itemsBuilder`. 53 | /// 54 | /// - Parameters: 55 | /// - keyPath: The key path of the child to replace. 56 | /// - itemsBuilder: The items builder with which to build the 57 | /// `AttributeListSyntax` value with which to replace the child. 58 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 59 | /// an `AttributeListSyntax` built using `itemsBuilder`. 60 | public func with( 61 | _ keyPath: WritableKeyPath, 62 | @AttributeListBuilder itemsBuilder: @escaping () throws -> AttributeListSyntax 63 | ) throws -> Self { 64 | self.with( 65 | keyPath, 66 | try AttributeListSyntax(itemsBuilder: itemsBuilder) 67 | ) 68 | } 69 | 70 | /// Returns a new syntax node that has the child at `keyPath` replaced by an 71 | /// `AttributeListSyntax` built using `itemsBuilder`. 72 | /// 73 | /// - Parameters: 74 | /// - keyPath: The key path of the child to replace. 75 | /// - itemsBuilder: The items builder with which to build the 76 | /// `AttributeListSyntax` value with which to replace the child. 77 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 78 | /// an `AttributeListSyntax` built using `itemsBuilder`. 79 | public func with( 80 | _ keyPath: WritableKeyPath, 81 | @AttributeListBuilder itemsBuilder: @escaping ( 82 | AttributeListSyntax? 83 | ) throws -> AttributeListSyntax 84 | ) throws -> Self { 85 | try self.with(keyPath) { 86 | try itemsBuilder(self[keyPath: keyPath]) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/SyntaxProtocol/SyntaxProtocol+WithCodeBlockSyntax.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxProtocol+WithCodeBlockSyntax.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | 10 | extension SyntaxProtocol { 11 | 12 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 13 | /// `CodeBlockSyntax` built using `statementsBuilder`. 14 | /// 15 | /// - Parameters: 16 | /// - keyPath: The key path of the child to replace. 17 | /// - statementsBuilder: The statements builder with which to build the 18 | /// `CodeBlockSyntax` value with which to replace the child. 19 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 20 | /// a `CodeBlockSyntax` built using `statementsBuilder`. 21 | public func with( 22 | _ keyPath: WritableKeyPath, 23 | @CodeBlockItemListBuilder statementsBuilder: @escaping () throws -> CodeBlockItemListSyntax 24 | ) throws -> Self { 25 | self.with( 26 | keyPath, 27 | try CodeBlockSyntax(statementsBuilder: statementsBuilder) 28 | ) 29 | } 30 | 31 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 32 | /// `CodeBlockSyntax` built using `statementsBuilder`. 33 | /// 34 | /// - Parameters: 35 | /// - keyPath: The key path of the child to replace. 36 | /// - statementsBuilder: The statements builder with which to build the 37 | /// `CodeBlockSyntax` value with which to replace the child. 38 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 39 | /// a `CodeBlockSyntax` built using `statementsBuilder`. 40 | public func with( 41 | _ keyPath: WritableKeyPath, 42 | @CodeBlockItemListBuilder statementsBuilder: @escaping ( 43 | CodeBlockSyntax 44 | ) throws -> CodeBlockItemListSyntax 45 | ) throws -> Self { 46 | try self.with(keyPath) { 47 | try statementsBuilder(self[keyPath: keyPath]) 48 | } 49 | } 50 | 51 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 52 | /// `CodeBlockSyntax` built using `statementsBuilder`. 53 | /// 54 | /// - Parameters: 55 | /// - keyPath: The key path of the child to replace. 56 | /// - statementsBuilder: The statements builder with which to build the 57 | /// `CodeBlockSyntax` value with which to replace the child. 58 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 59 | /// a `CodeBlockSyntax` built using `statementsBuilder`. 60 | public func with( 61 | _ keyPath: WritableKeyPath, 62 | @CodeBlockItemListBuilder statementsBuilder: @escaping () throws -> CodeBlockItemListSyntax 63 | ) throws -> Self { 64 | self.with( 65 | keyPath, 66 | try CodeBlockSyntax(statementsBuilder: statementsBuilder) 67 | ) 68 | } 69 | 70 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 71 | /// `CodeBlockSyntax` built using `statementsBuilder`. 72 | /// 73 | /// - Parameters: 74 | /// - keyPath: The key path of the child to replace. 75 | /// - statementsBuilder: The statements builder with which to build the 76 | /// `CodeBlockSyntax` value with which to replace the child. 77 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 78 | /// a `CodeBlockSyntax` built using `statementsBuilder`. 79 | public func with( 80 | _ keyPath: WritableKeyPath, 81 | @CodeBlockItemListBuilder statementsBuilder: @escaping ( 82 | CodeBlockSyntax? 83 | ) throws -> CodeBlockItemListSyntax 84 | ) throws -> Self { 85 | try self.with(keyPath) { 86 | try statementsBuilder(self[keyPath: keyPath]) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/SyntaxProtocol/SyntaxProtocol+WithDeclModifierListSyntax.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxProtocol+WithDeclModifierListSyntax.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | 10 | extension SyntaxProtocol { 11 | 12 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 13 | /// `DeclModifierListSyntax` built using `itemsBuilder`. 14 | /// 15 | /// - Parameters: 16 | /// - keyPath: The key path of the child to replace. 17 | /// - itemsBuilder: The items builder with which to build the 18 | /// `DeclModifierListSyntax` value with which to replace the child. 19 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 20 | /// a `DeclModifierListSyntax` built using `itemsBuilder`. 21 | public func with( 22 | _ keyPath: WritableKeyPath, 23 | @DeclModifierListBuilder itemsBuilder: @escaping () throws -> DeclModifierListSyntax 24 | ) throws -> Self { 25 | self.with( 26 | keyPath, 27 | try DeclModifierListSyntax(itemsBuilder: itemsBuilder) 28 | ) 29 | } 30 | 31 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 32 | /// `DeclModifierListSyntax` built using `itemsBuilder`. 33 | /// 34 | /// - Parameters: 35 | /// - keyPath: The key path of the child to replace. 36 | /// - itemsBuilder: The items builder with which to build the 37 | /// `DeclModifierListSyntax` value with which to replace the child. 38 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 39 | /// a `DeclModifierListSyntax` built using `itemsBuilder`. 40 | public func with( 41 | _ keyPath: WritableKeyPath, 42 | @DeclModifierListBuilder itemsBuilder: @escaping ( 43 | DeclModifierListSyntax 44 | ) throws -> DeclModifierListSyntax 45 | ) throws -> Self { 46 | try self.with(keyPath) { 47 | try itemsBuilder(self[keyPath: keyPath]) 48 | } 49 | } 50 | 51 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 52 | /// `DeclModifierListSyntax` built using `itemsBuilder`. 53 | /// 54 | /// - Parameters: 55 | /// - keyPath: The key path of the child to replace. 56 | /// - itemsBuilder: The items builder with which to build the 57 | /// `DeclModifierListSyntax` value with which to replace the child. 58 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 59 | /// a `DeclModifierListSyntax` built using `itemsBuilder`. 60 | public func with( 61 | _ keyPath: WritableKeyPath, 62 | @DeclModifierListBuilder itemsBuilder: @escaping () throws -> DeclModifierListSyntax 63 | ) throws -> Self { 64 | self.with( 65 | keyPath, 66 | try DeclModifierListSyntax(itemsBuilder: itemsBuilder) 67 | ) 68 | } 69 | 70 | /// Returns a new syntax node that has the child at `keyPath` replaced by a 71 | /// `DeclModifierListSyntax` built using `itemsBuilder`. 72 | /// 73 | /// - Parameters: 74 | /// - keyPath: The key path of the child to replace. 75 | /// - itemsBuilder: The items builder with which to build the 76 | /// `DeclModifierListSyntax` value with which to replace the child. 77 | /// - Returns: A new syntax node that has the child at `keyPath` replaced by 78 | /// a `DeclModifierListSyntax` built using `itemsBuilder`. 79 | public func with( 80 | _ keyPath: WritableKeyPath, 81 | @DeclModifierListBuilder itemsBuilder: @escaping ( 82 | DeclModifierListSyntax? 83 | ) throws -> DeclModifierListSyntax 84 | ) throws -> Self { 85 | try self.with(keyPath) { 86 | try itemsBuilder(self[keyPath: keyPath]) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/TypeSyntax/TypeSyntax+Describing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeSyntax+Describing.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension TypeSyntax { 10 | 11 | /// Creates a new `TypeSyntax` from a `String` representing the given value. 12 | /// 13 | /// This initializer uses `String(describing: subject)` to create a `String` 14 | /// representation of the given value and create a new `TypeSyntax`. 15 | /// 16 | /// Use this initializer to create a `TypeSyntax` instance from a type. This 17 | /// removes the need to use string literals to create `TypeSyntax` instances 18 | /// in most cases. 19 | /// 20 | /// ```swift 21 | /// TypeSyntax(describing: Int.self) 22 | /// ``` 23 | public init(describing subject: some Any) { 24 | let identifier = String(describing: subject) 25 | 26 | self.init( 27 | IdentifierTypeSyntax(name: .identifier(identifier)) 28 | ) 29 | } 30 | } 31 | 32 | // MARK: - TypeSyntaxProtocol 33 | 34 | extension TypeSyntaxProtocol where Self == TypeSyntax { 35 | 36 | /// A `TypeSyntax` instance describing `Bool`. 37 | public static var bool: TypeSyntax { TypeSyntax(describing: Bool.self) } 38 | 39 | /// A `TypeSyntax` instance describing `Int`. 40 | public static var int: TypeSyntax { TypeSyntax(describing: Int.self) } 41 | 42 | /// A `TypeSyntax` instance describing `String`. 43 | public static var string: TypeSyntax { TypeSyntax(describing: String.self) } 44 | 45 | /// A `TypeSyntax` instance describing `Void`. 46 | public static var void: TypeSyntax { "Void" } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/SwiftSyntaxSugar/VariableDeclSyntax/VariableDeclSyntax+AccessLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VariableDeclSyntax+AccessLevel.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | public import SwiftSyntax 8 | 9 | extension VariableDeclSyntax { 10 | 11 | /// The variable declaration's access level. 12 | public var accessLevel: AccessLevelSyntax { 13 | self.modifiers.accessLevel 14 | } 15 | 16 | /// Returns a copy of the variable declaration with the provided access 17 | /// level. 18 | /// 19 | /// - Parameter accessLevel: The access level of the new variable 20 | /// declaration. 21 | /// - Returns: A copy of the variable declaration with the provided access 22 | /// level. 23 | public func withAccessLevel( 24 | _ accessLevel: AccessLevelSyntax 25 | ) throws -> VariableDeclSyntax { 26 | try self.with(\.modifiers) { modifiers in 27 | if accessLevel != .internal { 28 | accessLevel.modifier 29 | } 30 | 31 | for modifier in modifiers where !modifier.isAccessLevel { 32 | modifier 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/AccessLevelSyntax/AccessLevelSyntaxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessLevelSyntaxTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct AccessLevelSyntaxTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = AccessLevelSyntax 16 | 17 | // MARK: Property Tests 18 | 19 | @Test(arguments: SUT.allCases) 20 | func rawValue(sut: SUT) { 21 | let expectedRawValue = switch sut { 22 | case .fileprivate: "fileprivate" 23 | case .internal: "internal" 24 | case .open: "open" 25 | case .package: "package" 26 | case .private: "private" 27 | case .public: "public" 28 | } 29 | 30 | #expect(sut.rawValue == expectedRawValue) 31 | } 32 | 33 | @Test(arguments: SUT.allCases) 34 | func keyword(sut: SUT) { 35 | let expectedKeyword: Keyword = switch sut { 36 | case .fileprivate: .fileprivate 37 | case .internal: .internal 38 | case .open: .open 39 | case .package: .package 40 | case .private: .private 41 | case .public: .public 42 | } 43 | 44 | #expect(sut.keyword == expectedKeyword) 45 | } 46 | 47 | @Test(arguments: SUT.allCases) 48 | func token(sut: SUT) { 49 | let expectedTokenKind: TokenKind = switch sut { 50 | case .fileprivate: .keyword(.fileprivate) 51 | case .internal: .keyword(.internal) 52 | case .open: .keyword(.open) 53 | case .package: .keyword(.package) 54 | case .private: .keyword(.private) 55 | case .public: .keyword(.public) 56 | } 57 | 58 | #expect(sut.token.tokenKind == expectedTokenKind) 59 | } 60 | 61 | @Test(arguments: SUT.allCases) 62 | func modifier(sut: SUT) { 63 | let expectedModifierTokenKind: TokenKind = switch sut { 64 | case .fileprivate: .keyword(.fileprivate) 65 | case .internal: .keyword(.internal) 66 | case .open: .keyword(.open) 67 | case .package: .keyword(.package) 68 | case .private: .keyword(.private) 69 | case .public: .keyword(.public) 70 | } 71 | 72 | #expect(sut.modifier.name.tokenKind == expectedModifierTokenKind) 73 | } 74 | 75 | // MARK: Raw Value Initializer Tests 76 | 77 | @Test(arguments: SUT.allCases) 78 | func initWithRawValueWithValidRawValue(from sut: SUT) { 79 | #expect(SUT(rawValue: sut.rawValue) == sut) 80 | } 81 | 82 | @Test 83 | func initWithRawValueWithInvalidRawValue() { 84 | let sut = SUT(rawValue: "sut") 85 | 86 | #expect(sut == nil) 87 | } 88 | 89 | // MARK: Token Initializer Tests 90 | 91 | @Test(arguments: SUT.allCases) 92 | func initWithTokenWithValidToken(from sut: SUT) { 93 | #expect(SUT(token: sut.token) == sut) 94 | } 95 | 96 | @Test 97 | func initWithTokenWithInvalidToken() { 98 | let sut = SUT(token: "sut") 99 | 100 | #expect(sut == nil) 101 | } 102 | 103 | // MARK: Modifier Initializer Tests 104 | 105 | @Test(arguments: SUT.allCases) 106 | func initWithModifierWithValidModifier(from sut: SUT) { 107 | #expect(SUT(modifier: sut.modifier) == sut) 108 | } 109 | 110 | @Test 111 | func initWithModifierWithInvalidModifier() { 112 | let sut = SUT(modifier: DeclModifierSyntax(name: .keyword(.static))) 113 | 114 | #expect(sut == nil) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/AccessorBlockSyntax/AccessorBlockSyntax_AccessorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessorBlockSyntax_AccessorsTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import SwiftSyntaxBuilder 9 | import Testing 10 | @testable import SwiftSyntaxSugar 11 | 12 | struct AccessorBlockSyntax_AccessorsTests { 13 | 14 | // MARK: Typealiases 15 | 16 | typealias SUT = AccessorBlockSyntax 17 | 18 | // MARK: Get Accessor Declaration Tests 19 | 20 | @Test 21 | func getAccessorDeclarationWithAccessorsWithGetAccessor() { 22 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) 23 | let accessors = AccessorDeclListSyntax { accessor } 24 | let sut = SUT(accessors: .accessors(accessors)) 25 | 26 | #expect(sut.getAccessorDeclaration != nil) 27 | } 28 | 29 | @Test 30 | func getAccessorDeclarationWithAccessorsWithoutGetAccessor() { 31 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.set)) 32 | let accessors = AccessorDeclListSyntax { accessor } 33 | let sut = SUT(accessors: .accessors(accessors)) 34 | 35 | #expect(sut.getAccessorDeclaration == nil) 36 | } 37 | 38 | @Test 39 | func getAccessorDeclarationWithGetter() { 40 | let sut = SUT(accessors: .getter(CodeBlockItemListSyntax())) 41 | 42 | #expect(sut.getAccessorDeclaration == nil) 43 | } 44 | 45 | // MARK: Set Accessor Declaration Tests 46 | 47 | @Test 48 | func setAccessorDeclarationWithAccessorsWithSetAccessor() { 49 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.set)) 50 | let accessors = AccessorDeclListSyntax { accessor } 51 | let sut = SUT(accessors: .accessors(accessors)) 52 | 53 | #expect(sut.setAccessorDeclaration != nil) 54 | } 55 | 56 | @Test 57 | func setAccessorDeclarationWithAccessorsWithoutSetAccessor() { 58 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) 59 | let accessors = AccessorDeclListSyntax { accessor } 60 | let sut = SUT(accessors: .accessors(accessors)) 61 | 62 | #expect(sut.setAccessorDeclaration == nil) 63 | } 64 | 65 | @Test 66 | func setAccessorDeclarationWithGetter() { 67 | let sut = SUT(accessors: .getter(CodeBlockItemListSyntax())) 68 | 69 | #expect(sut.setAccessorDeclaration == nil) 70 | } 71 | 72 | // MARK: Contains Get Accessor Tests 73 | 74 | @Test 75 | func containsGetAccessorWithAccessorsWithGetAccessor() { 76 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) 77 | let accessors = AccessorDeclListSyntax { accessor } 78 | let sut = SUT(accessors: .accessors(accessors)) 79 | 80 | #expect(sut.containsGetAccessor) 81 | } 82 | 83 | @Test 84 | func containsGetAccessorWithAccessorsWithoutGetAccessor() { 85 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.set)) 86 | let accessors = AccessorDeclListSyntax { accessor } 87 | let sut = SUT(accessors: .accessors(accessors)) 88 | 89 | #expect(!sut.containsGetAccessor) 90 | } 91 | 92 | @Test 93 | func containsGetAccessorWithGetter() { 94 | let sut = SUT(accessors: .getter(CodeBlockItemListSyntax())) 95 | 96 | #expect(!sut.containsGetAccessor) 97 | } 98 | 99 | // MARK: Contains Set Accessor Tests 100 | 101 | @Test 102 | func containsSetAccessorWithAccessorsWithSetAccessor() { 103 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.set)) 104 | let accessors = AccessorDeclListSyntax { accessor } 105 | let sut = SUT(accessors: .accessors(accessors)) 106 | 107 | #expect(sut.containsSetAccessor) 108 | } 109 | 110 | @Test 111 | func containsSetAccessorWithAccessorsWithoutSetAccessor() { 112 | let accessor = AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) 113 | let accessors = AccessorDeclListSyntax { accessor } 114 | let sut = SUT(accessors: .accessors(accessors)) 115 | 116 | #expect(!sut.containsSetAccessor) 117 | } 118 | 119 | @Test 120 | func containsSetAccessorWithGetter() { 121 | let sut = SUT(accessors: .getter(CodeBlockItemListSyntax())) 122 | 123 | #expect(!sut.containsSetAccessor) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/AccessorDeclSyntax/AccessorDeclSyntax_EffectsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessorDeclSyntax_EffectsTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct AccessorDeclSyntax_EffectsTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = AccessorDeclSyntax 16 | 17 | // MARK: Is Async Tests 18 | 19 | @Test 20 | func isAsyncWithNilAsyncSpecifier() { 21 | let sut = SUT(accessorSpecifier: .keyword(.get)) 22 | 23 | #expect(!sut.isAsync) 24 | } 25 | 26 | @Test 27 | func isAsyncWithNonNilAsyncSpecifier() { 28 | let sut = SUT( 29 | accessorSpecifier: .keyword(.get), 30 | effectSpecifiers: AccessorEffectSpecifiersSyntax( 31 | asyncSpecifier: .keyword(.async) 32 | ) 33 | ) 34 | 35 | #expect(sut.isAsync) 36 | } 37 | 38 | // MARK: Is Throwing Tests 39 | 40 | @Test 41 | func isThrowingWithNilThrowsSpecifier() { 42 | let sut = SUT(accessorSpecifier: .keyword(.get)) 43 | 44 | #expect(!sut.isThrowing) 45 | } 46 | 47 | @Test 48 | func isThrowingWithNonNilThrowsSpecifier() { 49 | let sut = SUT( 50 | accessorSpecifier: .keyword(.get), 51 | effectSpecifiers: AccessorEffectSpecifiersSyntax( 52 | throwsClause: ThrowsClauseSyntax( 53 | throwsSpecifier: .keyword(.throws) 54 | ) 55 | ) 56 | ) 57 | 58 | #expect(sut.isThrowing) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/ActorDeclSyntax/ActorDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActorDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct ActorDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ActorDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let modifiers = DeclModifierListSyntax { 24 | accessLevel.modifier 25 | DeclModifierSyntax(name: .keyword(.final)) 26 | } 27 | let sut = SUT(modifiers: modifiers, name: "SUT") {} 28 | 29 | #expect(sut.accessLevel == accessLevel) 30 | } 31 | 32 | @Test 33 | func accessLevelWithImplicitInternalAccessLevelModifier() { 34 | let modifiers = DeclModifierListSyntax { 35 | DeclModifierSyntax(name: .keyword(.final)) 36 | } 37 | let sut = SUT(modifiers: modifiers, name: "SUT") {} 38 | 39 | #expect(sut.accessLevel == .internal) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/ClassDeclSyntax/ClassDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClassDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct ClassDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ClassDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let modifiers = DeclModifierListSyntax { 24 | accessLevel.modifier 25 | DeclModifierSyntax(name: .keyword(.final)) 26 | } 27 | let sut = SUT(modifiers: modifiers, name: "SUT") {} 28 | 29 | #expect(sut.accessLevel == accessLevel) 30 | } 31 | 32 | @Test 33 | func accessLevelWithImplicitInternalAccessLevelModifier() { 34 | let modifiers = DeclModifierListSyntax { 35 | DeclModifierSyntax(name: .keyword(.final)) 36 | } 37 | let sut = SUT(modifiers: modifiers, name: "SUT") {} 38 | 39 | #expect(sut.accessLevel == .internal) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/DeclModifierListSyntax/DeclModifierListSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclModifierListSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct DeclModifierListSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = DeclModifierListSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let sut = SUT { 24 | accessLevel.modifier 25 | DeclModifierSyntax(name: .keyword(.static)) 26 | } 27 | 28 | #expect(sut.accessLevel == accessLevel) 29 | } 30 | 31 | @Test 32 | func accessLevelWithImplicitInternalAccessLevelModifier() { 33 | let sut = SUT { 34 | DeclModifierSyntax(name: .keyword(.static)) 35 | } 36 | 37 | #expect(sut.accessLevel == .internal) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/DeclModifierSyntax/DeclModifierSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeclModifierSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct DeclModifierSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = DeclModifierSyntax 16 | 17 | // MARK: Is Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func isAccessLevelWithAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let sut = SUT(name: accessLevel.token) 24 | 25 | #expect(sut.isAccessLevel) 26 | } 27 | 28 | @Test(arguments: [Keyword.class, Keyword.lazy, Keyword.static]) 29 | func isAccessLevelWithNonAccessLevelModifier(keyword: Keyword) { 30 | let sut = SUT(name: .keyword(keyword)) 31 | 32 | #expect(!sut.isAccessLevel) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/FunctionDeclSyntax/FunctionDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct FunctionDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = FunctionDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let sut = SUT( 24 | modifiers: DeclModifierListSyntax { accessLevel.modifier }, 25 | name: "sut", 26 | signature: FunctionSignatureSyntax( 27 | parameterClause: FunctionParameterClauseSyntax {} 28 | ) 29 | ) 30 | 31 | #expect(sut.accessLevel == accessLevel) 32 | } 33 | 34 | @Test 35 | func accessLevelWithImplicitInternalAccessLevelModifier() { 36 | let sut = SUT( 37 | name: "sut", 38 | signature: FunctionSignatureSyntax( 39 | parameterClause: FunctionParameterClauseSyntax {} 40 | ) 41 | ) 42 | 43 | #expect(sut.accessLevel == .internal) 44 | } 45 | 46 | // MARK: With Access Level Tests 47 | 48 | @Test(arguments: AccessLevelSyntax.allCases, AccessLevelSyntax.allCases) 49 | func withAccessLevelWithExplicitAccessLevelModifier( 50 | oldAccessLevel: AccessLevelSyntax, 51 | newAccessLevel: AccessLevelSyntax 52 | ) throws { 53 | let sut = try SUT( 54 | modifiers: DeclModifierListSyntax { 55 | oldAccessLevel.modifier 56 | DeclModifierSyntax(name: .keyword(.static)) 57 | }, 58 | name: "sut", 59 | signature: FunctionSignatureSyntax( 60 | parameterClause: FunctionParameterClauseSyntax {} 61 | ) 62 | ) 63 | .withAccessLevel(newAccessLevel) 64 | 65 | let expectedModifierCount = newAccessLevel == .internal ? 1 : 2 66 | 67 | #expect(sut.accessLevel == newAccessLevel) 68 | #expect(sut.modifiers.count == expectedModifierCount) 69 | } 70 | 71 | @Test(arguments: AccessLevelSyntax.allCases) 72 | func withAccessLevelWithImplicitInternalAccessLevelModifier( 73 | newAccessLevel: AccessLevelSyntax 74 | ) throws { 75 | let sut = try SUT( 76 | modifiers: DeclModifierListSyntax { 77 | DeclModifierSyntax(name: .keyword(.static)) 78 | }, 79 | name: "sut", 80 | signature: FunctionSignatureSyntax( 81 | parameterClause: FunctionParameterClauseSyntax {} 82 | ) 83 | ) 84 | .withAccessLevel(newAccessLevel) 85 | 86 | let expectedModifierCount = newAccessLevel == .internal ? 1 : 2 87 | 88 | #expect(sut.accessLevel == newAccessLevel) 89 | #expect(sut.modifiers.count == expectedModifierCount) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/FunctionDeclSyntax/FunctionDeclSyntax_EffectsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionDeclSyntax_EffectsTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct FunctionDeclSyntax_EffectsTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = FunctionDeclSyntax 16 | 17 | // MARK: Is Async Tests 18 | 19 | @Test 20 | func isAsyncWithNilAsyncSpecifier() { 21 | let sut = SUT( 22 | name: "sut", 23 | signature: FunctionSignatureSyntax( 24 | parameterClause: FunctionParameterClauseSyntax {}, 25 | effectSpecifiers: FunctionEffectSpecifiersSyntax() 26 | ) 27 | ) 28 | 29 | #expect(!sut.isAsync) 30 | } 31 | 32 | @Test 33 | func isAsyncWithNonNilAsyncSpecifier() { 34 | let sut = SUT( 35 | name: "sut", 36 | signature: FunctionSignatureSyntax( 37 | parameterClause: FunctionParameterClauseSyntax {}, 38 | effectSpecifiers: FunctionEffectSpecifiersSyntax( 39 | asyncSpecifier: .keyword(.async) 40 | ) 41 | ) 42 | ) 43 | 44 | #expect(sut.isAsync) 45 | } 46 | 47 | @Test 48 | func isAsyncWithNilEffectSpecifiers() { 49 | let sut = SUT( 50 | name: "sut", 51 | signature: FunctionSignatureSyntax( 52 | parameterClause: FunctionParameterClauseSyntax {} 53 | ) 54 | ) 55 | 56 | #expect(!sut.isAsync) 57 | } 58 | 59 | // MARK: Is Throwing Tests 60 | 61 | @Test 62 | func isThrowingWithNilThrowsSpecifier() { 63 | let sut = SUT( 64 | name: "sut", 65 | signature: FunctionSignatureSyntax( 66 | parameterClause: FunctionParameterClauseSyntax {}, 67 | effectSpecifiers: FunctionEffectSpecifiersSyntax() 68 | ) 69 | ) 70 | 71 | #expect(!sut.isThrowing) 72 | } 73 | 74 | @Test 75 | func isThrowingWithNonNilThrowsSpecifier() { 76 | let sut = SUT( 77 | name: "sut", 78 | signature: FunctionSignatureSyntax( 79 | parameterClause: FunctionParameterClauseSyntax {}, 80 | effectSpecifiers: FunctionEffectSpecifiersSyntax( 81 | throwsClause: ThrowsClauseSyntax( 82 | throwsSpecifier: .keyword(.throws) 83 | ) 84 | ) 85 | ) 86 | ) 87 | 88 | #expect(sut.isThrowing) 89 | } 90 | 91 | @Test 92 | func isThrowingWithNilEffectSpecifiers() { 93 | let sut = SUT( 94 | name: "sut", 95 | signature: FunctionSignatureSyntax( 96 | parameterClause: FunctionParameterClauseSyntax {} 97 | ) 98 | ) 99 | 100 | #expect(!sut.isThrowing) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/FunctionParameterSyntax/FunctionParameterSyntax_IsVariadicTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionParameterSyntax_IsVariadicTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct FunctionParameterSyntax_IsVariadicTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = FunctionParameterSyntax 16 | 17 | // MARK: Is Variadic Tests 18 | 19 | @Test 20 | func isVariadicWhenEllipsisIsNil() { 21 | let sut = SUT( 22 | firstName: "parameter", 23 | colon: .colonToken(), 24 | type: .int 25 | ) 26 | 27 | #expect(!sut.isVariadic) 28 | } 29 | 30 | @Test 31 | func isVariadicWhenEllipsisIsNotNil() { 32 | let sut = SUT( 33 | firstName: "parameter", 34 | colon: .colonToken(), 35 | type: .int, 36 | ellipsis: .ellipsisToken() 37 | ) 38 | 39 | #expect(sut.isVariadic) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/FunctionTypeSyntax/FunctionTypeSyntax_EscapingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FunctionTypeSyntax_EscapingTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct FunctionTypeSyntax_EscapingTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = FunctionTypeSyntax 16 | 17 | // MARK: Escaping Tests 18 | 19 | @Test 20 | func escaping() { 21 | let sut = SUT( 22 | parameters: TupleTypeElementListSyntax(), 23 | returnClause: ReturnClauseSyntax(type: .bool) 24 | ) 25 | 26 | let sutEscaping = sut.escaping() 27 | let expectedDescription = "@escaping()->Bool" 28 | 29 | #expect(sutEscaping.description == expectedDescription) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/InheritanceClauseSyntax/InheritanceClauseSyntax_InheritedTypesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InheritanceClauseSyntax_InheritedTypesTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct InheritanceClauseSyntax_InheritedTypesTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = InheritanceClauseSyntax 16 | 17 | // MARK: Identifier Types Tests 18 | 19 | @Test 20 | func identifierTypes() { 21 | let sut = SUT { 22 | InheritedTypeSyntax( 23 | type: IdentifierTypeSyntax(name: "Hashable") 24 | ) 25 | InheritedTypeSyntax( 26 | type: ArrayTypeSyntax( 27 | element: IdentifierTypeSyntax(name: "String") 28 | ) 29 | ) 30 | InheritedTypeSyntax( 31 | type: IdentifierTypeSyntax(name: "Identifiable") 32 | ) 33 | } 34 | 35 | let identifierTypes = sut.inheritedTypes(ofType: IdentifierTypeSyntax.self) 36 | let identifierTypeTokenKinds = identifierTypes.map(\.name.tokenKind) 37 | let expectedIdentifierTypeTokenKinds: [TokenKind] = [ 38 | .identifier("Hashable"), 39 | .identifier("Identifiable"), 40 | ] 41 | 42 | #expect(identifierTypeTokenKinds == expectedIdentifierTypeTokenKinds) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/InitializerDeclSyntax/InitializerDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitializerDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct InitializerDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = InitializerDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let sut = SUT( 24 | modifiers: DeclModifierListSyntax { accessLevel.modifier }, 25 | signature: FunctionSignatureSyntax( 26 | parameterClause: FunctionParameterClauseSyntax {} 27 | ) 28 | ) 29 | 30 | #expect(sut.accessLevel == accessLevel) 31 | } 32 | 33 | @Test 34 | func accessLevelWithImplicitInternalAccessLevelModifier() { 35 | let sut = SUT( 36 | signature: FunctionSignatureSyntax( 37 | parameterClause: FunctionParameterClauseSyntax {} 38 | ) 39 | ) 40 | 41 | #expect(sut.accessLevel == .internal) 42 | } 43 | 44 | // MARK: With Access Level Tests 45 | 46 | @Test(arguments: AccessLevelSyntax.allCases, AccessLevelSyntax.allCases) 47 | func withAccessLevelWithExplicitAccessLevelModifier( 48 | oldAccessLevel: AccessLevelSyntax, 49 | newAccessLevel: AccessLevelSyntax 50 | ) throws { 51 | let sut = try SUT( 52 | modifiers: DeclModifierListSyntax { 53 | oldAccessLevel.modifier 54 | }, 55 | signature: FunctionSignatureSyntax( 56 | parameterClause: FunctionParameterClauseSyntax {} 57 | ) 58 | ) 59 | .withAccessLevel(newAccessLevel) 60 | 61 | let expectedModifierCount = newAccessLevel == .internal ? 0 : 1 62 | 63 | #expect(sut.accessLevel == newAccessLevel) 64 | #expect(sut.modifiers.count == expectedModifierCount) 65 | } 66 | 67 | @Test(arguments: AccessLevelSyntax.allCases) 68 | func withAccessLevelWithImplicitInternalAccessLevelModifier( 69 | newAccessLevel: AccessLevelSyntax 70 | ) throws { 71 | let sut = try SUT( 72 | signature: FunctionSignatureSyntax( 73 | parameterClause: FunctionParameterClauseSyntax {} 74 | ) 75 | ) 76 | .withAccessLevel(newAccessLevel) 77 | 78 | let expectedModifierCount = newAccessLevel == .internal ? 0 : 1 79 | 80 | #expect(sut.accessLevel == newAccessLevel) 81 | #expect(sut.modifiers.count == expectedModifierCount) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/MemberBlockSyntax/MemberBlockSyntax_MemberDeclarationsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemberBlockSyntax_MemberDeclarationsTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct MemberBlockSyntax_MemberDeclarationsTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ProtocolDeclSyntax 16 | 17 | // MARK: Associated Type Declarations Tests 18 | 19 | @Test 20 | func associatedTypeDeclarationsWithEmptyMemberBlock() { 21 | let sut = SUT(name: "SUT") {} 22 | let associatedTypeDeclarations = sut.memberBlock.memberDeclarations( 23 | ofType: AssociatedTypeDeclSyntax.self 24 | ) 25 | 26 | #expect(associatedTypeDeclarations.isEmpty) 27 | } 28 | 29 | @Test 30 | func associatedTypeDeclarationsWithNonEmptyMemberBlockWithAssociatedTypes() { 31 | let sut = SUT(name: "SUT") { 32 | self.propertyDeclaration(name: "a") 33 | self.associatedTypeDeclaration(name: "A") 34 | self.propertyDeclaration(name: "b") 35 | self.associatedTypeDeclaration(name: "B") 36 | } 37 | let associatedTypeDeclarations = sut.memberBlock.memberDeclarations( 38 | ofType: AssociatedTypeDeclSyntax.self 39 | ) 40 | 41 | #expect(associatedTypeDeclarations.count == 2) 42 | } 43 | 44 | @Test 45 | func associatedTypeDeclarationsWithNonEmptyMemberBlockWithoutAssociatedTypes() { 46 | let sut = SUT(name: "SUT") { 47 | self.propertyDeclaration(name: "a") 48 | self.propertyDeclaration(name: "b") 49 | } 50 | let associatedTypeDeclarations = sut.memberBlock.memberDeclarations( 51 | ofType: AssociatedTypeDeclSyntax.self 52 | ) 53 | 54 | #expect(associatedTypeDeclarations.isEmpty) 55 | } 56 | 57 | // MARK: Function Declarations Tests 58 | 59 | @Test 60 | func functionDeclarations() { 61 | let sut = SUT(name: "SUT") { 62 | self.initializerDeclaration() 63 | self.propertyDeclaration(name: "property1") 64 | self.initializerDeclaration() 65 | self.methodDeclaration(name: "method1") 66 | self.initializerDeclaration() 67 | self.propertyDeclaration(name: "property2") 68 | self.initializerDeclaration() 69 | self.methodDeclaration(name: "method2") 70 | self.initializerDeclaration() 71 | self.propertyDeclaration(name: "property3") 72 | self.initializerDeclaration() 73 | } 74 | let functionDeclarations = sut.memberBlock.memberDeclarations( 75 | ofType: FunctionDeclSyntax.self 76 | ) 77 | 78 | #expect(functionDeclarations.count == 2) 79 | } 80 | 81 | // MARK: Initializer Declarations Tests 82 | 83 | @Test 84 | func initializerDeclarations() { 85 | let sut = SUT(name: "SUT") { 86 | self.initializerDeclaration() 87 | self.propertyDeclaration(name: "property1") 88 | self.initializerDeclaration() 89 | self.methodDeclaration(name: "method1") 90 | self.initializerDeclaration() 91 | self.propertyDeclaration(name: "property2") 92 | self.initializerDeclaration() 93 | self.methodDeclaration(name: "method2") 94 | self.initializerDeclaration() 95 | self.propertyDeclaration(name: "property3") 96 | self.initializerDeclaration() 97 | } 98 | let initializerDeclarations = sut.memberBlock.memberDeclarations( 99 | ofType: InitializerDeclSyntax.self 100 | ) 101 | 102 | #expect(initializerDeclarations.count == 6) 103 | } 104 | 105 | // MARK: Variable Declarations Tests 106 | 107 | @Test 108 | func variableDeclarations() { 109 | let sut = SUT(name: "SUT") { 110 | self.initializerDeclaration() 111 | self.propertyDeclaration(name: "property1") 112 | self.initializerDeclaration() 113 | self.methodDeclaration(name: "method1") 114 | self.initializerDeclaration() 115 | self.propertyDeclaration(name: "property2") 116 | self.initializerDeclaration() 117 | self.methodDeclaration(name: "method2") 118 | self.initializerDeclaration() 119 | self.propertyDeclaration(name: "property3") 120 | self.initializerDeclaration() 121 | } 122 | let variableDeclarations = sut.memberBlock.memberDeclarations( 123 | ofType: VariableDeclSyntax.self 124 | ) 125 | 126 | #expect(variableDeclarations.count == 3) 127 | } 128 | } 129 | 130 | // MARK: - Helpers 131 | 132 | extension MemberBlockSyntax_MemberDeclarationsTests { 133 | private func associatedTypeDeclaration( 134 | name: TokenSyntax 135 | ) -> AssociatedTypeDeclSyntax { 136 | AssociatedTypeDeclSyntax(name: name) 137 | } 138 | 139 | private func initializerDeclaration() -> InitializerDeclSyntax { 140 | InitializerDeclSyntax( 141 | signature: FunctionSignatureSyntax( 142 | parameterClause: FunctionParameterClauseSyntax {} 143 | ) 144 | ) {} 145 | } 146 | 147 | private func methodDeclaration(name: String) -> FunctionDeclSyntax { 148 | FunctionDeclSyntax( 149 | name: .identifier(name), 150 | signature: FunctionSignatureSyntax( 151 | parameterClause: FunctionParameterClauseSyntax {} 152 | ) 153 | ) {} 154 | } 155 | 156 | private func propertyDeclaration(name: PatternSyntax) -> VariableDeclSyntax { 157 | VariableDeclSyntax(.var, name: name) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/ProtocolDeclSyntax/ProtocolDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct ProtocolDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ProtocolDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let sut = SUT( 24 | modifiers: DeclModifierListSyntax { 25 | accessLevel.modifier 26 | DeclModifierSyntax(name: .keyword(.static)) 27 | }, 28 | name: "SUT" 29 | ) {} 30 | 31 | #expect(sut.accessLevel == accessLevel) 32 | } 33 | 34 | @Test 35 | func accessLevelWithImplicitInternalAccessLevelModifier() { 36 | let sut = SUT( 37 | modifiers: DeclModifierListSyntax { 38 | DeclModifierSyntax(name: .keyword(.static)) 39 | }, 40 | name: "SUT" 41 | ) {} 42 | 43 | #expect(sut.accessLevel == .internal) 44 | } 45 | 46 | // MARK: Minimum Conforming Access Level Tests 47 | 48 | @Test(arguments: AccessLevelSyntax.allCases) 49 | func minimumConformingAccessLevelWithExplicitAccessLevelModifier( 50 | from accessLevel: AccessLevelSyntax 51 | ) { 52 | let sut = SUT( 53 | modifiers: DeclModifierListSyntax { 54 | accessLevel.modifier 55 | DeclModifierSyntax(name: .keyword(.static)) 56 | }, 57 | name: "SUT" 58 | ) {} 59 | 60 | let expectedMinimumConformingAccessLevel: AccessLevelSyntax = switch accessLevel { 61 | case .fileprivate, .private: .fileprivate 62 | case .internal, .open, .package, .public: accessLevel 63 | } 64 | 65 | #expect( 66 | sut.minimumConformingAccessLevel == expectedMinimumConformingAccessLevel 67 | ) 68 | } 69 | 70 | @Test 71 | func minimumConformingAccessLevelWithImplicitInternalAccessLevelModifier() { 72 | let sut = SUT( 73 | modifiers: DeclModifierListSyntax { 74 | DeclModifierSyntax(name: .keyword(.static)) 75 | }, 76 | name: "SUT" 77 | ) {} 78 | 79 | #expect(sut.minimumConformingAccessLevel == .internal) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/ProtocolDeclSyntax/ProtocolDeclSyntax_ActorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax_ActorTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct ProtocolDeclSyntax_ActorTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ProtocolDeclSyntax 16 | 17 | // MARK: Is Actor Constrained Tests 18 | 19 | @Test 20 | func isActorConstrainedWithNilInheritanceClause() { 21 | let sut = SUT(name: "SUT") {} 22 | 23 | #expect(!sut.isActorConstrained) 24 | } 25 | 26 | @Test 27 | func isActorConstrainedWithNonNilInheritanceClauseWithActorConformance() { 28 | let sut = SUT( 29 | name: "SUT", 30 | inheritanceClause: InheritanceClauseSyntax { 31 | InheritedTypeSyntax(type: TypeSyntax(describing: (any Hashable).self)) 32 | InheritedTypeSyntax(type: TypeSyntax(describing: (any Actor).self)) 33 | InheritedTypeSyntax(type: TypeSyntax(describing: (any Identifiable).self)) 34 | } 35 | ) {} 36 | 37 | #expect(sut.isActorConstrained) 38 | } 39 | 40 | @Test 41 | func isActorConstrainedWithNonNilInheritanceClauseWithoutActorConformance() { 42 | let sut = SUT( 43 | name: "SUT", 44 | inheritanceClause: InheritanceClauseSyntax { 45 | InheritedTypeSyntax(type: TypeSyntax(describing: (any Hashable).self)) 46 | InheritedTypeSyntax(type: TypeSyntax(describing: (any Identifiable).self)) 47 | } 48 | ) {} 49 | 50 | #expect(!sut.isActorConstrained) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/ProtocolDeclSyntax/ProtocolDeclSyntax_GenericWhereClausesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax_GenericWhereClausesTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct ProtocolDeclSyntax_GenericWhereClausesTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ProtocolDeclSyntax 16 | 17 | // MARK: Generic Where Clauses Tests 18 | 19 | @Test 20 | func genericWhereClauses() throws { 21 | let sut = SUT( 22 | name: "SUT", 23 | genericWhereClause: GenericWhereClauseSyntax {} 24 | ) { 25 | AssociatedTypeDeclSyntax( 26 | name: "A", 27 | genericWhereClause: GenericWhereClauseSyntax {} 28 | ) 29 | AssociatedTypeDeclSyntax(name: "B") 30 | AssociatedTypeDeclSyntax( 31 | name: "C", 32 | genericWhereClause: GenericWhereClauseSyntax {} 33 | ) 34 | } 35 | 36 | #expect(sut.genericWhereClauses.count == 3) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/ProtocolDeclSyntax/ProtocolDeclSyntax_TypeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtocolDeclSyntax_TypeTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct ProtocolDeclSyntax_TypeTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = ProtocolDeclSyntax 16 | 17 | // MARK: Type Tests 18 | 19 | @Test 20 | func type() throws { 21 | let sut = SUT(name: "SUT") {} 22 | 23 | let type = sut.type 24 | let identifierType = try #require(type.as(IdentifierTypeSyntax.self)) 25 | let identifierTypeTokenKind = identifierType.name.tokenKind 26 | let expectedIdentifierTypeTokenKind: TokenKind = .identifier("SUT") 27 | 28 | #expect(identifierTypeTokenKind == expectedIdentifierTypeTokenKind) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/StructDeclSyntax/StructDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StructDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct StructDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = StructDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let modifiers = DeclModifierListSyntax { 24 | accessLevel.modifier 25 | DeclModifierSyntax(name: .keyword(.final)) 26 | } 27 | let sut = SUT(modifiers: modifiers, name: "SUT") {} 28 | 29 | #expect(sut.accessLevel == accessLevel) 30 | } 31 | 32 | @Test 33 | func accessLevelWithImplicitInternalAccessLevelModifier() { 34 | let modifiers = DeclModifierListSyntax { 35 | DeclModifierSyntax(name: .keyword(.final)) 36 | } 37 | let sut = SUT(modifiers: modifiers, name: "SUT") {} 38 | 39 | #expect(sut.accessLevel == .internal) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/SyntaxProtocol/SyntaxProtocol_WithAttributeListSyntaxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxProtocol_WithAttributeListSyntaxTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct SyntaxProtocol_WithAttributeListSyntaxTests { 12 | 13 | // MARK: With Optional AttributeListSyntax Tests 14 | 15 | @Test 16 | func withOptionalAttributeListSyntax() throws { 17 | let sut = try FunctionDeclSyntax( 18 | name: "sut", 19 | signature: FunctionSignatureSyntax( 20 | parameterClause: FunctionParameterClauseSyntax {} 21 | ) 22 | ) 23 | .with(\.attributes) { 24 | AttributeSyntax( 25 | atSign: .atSignToken(), 26 | attributeName: IdentifierTypeSyntax( 27 | name: .identifier("MainActor") 28 | ) 29 | ) 30 | } 31 | 32 | let attribute = try #require(sut.attributes.first) 33 | 34 | guard case let .attribute(attribute) = attribute else { 35 | Issue.record("Expected .attribute") 36 | return 37 | } 38 | 39 | let attributeName = try #require( 40 | attribute.attributeName.as(IdentifierTypeSyntax.self) 41 | ) 42 | 43 | #expect(sut.attributes.count == 1) 44 | #expect(attribute.atSign.tokenKind == .atSign) 45 | #expect(attributeName.name.tokenKind == .identifier("MainActor")) 46 | } 47 | 48 | @Test 49 | func withOptionalAttributeListSyntaxWithClosureParameter() throws { 50 | let sut = try FunctionDeclSyntax( 51 | name: "sut", 52 | signature: FunctionSignatureSyntax( 53 | parameterClause: FunctionParameterClauseSyntax {} 54 | ) 55 | ) 56 | .with(\.attributes) { _ in 57 | AttributeSyntax( 58 | atSign: .atSignToken(), 59 | attributeName: IdentifierTypeSyntax( 60 | name: .identifier("MainActor") 61 | ) 62 | ) 63 | } 64 | 65 | let attribute = try #require(sut.attributes.first) 66 | 67 | guard case let .attribute(attribute) = attribute else { 68 | Issue.record("Expected .attribute") 69 | return 70 | } 71 | 72 | let attributeName = try #require( 73 | attribute.attributeName.as(IdentifierTypeSyntax.self) 74 | ) 75 | 76 | #expect(sut.attributes.count == 1) 77 | #expect(attribute.atSign.tokenKind == .atSign) 78 | #expect(attributeName.name.tokenKind == .identifier("MainActor")) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/SyntaxProtocol/SyntaxProtocol_WithCodeBlockSyntaxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxProtocol_WithCodeBlockSyntaxTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct SyntaxProtocol_WithCodeBlockSyntaxTests { 12 | 13 | // MARK: With Optional CodeBlockSyntax Tests 14 | 15 | @Test 16 | func withOptionalCodeBlockSyntax() throws { 17 | let sut = try FunctionDeclSyntax( 18 | name: "sut", 19 | signature: FunctionSignatureSyntax( 20 | parameterClause: FunctionParameterClauseSyntax {} 21 | ) 22 | ) 23 | .with(\.body) {} 24 | 25 | #expect(sut.body != nil) 26 | } 27 | 28 | @Test 29 | func withOptionalCodeBlockSyntaxWithClosureParameter() throws { 30 | let sut = try FunctionDeclSyntax( 31 | name: "sut", 32 | signature: FunctionSignatureSyntax( 33 | parameterClause: FunctionParameterClauseSyntax {} 34 | ) 35 | ) 36 | .with(\.body) { _ in } 37 | 38 | #expect(sut.body != nil) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/SyntaxProtocol/SyntaxProtocol_WithDeclModifierListSyntaxTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxProtocol_WithDeclModifierListSyntaxTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct SyntaxProtocol_WithDeclModifierListSyntaxTests { 12 | 13 | // MARK: With Optional DeclModifierListSyntax Tests 14 | 15 | @Test 16 | func withOptionalDeclModifierListSyntax() throws { 17 | let sut = try FunctionDeclSyntax( 18 | name: "sut", 19 | signature: FunctionSignatureSyntax( 20 | parameterClause: FunctionParameterClauseSyntax {} 21 | ) 22 | ) 23 | .with(\.modifiers) { 24 | DeclModifierSyntax(name: .keyword(.private)) 25 | DeclModifierSyntax(name: .keyword(.static)) 26 | } 27 | 28 | let modifier1 = try #require(sut.modifiers.first) 29 | let modifier2 = try #require(sut.modifiers.last) 30 | 31 | #expect(sut.modifiers.count == 2) 32 | #expect(modifier1.name.tokenKind == .keyword(.private)) 33 | #expect(modifier2.name.tokenKind == .keyword(.static)) 34 | } 35 | 36 | @Test 37 | func withOptionalDeclModifierListSyntaxWithClosureParameter() throws { 38 | let sut = try FunctionDeclSyntax( 39 | name: "sut", 40 | signature: FunctionSignatureSyntax( 41 | parameterClause: FunctionParameterClauseSyntax {} 42 | ) 43 | ) 44 | .with(\.modifiers) { _ in 45 | DeclModifierSyntax(name: .keyword(.private)) 46 | DeclModifierSyntax(name: .keyword(.static)) 47 | } 48 | 49 | let modifier1 = try #require(sut.modifiers.first) 50 | let modifier2 = try #require(sut.modifiers.last) 51 | 52 | #expect(sut.modifiers.count == 2) 53 | #expect(modifier1.name.tokenKind == .keyword(.private)) 54 | #expect(modifier2.name.tokenKind == .keyword(.static)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/TypeSyntax/TypeSyntax_DescribingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeSyntax_DescribingTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct TypeSyntax_DescribingTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = TypeSyntax 16 | 17 | // MARK: Initializer Tests 18 | 19 | @Test 20 | func initDescribingSubject() throws { 21 | let sut = SUT(describing: Int.self) 22 | 23 | let identifierType = try #require(sut.as(IdentifierTypeSyntax.self)) 24 | let identifierTypeTokenKind = identifierType.name.tokenKind 25 | 26 | #expect(identifierTypeTokenKind == .identifier("Int")) 27 | } 28 | 29 | // MARK: Static Property Tests 30 | 31 | @Test( 32 | arguments: [ 33 | (SUT.bool, "Bool"), 34 | (SUT.int, "Int"), 35 | (SUT.string, "String"), 36 | (SUT.void, "Void"), 37 | ] 38 | ) 39 | func staticProperty(sut: SUT, expectedIdentifier: String) throws { 40 | let identifierType = try #require(sut.as(IdentifierTypeSyntax.self)) 41 | let identifierTypeTokenKind = identifierType.name.tokenKind 42 | 43 | #expect(identifierTypeTokenKind == .identifier(expectedIdentifier)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/SwiftSyntaxSugarTests/VariableDeclSyntax/VariableDeclSyntax_AccessLevelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VariableDeclSyntax_AccessLevelTests.swift 3 | // 4 | // Copyright © 2025 Fetch. 5 | // 6 | 7 | import SwiftSyntax 8 | import Testing 9 | @testable import SwiftSyntaxSugar 10 | 11 | struct VariableDeclSyntax_AccessLevelTests { 12 | 13 | // MARK: Typealiases 14 | 15 | typealias SUT = VariableDeclSyntax 16 | 17 | // MARK: Access Level Tests 18 | 19 | @Test(arguments: AccessLevelSyntax.allCases) 20 | func accessLevelWithExplicitAccessLevelModifier( 21 | from accessLevel: AccessLevelSyntax 22 | ) { 23 | let sut = SUT( 24 | modifiers: DeclModifierListSyntax { accessLevel.modifier }, 25 | .var, 26 | name: "sut" 27 | ) 28 | 29 | #expect(sut.accessLevel == accessLevel) 30 | } 31 | 32 | @Test 33 | func accessLevelWithImplicitInternalAccessLevelModifier() { 34 | let sut = SUT(.var, name: "sut") 35 | 36 | #expect(sut.accessLevel == .internal) 37 | } 38 | 39 | // MARK: With Access Level Tests 40 | 41 | @Test(arguments: AccessLevelSyntax.allCases, AccessLevelSyntax.allCases) 42 | func withAccessLevelWithExplicitAccessLevelModifier( 43 | oldAccessLevel: AccessLevelSyntax, 44 | newAccessLevel: AccessLevelSyntax 45 | ) throws { 46 | let sut = try SUT( 47 | modifiers: DeclModifierListSyntax { 48 | oldAccessLevel.modifier 49 | DeclModifierSyntax(name: .keyword(.static)) 50 | }, 51 | .var, 52 | name: "sut" 53 | ) 54 | .withAccessLevel(newAccessLevel) 55 | 56 | let expectedModifierCount = newAccessLevel == .internal ? 1 : 2 57 | 58 | #expect(sut.accessLevel == newAccessLevel) 59 | #expect(sut.modifiers.count == expectedModifierCount) 60 | } 61 | 62 | @Test(arguments: AccessLevelSyntax.allCases) 63 | func withAccessLevelWithImplicitInternalAccessLevelModifier( 64 | newAccessLevel: AccessLevelSyntax 65 | ) throws { 66 | let sut = try SUT( 67 | modifiers: DeclModifierListSyntax { 68 | DeclModifierSyntax(name: .keyword(.static)) 69 | }, 70 | .var, 71 | name: "sut" 72 | ) 73 | .withAccessLevel(newAccessLevel) 74 | 75 | let expectedModifierCount = newAccessLevel == .internal ? 1 : 2 76 | 77 | #expect(sut.accessLevel == newAccessLevel) 78 | #expect(sut.modifiers.count == expectedModifierCount) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "Tests" 3 | --------------------------------------------------------------------------------