diff --git a/.clang-format-ignore b/.clang-format-ignore index c955f0ad4d5d..8d232f8c190b 100644 --- a/.clang-format-ignore +++ b/.clang-format-ignore @@ -1,3 +1,6 @@ +**/Pods/** +**/build/** +**/node_modules/** packages/react-native/React/I18n/FBXXHashUtils.h packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/** packages/react-native/ReactAndroid/src/main/jni/third-party/** diff --git a/package.json b/package.json index cdba8ff29db9..d98b49bbff4b 100644 --- a/package.json +++ b/package.json @@ -9,22 +9,24 @@ "build-android": "./gradlew :packages:react-native:ReactAndroid:build", "build": "node ./scripts/build/build.js", "build-types": "node ./scripts/js-api/build-types", - "clang-format": "node ./scripts/clang-format.js", "clean": "node ./scripts/build/clean.js", "cxx-api-build": "python -m scripts.cxx-api.parser", "cxx-api-validate": "python -m scripts.cxx-api.parser --validate", "flow-check": "flow full-check", "flow": "flow", - "format-check": "prettier --list-different \"./**/*.{js,md,yml,ts,tsx}\"", - "format": "npm run prettier && npm run clang-format", + "format-check": "yarn format-check-javascript && yarn format-check-cpp && yarn format-check-kotlin", + "format-check-cpp": "node ./scripts/clang-format.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": "yarn format-javascript && yarn format-cpp && yarn format-kotlin", + "format-cpp": "node ./scripts/clang-format.js", + "format-javascript": "prettier --write \"./**/*.{cjs,cts,flow,js,jsx,md,mjs,mts,ts,tsx,yaml,yml}\"", + "format-kotlin": "node ./scripts/format-kotlin.js", "featureflags": "yarn --cwd packages/react-native featureflags", "js-api-diff": "node ./scripts/js-api/diff-api-snapshot", - "lint-kotlin-check": "./gradlew ktfmtCheck", - "lint-kotlin": "./gradlew ktfmtFormat", "lint-markdown": "markdownlint-cli2 2>&1", "lint": "eslint --max-warnings 0 .", "preinstall": "node ./scripts/try-set-hermes-compiler-prebuilt.js", - "prettier": "prettier --write \"./**/*.{js,md,yml,ts,tsx}\"", "shellcheck": "./.github/workflow-scripts/analyze_scripts.sh", "start": "yarn --cwd packages/rn-tester start", "set-version": "node ./scripts/releases/set-version.js", @@ -99,6 +101,7 @@ "jest-junit": "^16.0.0", "jest-snapshot": "^29.7.0", "jsonc-parser": "2.2.1", + "ktfmt": "0.59.0", "markdownlint-cli2": "^0.17.2", "markdownlint-rule-relative-links": "^3.0.0", "memfs": "^4.38.2", diff --git a/scripts/clang-format.js b/scripts/clang-format.js index abdf8c81df45..9b7181374b19 100644 --- a/scripts/clang-format.js +++ b/scripts/clang-format.js @@ -10,18 +10,49 @@ 'use strict'; -const dotslash = require('fb-dotslash'); 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 CLANG_FORMAT = path.join(__dirname, 'clang-format'); +const OSS_CLANG_FORMAT_DOTSLASH = path.join(__dirname, 'clang-format'); const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const IGNORE_FILE = path.join(REPO_ROOT, '.clang-format-ignore'); const MAX_HEADER_BYTES = 4096; const MAX_FILES_PER_PROCESS = 30; +const SOURCE_GLOB = '**/*.{c,cc,cpp,cu,cuh,cxx,h,hh,hpp,hxx,m,mm,proto,tcc}'; + +function findClangFormat() { + if (process.env.CLANG_FORMAT != null && process.env.CLANG_FORMAT !== '') { + return {command: process.env.CLANG_FORMAT, prefixArguments: []}; + } + + try { + const metaClangFormat = require('./clang-format.fb').findMetaClangFormat(); + if (metaClangFormat != null) { + return metaClangFormat; + } + } catch (error) { + if ( + error == null || + error.code !== 'MODULE_NOT_FOUND' || + !String(error.message).includes("'./clang-format.fb'") + ) { + throw error; + } + } + + return { + command: + process.env.DOTSLASH != null && process.env.DOTSLASH !== '' + ? process.env.DOTSLASH + : require('fb-dotslash'), + prefixArguments: [OSS_CLANG_FORMAT_DOTSLASH], + }; +} + /** @param {string} file */ function isGenerated(file) { let fd; @@ -41,16 +72,33 @@ function isGenerated(file) { } function main() { + const arguments_ = process.argv.slice(2); + const check = arguments_.includes('--check'); + const clangFormat = findClangFormat(); + const positionalArguments = arguments_.filter( + argument => argument !== '--check', + ); + const ignore = fs + .readFileSync(IGNORE_FILE, 'utf8') + .split('\n') + .map(line => line.trim()) + .filter(line => line !== '' && !line.startsWith('#')); + const discoveredFiles = globSync(SOURCE_GLOB, {cwd: REPO_ROOT, ignore}); const files = - process.argv.length > 2 - ? process.argv.slice(2) - : globSync('*/**/*.{h,cpp,m,mm}', {cwd: REPO_ROOT}); + positionalArguments.length > 0 + ? positionalArguments.filter(file => discoveredFiles.includes(file)) + : discoveredFiles; const sourceFiles = files.filter(file => !isGenerated(file)); + let exitStatus = 0; for (let i = 0; i < sourceFiles.length; i += MAX_FILES_PER_PROCESS) { + const formatterArguments = [ + ...(check ? ['--dry-run', '--Werror'] : ['-i']), + ...sourceFiles.slice(i, i + MAX_FILES_PER_PROCESS), + ]; const result = spawnSync( - dotslash, - [CLANG_FORMAT, '-i', ...sourceFiles.slice(i, i + MAX_FILES_PER_PROCESS)], + clangFormat.command, + [...clangFormat.prefixArguments, ...formatterArguments], { cwd: REPO_ROOT, stdio: 'inherit', @@ -61,13 +109,13 @@ function main() { throw result.error; } if (result.signal != null) { - process.kill(process.pid, result.signal); - return; + throw new Error(`clang-format was terminated by ${result.signal}`); } if (result.status !== 0) { - process.exit(result.status ?? 1); + exitStatus = result.status ?? 1; } } + process.exitCode = exitStatus; } main(); diff --git a/scripts/format-kotlin.js b/scripts/format-kotlin.js new file mode 100644 index 000000000000..9879f9bcbf97 --- /dev/null +++ b/scripts/format-kotlin.js @@ -0,0 +1,81 @@ +/** + * 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 {findJava, warnMissingJava} = 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 KTFMT_JAR = require.resolve('ktfmt/lib/ktfmt.jar'); +const GENERATED_MARKER = Buffer.from('@' + 'generated'); +const MINIMUM_JAVA_VERSION = 17; +const MAX_FILES_PER_PROCESS = 100; +const MAX_HEADER_BYTES = 4096; +const IGNORE = [ + '**/build/**', + '**/com/facebook/yoga/**', + '**/hermes-engine/**', + '**/internal/featureflags/**', + '**/node_modules/**', + '**/systeminfo/ReactNativeVersion.kt', +]; + +function isGenerated(file) { + const fd = fs.openSync(path.resolve(REPO_ROOT, file), 'r'); + try { + 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); + } finally { + fs.closeSync(fd); + } +} + +function main() { + const check = process.argv[2] === '--check'; + const java = findJava(MINIMUM_JAVA_VERSION); + if (java == null) { + warnMissingJava('Kotlin'); + return; + } + const files = globSync('**/*.{kt,kts}', { + cwd: REPO_ROOT, + ignore: IGNORE, + }).filter(file => !isGenerated(file)); + for (let i = 0; i < files.length; i += MAX_FILES_PER_PROCESS) { + const result = spawnSync( + java, + [ + '-jar', + KTFMT_JAR, + '--do-not-remove-unused-imports', + ...(check ? ['--dry-run', '--set-exit-if-changed'] : []), + ...files.slice(i, i + MAX_FILES_PER_PROCESS), + ], + {cwd: REPO_ROOT, stdio: 'inherit'}, + ); + if (result.error != null) { + throw result.error; + } + if (result.signal != null) { + process.kill(process.pid, result.signal); + return; + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + } +} + +main(); diff --git a/scripts/format-utils.js b/scripts/format-utils.js new file mode 100644 index 000000000000..7b4dac220cb3 --- /dev/null +++ b/scripts/format-utils.js @@ -0,0 +1,88 @@ +/** + * 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 {spawnSync} = require('node:child_process'); +const path = require('node:path'); + +const REPO_ROOT = path.resolve(__dirname, '..'); + +let metaUtils = null; +try { + metaUtils = require('./format-utils.fb'); +} catch (error) { + if ( + error == null || + typeof error !== 'object' || + error.code !== 'MODULE_NOT_FOUND' || + !String(error.message).includes("'./format-utils.fb'") + ) { + throw error; + } +} + +const IS_META_CHECKOUT = metaUtils != null; + +function commandVersion(command, prefixArguments = []) { + const result = spawnSync(command, [...prefixArguments, '--version'], { + encoding: 'utf8', + env: {...process.env, PWD: REPO_ROOT}, + }); + return { + output: `${result.stdout ?? ''}\n${result.stderr ?? ''}`, + status: result.status, + }; +} + +function findMetaTool(...relativePath) { + return metaUtils?.findMetaTool(...relativePath) ?? null; +} + +function javaMajorVersion(command) { + const result = spawnSync(command, ['-version'], {encoding: 'utf8'}); + const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`; + const version = /version "(?:1\.)?(\d+)/.exec(output); + return result.status === 0 && version != null ? Number(version[1]) : null; +} + +function findJava(minimumVersion) { + if (process.env.JAVA != null && process.env.JAVA !== '') { + return javaMajorVersion(process.env.JAVA) >= minimumVersion + ? process.env.JAVA + : null; + } + + const candidates = []; + candidates.push(...(metaUtils?.findJavaCandidates() ?? [])); + candidates.push('java'); + + return ( + candidates.find(command => javaMajorVersion(command) >= minimumVersion) ?? + null + ); +} + +function warnMissingJava(language) { + const instructions = + metaUtils?.missingJavaInstructions() ?? + 'Please install a JDK of your choice with Java 17 or newer and make sure the `java` command is in your PATH, or set JAVA=/path/to/java.'; + console.warn( + `warning: Skipping ${language} formatting because Java 17 or newer was not found.\n${instructions}`, + ); +} + +module.exports = { + commandVersion, + findJava, + findMetaTool, + IS_META_CHECKOUT, + warnMissingJava, +}; diff --git a/yarn.lock b/yarn.lock index 5126518dbbe1..3327c4cc333c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6373,6 +6373,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +ktfmt@0.59.0: + version "0.59.0" + resolved "https://registry.yarnpkg.com/ktfmt/-/ktfmt-0.59.0.tgz#99f98b81dbdc7f1487dfbc9850eb17b3780cf6d5" + integrity sha512-lOEn/7y2Ez2/nxDTn5EwJv6BSugB8BtzY2Gn6GvyLIAjdUf3xgKzirIxD57t/vu5I6eybivmVtONI3WGXyZ3lw== + language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7"