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
This commit is contained in:
Nicola Corti
2026-09-15 11:23:27 -07:00
committed by meta-codesync[bot]
parent 2334bf755d
commit 30b4c7e947
3 changed files with 228 additions and 2 deletions
+78
View File
@@ -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
}
+4 -2
View File
@@ -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",
+146
View File
@@ -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();