Skip to content

Commit 43afe28

Browse files
committed
feat: implement google-java-format ༼ つ ◕_◕ ༽つ
1 parent e79e51d commit 43afe28

File tree

8 files changed

+1067
-1
lines changed

8 files changed

+1067
-1
lines changed

README.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,72 @@
1-
# google-java-format
1+
# google-java-format
2+
3+
Node repackaging of Google's native [google-java-format](https://github.com/google/google-java-format) tool
4+
5+
Very useful in libraries that wrap native java code in javascript
6+
7+
## Usage Example
8+
9+
1. (optional) `yarn add @invertase/google-java-format` (or `npm -i @invertase/google-java-format` if you prefer)
10+
1. `npx google-java-format <path to java file>`
11+
12+
Please note this wrapper has made the design decision to pass all arguments directly to the underlying tool.
13+
This ensures that we will never have compatibility issues with versions.
14+
15+
The only exception is globbing multiple files, which we must process to not cause errors with command line arguments being too long
16+
17+
### Globbing files
18+
19+
`npx google-java-format -i --glob=folder/**/*.java`
20+
21+
This will run find the matching files and run `google-java-format` on chunks of 30 files at a time, then show the total
22+
formatted files at the end.
23+
24+
See [node-glob](https://github.com/isaacs/node-glob) for globbing semantics.
25+
26+
### Dry-run
27+
28+
To just check files use the `-n` (or `--dry-run`) parameter:
29+
30+
`npx google-java-format -n --glob=folder/**/*.java`
31+
32+
## Integrations
33+
34+
### Pre-commit
35+
36+
May be wrapped in git pre-commit if desired, there are [examples on line](https://github.com/justinludwig/gjfpc-hook) demonstrating this
37+
38+
If you would like to contribute a pre-commit hook, perhaps one that is Husky-compatible, that would be great! Open a PR with the hook and remove this comment 🙏😊
39+
40+
### Github Actions
41+
42+
May be used in GitHub Actions workflows to verify PRs meet formatting standards,
43+
as done in [Invertase](https://invertase.io) [react-native-firebase](https://github.com/Invertase/react-native-firebase/)
44+
45+
## Inspiration
46+
47+
The wonderful [clang-format](https://github.com/angular/clang-format) from the Angular team is so useful, we wanted the same package for java formatting.
48+
49+
That package already solves so many of the little problems that occur with different platforms, and globbing etc - it's fantastic.
50+
51+
Go to [their repo](https://github.com/angular/clang-format), integrate it for objective-c code, and give them a ⭐, they have earned it. Thanks for the code + inspiration Angular community!
52+
53+
## Contributing
54+
55+
- [Issues](https://github.com/invertase/google-java-format/issues)
56+
- [PRs welcome!](https://github.com/invertase/google-java-format/pulls)
57+
- [Code of Conduct](https://github.com/invertase/meta/blob/master/CODE_OF_CONDUCT.md)
58+
59+
## License
60+
61+
- See [LICENSE](/LICENSE)
62+
63+
---
64+
65+
<p align="center">
66+
<a href="https://invertase.io/?utm_source=readme&utm_medium=footer&utm_campaign=react-native-firebase">
67+
<img width="75px" src="https://static.invertase.io/assets/invertase/invertase-rounded-avatar.png">
68+
</a>
69+
<p align="center">
70+
Built and maintained by <a href="https://invertase.io/?utm_source=readme&utm_medium=footer&utm_campaign=react-native-firebase">Invertase</a>.
71+
</p>
72+
</p>

index.js

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
const fs = require("fs");
5+
const os = require("os");
6+
const path = require("path");
7+
const resolve = require("resolve").sync;
8+
const spawn = require("child_process").spawn;
9+
const glob = require("glob");
10+
const async = require("async");
11+
12+
const VERSION = require("./package.json").version;
13+
const LOCATION = __dirname;
14+
15+
// Glob pattern option name
16+
const GLOB_OPTION = "--glob=";
17+
18+
function errorFromExitCode(exitCode) {
19+
return new Error(`google-java-format exited with exit code ${exitCode}.`);
20+
}
21+
22+
/**
23+
* Starts a child process running the native google-java-format binary.
24+
*
25+
* @param file a Vinyl virtual file reference
26+
* @param enc the encoding to use for reading stdout
27+
* @param style valid argument to google-java-format's '-style' flag
28+
* @param done callback invoked when the child process terminates
29+
* @returns {stream.Readable} the formatted code as a Readable stream
30+
*/
31+
function googleJavaFormat(file, enc, style, done) {
32+
let args = [`-style=${style}`, file.path];
33+
let result = spawnGoogleJavaFormat(args, done, [
34+
"ignore",
35+
"pipe",
36+
process.stderr,
37+
]);
38+
if (result) {
39+
// must be ChildProcess
40+
result.stdout.setEncoding(enc);
41+
return result.stdout;
42+
} else {
43+
// We shouldn't be able to reach this line, because it's not possible to
44+
// set the --glob arg in this function.
45+
throw new Error("Can't get output stream when --glob flag is set");
46+
}
47+
}
48+
49+
/**
50+
* Spawn the google-java-format binary with given arguments.
51+
*/
52+
function spawnGoogleJavaFormat(args, done, stdio) {
53+
// WARNING: This function's interface should stay stable across versions for the cross-version
54+
// loading below to work.
55+
let nativeBinary;
56+
57+
try {
58+
nativeBinary = getNativeBinary();
59+
} catch (e) {
60+
setImmediate(() => done(e));
61+
return;
62+
}
63+
64+
if (args.find((a) => a === "-version" || a === "--version")) {
65+
// Print our version.
66+
// This makes it impossible to format files called '-version' or '--version'. That's a feature.
67+
// minimist & Co don't support single dash args, which we need to match binary google-java-format.
68+
console.log(`google-java-format NPM version ${VERSION} at ${LOCATION}`);
69+
args = ["--version"];
70+
}
71+
72+
// Add the library in, with java 16 compat
73+
args = [
74+
"--add-exports",
75+
"jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
76+
"--add-exports",
77+
"jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
78+
"--add-exports",
79+
"jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
80+
"--add-exports",
81+
"jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
82+
"--add-exports",
83+
"jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
84+
"-jar",
85+
`${LOCATION}/lib/google-java-format-1.11.0-all-deps.jar`,
86+
].concat(args);
87+
88+
// extract glob, if present
89+
const filesGlob = getGlobArg(args);
90+
91+
if (filesGlob) {
92+
// remove glob from arg list
93+
args = args.filter((arg) => arg.indexOf(GLOB_OPTION) === -1);
94+
95+
glob(filesGlob, function (err, files) {
96+
if (err) {
97+
done(err);
98+
return;
99+
}
100+
101+
// split file array into chunks of 30
102+
let i,
103+
j,
104+
chunks = [],
105+
chunkSize = 30;
106+
107+
for (i = 0, j = files.length; i < j; i += chunkSize) {
108+
chunks.push(files.slice(i, i + chunkSize));
109+
}
110+
111+
// launch a new process for each chunk
112+
async.series(
113+
chunks.map(function (chunk) {
114+
return function (callback) {
115+
const googlejavaFormatProcess = spawn(
116+
nativeBinary,
117+
args.concat(chunk),
118+
{ stdio: stdio }
119+
);
120+
googlejavaFormatProcess.on("close", function (exit) {
121+
if (exit !== 0) callback(errorFromExitCode(exit));
122+
else callback();
123+
});
124+
};
125+
}),
126+
function (err) {
127+
if (err) {
128+
done(err);
129+
return;
130+
}
131+
console.log("\n");
132+
console.log(
133+
`ran google-java-format on ${files.length} ${
134+
files.length === 1 ? "file" : "files"
135+
}`
136+
);
137+
done();
138+
}
139+
);
140+
});
141+
} else {
142+
const googlejavaFormatProcess = spawn(nativeBinary, args, { stdio: stdio });
143+
googlejavaFormatProcess.on("close", function (exit) {
144+
if (exit) {
145+
done(errorFromExitCode(exit));
146+
} else {
147+
done();
148+
}
149+
});
150+
return googlejavaFormatProcess;
151+
}
152+
}
153+
154+
function main() {
155+
// Find google-java-format in node_modules of the project of the .js file, or cwd.
156+
const nonDashArgs = process.argv.filter(
157+
(arg, idx) => idx > 1 && arg[0] != "-"
158+
);
159+
160+
// Using the last file makes it less likely to collide with google-java-format's argument parsing.
161+
const lastFileArg = nonDashArgs[nonDashArgs.length - 1];
162+
const basedir = lastFileArg
163+
? path.dirname(lastFileArg) // relative to the last .js file given.
164+
: process.cwd(); // or relative to the cwd()
165+
let resolvedGoogleJavaFormat;
166+
let googleJavaFormatLocation;
167+
try {
168+
googleJavaFormatLocation = resolve("google-java-format", { basedir });
169+
resolvedGoogleJavaFormat = require(googleJavaFormatLocation);
170+
} catch (e) {
171+
// Ignore and use the google-java-format that came with this package.
172+
}
173+
let actualSpawnFn;
174+
if (!resolvedGoogleJavaFormat) {
175+
actualSpawnFn = spawnGoogleJavaFormat;
176+
} else if (resolvedGoogleJavaFormat.spawnGoogleJavaFormat) {
177+
actualSpawnFn = resolvedGoogleJavaFormat.spawnGoogleJavaFormat;
178+
} else {
179+
throw new Error(
180+
`Incompatible google-java-format loaded from ${googleJavaFormatLocation}`
181+
);
182+
}
183+
// Run google-java-format.
184+
try {
185+
// Pass all arguments to google-java-format, including e.g. -version etc.
186+
actualSpawnFn(
187+
process.argv.slice(2),
188+
function (e) {
189+
if (e instanceof Error) {
190+
console.error(e);
191+
process.exit(1);
192+
} else {
193+
process.exit(e);
194+
}
195+
},
196+
"inherit"
197+
);
198+
} catch (e) {
199+
process.stdout.write(e.message);
200+
process.exit(1);
201+
}
202+
}
203+
204+
/**
205+
* @returns the native `java` binary for the current platform
206+
* @throws when the `java` executable can not be found
207+
*/
208+
function getNativeBinary() {
209+
let nativeBinary = "java";
210+
const platform = os.platform();
211+
const arch = os.arch();
212+
if (platform === "win32") {
213+
nativeBinary = "java.exe";
214+
}
215+
return nativeBinary;
216+
}
217+
218+
/**
219+
* Filters the arguments to return the value of the `--glob=` option.
220+
*
221+
* @returns The value of the glob option or null if not found
222+
*/
223+
function getGlobArg(args) {
224+
const found = args.find((a) => a.startsWith(GLOB_OPTION));
225+
return found ? found.substring(GLOB_OPTION.length) : null;
226+
}
227+
228+
module.exports = googleJavaFormat;
229+
module.exports.version = VERSION;
230+
module.exports.location = LOCATION;
231+
module.exports.spawnGoogleJavaFormat = spawnGoogleJavaFormat;
232+
module.exports.getNativeBinary = getNativeBinary;
233+
234+
if (require.main === module) main();
3.23 MB
Binary file not shown.

package.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "google-java-format",
3+
"version": "1.0.0",
4+
"description": "node wrapper around google-java-format",
5+
"author": "Invertase <[email protected]> (http://invertase.io)",
6+
"repository": {
7+
"type": "git",
8+
"url": "[email protected]:invertase/google-java-format.git"
9+
},
10+
"main": "index.js",
11+
"bin": {
12+
"google-java-format": "index.js"
13+
},
14+
"scripts": {
15+
"test": "bash ./test.sh",
16+
"shipit": "nc --no-tests"
17+
},
18+
"contributors": [
19+
"Mike Hardy <[email protected]>"
20+
],
21+
"keywords": [
22+
"java-format",
23+
"react-native",
24+
"google-java-format",
25+
"java",
26+
"format"
27+
],
28+
"license": "Apache-2.0",
29+
"dependencies": {
30+
"async": "^3.2.1",
31+
"glob": "^7.0.0",
32+
"resolve": "^1.1.6"
33+
},
34+
"devDependencies": {
35+
"nc": "^1.0.3"
36+
}
37+
}

test.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/sh
2+
set -e
3+
4+
EXPECTED='(x): number => 123\n'
5+
ACTUAL=$(echo '( \n x ) : number => 123 ' | /usr/bin/env node index.js -assume-filename a.js)
6+
if [[ "$ACTUAL" = "$EXPECTED" ]]; then
7+
echo "[FAIL] Expected $EXPECTED, got $ACTUAL" >&2
8+
exit 1
9+
fi
10+
11+
# Make sure we can run on relative an absolute paths (set -e checks for errors).
12+
/usr/bin/env node index.js index.js >/dev/null
13+
echo "[PASS] relative path" >&2
14+
/usr/bin/env node index.js "$PWD"/index.js >/dev/null
15+
echo "[PASS] absolute path" >&2
16+
17+
FULL_SCRIPT_PATH="$PWD/index.js"
18+
EXPECTED_VERSION_STRING=" at $PWD" # somewhere in there
19+
EXPECTED_FAIL_FILE="testproject/android/src/java/io/invertase/PoorlyFormattedTest.java"
20+
EXPECTED_GLOB_STRING="ran google-java-format on 2 files" # somewhere in there
21+
22+
(
23+
cd "$PWD"/testproject
24+
yarn > /dev/null # Should give us a local clang-format, version doesn't really matter.
25+
VERSION=$(/usr/bin/env node "$FULL_SCRIPT_PATH" -version)
26+
if [[ $VERSION != *"$EXPECTED_VERSION_STRING"* ]]; then
27+
echo "[FAIL] Expected string containing $EXPECTED_VERSION_STRING, got $VERSION" >&2
28+
exit 1
29+
fi
30+
echo "[PASS] no file argument uses working directory" >&2
31+
)
32+
33+
VERSION=$(/usr/bin/env node "$FULL_SCRIPT_PATH" -version)
34+
if [[ $VERSION != *"$EXPECTED_VERSION_STRING"* ]]; then
35+
echo "[FAIL] Expected string containing $EXPECTED_VERSION_STRING, got $VERSION" >&2
36+
exit 1
37+
fi
38+
echo "[PASS] file argument anchors resolution" >&2
39+
40+
GLOB=$(/usr/bin/env node "$FULL_SCRIPT_PATH" -n --glob=testproject/**/*.java)
41+
if [[ $GLOB != *"$EXPECTED_GLOB_STRING" ]]; then
42+
echo "[FAIL] Expected string ending in $EXPECTED_GLOB_STRING, got $GLOB" >&2
43+
exit 1
44+
fi
45+
if [[ "$GLOB" != *"$EXPECTED_FAIL_FILE"* ]]; then
46+
echo "[FAIL] Expected string containing $EXPECTED_FAIL_FILE, got $GLOB" >&2
47+
exit 1
48+
fi
49+
echo "[PASS] glob argument resolution" >&2

0 commit comments

Comments
 (0)