From 30b4c7e947fc86b6125928c2c79acd40d538ba9a Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Tue, 15 Sep 2026 11:23:27 -0700 Subject: [PATCH] Add Swift to `yarn format` (#58463) Summary: Pull Request resolved: https://github.com/react/react-native/pull/58463 Add `yarn format-swift` and `yarn format-check-swift` using Apple swift-format, and compose them into the repository-wide commands. The wrapper selects a repository-provided formatter when available or uses swift-format 6.3 or newer from the Swift toolchain. Missing tools produce environment-specific setup guidance before Swift is skipped. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D119487615 fbshipit-source-id: 4da9939aa7ddf400e810cd17aeec12aa6118023b --- .swift-format | 78 +++++++++++++++++++++ package.json | 6 +- scripts/format-swift.js | 146 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 .swift-format create mode 100644 scripts/format-swift.js diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000000..6688179cb24 --- /dev/null +++ b/.swift-format @@ -0,0 +1,78 @@ +{ + "indentConditionalCompilationBlocks": false, + "indentSwitchCaseLabels": false, + "indentation": { + "spaces": 2 + }, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineBreakBetweenDeclarationAttributes": false, + "lineLength": 2000, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "orderedImports": { + "includeConditionalImports": false, + "shouldGroupImports": false + }, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": true, + "reflowMultilineStringLiterals": { + "never": {} + }, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": false, + "AmbiguousTrailingClosureOverload": false, + "AvoidRetroactiveConformances": false, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": false, + "FileScopedDeclarationPrivacy": false, + "FullyIndirectEnum": false, + "GroupNumericLiterals": false, + "IdentifiersMustBeASCII": false, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": false, + "NoAssignmentInExpressions": true, + "NoBlockComments": false, + "NoCasesWithOnlyFallthrough": false, + "NoEmptyLinesOpeningClosingBraces": false, + "NoEmptyTrailingClosureParentheses": false, + "NoLabelsInCasePatterns": false, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": false, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": false, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": false, + "OrderedImports": true, + "ReplaceForEachWithForLoop": false, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": false, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": false, + "UseLetInEveryBoundCaseVariable": false, + "UseShorthandTypeNames": false, + "UseSingleLinePropertyGetter": false, + "UseSynthesizedInitializer": false, + "UseTripleSlashForDocumentationComments": false, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "spacesBeforeEndOfLineComments": 1, + "tabWidth": 8, + "version": 1 +} diff --git a/package.json b/package.json index 86b24ef28b9..61d664d3d1e 100644 --- a/package.json +++ b/package.json @@ -14,18 +14,20 @@ "cxx-api-validate": "python -m scripts.cxx-api.parser --validate", "flow-check": "flow full-check", "flow": "flow", - "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin && yarn format-check-java && yarn format-check-python", + "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin && yarn format-check-java && yarn format-check-python && yarn format-check-swift", "format-check-cpp": "node ./scripts/clang-format.js --check", "format-check-java": "node ./scripts/format-java.js --check", "format-check-javascript": "prettier --check \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", "format-check-kotlin": "node ./scripts/format-kotlin.js --check", "format-check-python": "node ./scripts/format-python.js --check", - "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin && yarn format-java && yarn format-python", + "format-check-swift": "node ./scripts/format-swift.js --check", + "format": "yarn format-javascript && yarn format-cpp && yarn format-kotlin && yarn format-java && yarn format-python && yarn format-swift", "format-cpp": "node ./scripts/clang-format.js", "format-java": "node ./scripts/format-java.js", "format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", "format-kotlin": "node ./scripts/format-kotlin.js", "format-python": "node ./scripts/format-python.js", + "format-swift": "node ./scripts/format-swift.js", "featureflags": "yarn --cwd packages/react-native featureflags", "js-api-diff": "node ./scripts/js-api/diff-api-snapshot", "lint-markdown": "markdownlint-cli2 2>&1", diff --git a/scripts/format-swift.js b/scripts/format-swift.js new file mode 100644 index 00000000000..74885bd7d79 --- /dev/null +++ b/scripts/format-swift.js @@ -0,0 +1,146 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @noflow + * @format + */ + +'use strict'; + +const { + IS_META_CHECKOUT, + commandVersion, + findMetaTool, +} = require('./format-utils'); +const {spawnSync} = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); +const {globSync} = require('tinyglobby'); + +const REPO_ROOT = path.resolve(__dirname, '..'); +const CONFIG = path.join(REPO_ROOT, '.swift-format'); +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MINIMUM_SWIFT_FORMAT_MAJOR = 6; +const MINIMUM_SWIFT_FORMAT_MINOR = 3; +const MAX_FILES_PER_PROCESS = 100; +const MAX_HEADER_BYTES = 4096; +const IGNORE = ['**/Pods/**', '**/build/**', '**/node_modules/**']; + +function isGenerated(file) { + let fd; + try { + fd = fs.openSync(path.resolve(REPO_ROOT, file), 'r'); + const header = Buffer.alloc(MAX_HEADER_BYTES); + const bytesRead = fs.readSync(fd, header, 0, header.length, 0); + return header.subarray(0, bytesRead).includes(GENERATED_MARKER); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Unable to inspect ${file}: ${message}`, {cause: error}); + } finally { + if (fd != null) { + fs.closeSync(fd); + } + } +} + +function parseSwiftFormatVersion(output) { + const version = + /swift-format(?: version)?[:\s]+(\d+)\.(\d+)/i.exec(output) ?? + /Swift version\s+(\d+)\.(\d+)/i.exec(output) ?? + /^\s*(\d+)\.(\d+)/.exec(output); + if (version == null) { + return null; + } + const reportedMajor = Number(version[1]); + return reportedMajor >= 100 + ? [Math.floor(reportedMajor / 100), reportedMajor % 100] + : [reportedMajor, Number(version[2])]; +} + +function findSwiftFormat() { + const candidates = []; + if (process.env.SWIFT_FORMAT != null && process.env.SWIFT_FORMAT !== '') { + candidates.push([process.env.SWIFT_FORMAT, []]); + } else { + const metaSwiftFormat = findMetaTool( + 'tools', + 'lint', + 'swift-format', + 'swift-format', + ); + if (metaSwiftFormat != null) { + candidates.push([ + metaSwiftFormat.command, + metaSwiftFormat.prefixArguments, + ]); + } + candidates.push(['swift-format', []], ['swift', ['format']]); + } + for (const [command, prefixArguments] of candidates) { + const result = commandVersion(command, prefixArguments); + const version = parseSwiftFormatVersion(result.output); + if ( + result.status === 0 && + version != null && + (version[0] > MINIMUM_SWIFT_FORMAT_MAJOR || + (version[0] === MINIMUM_SWIFT_FORMAT_MAJOR && + version[1] >= MINIMUM_SWIFT_FORMAT_MINOR)) + ) { + return {command, prefixArguments}; + } + } + const instructions = IS_META_CHECKOUT + ? 'Meta: unset SWIFT_FORMAT and run `tools/lint/swift-format/swift-format --version` from the fbsource root. If that fails, repair your Meta DotSlash setup.' + : 'Please install Swift 6.3 or newer and make sure `swift-format` or `swift` is in your PATH, or set SWIFT_FORMAT=/path/to/swift-format.'; + console.warn( + 'warning: Skipping Swift formatting because swift-format 6.3 or newer was not found.\n' + + instructions, + ); + return null; +} + +function main() { + const check = process.argv[2] === '--check'; + const swiftFormat = findSwiftFormat(); + if (swiftFormat == null) { + return; + } + const files = globSync('**/*.swift', {cwd: REPO_ROOT, ignore: IGNORE}).filter( + file => !isGenerated(file), + ); + + let exitStatus = 0; + for (let i = 0; i < files.length; i += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + swiftFormat.command, + [ + ...swiftFormat.prefixArguments, + check ? 'lint' : 'format', + '--configuration', + CONFIG, + ...(check ? ['--strict'] : ['--in-place']), + ...files.slice(i, i + MAX_FILES_PER_PROCESS), + ], + { + cwd: REPO_ROOT, + env: {...process.env, PWD: REPO_ROOT}, + stdio: 'inherit', + }, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + throw new Error(`swift-format was terminated by ${result.signal}`); + } + if (result.status !== 0) { + exitStatus = result.status ?? 1; + } + } + process.exitCode = exitStatus; +} + +main();