1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @format
8 */
9
10'use strict';
11
12const {exec} = require('shelljs');
13const https = require('https');
14const fs = require('fs');
15const path = require('path');
16const os = require('os');
17const yargs = require('yargs');
18
19const googleJavaFormatUrl =
20  'https://github.com/google/google-java-format/releases/download/google-java-format-1.7/google-java-format-1.7-all-deps.jar';
21const googleJavaFormatPath = path.join(
22  os.tmpdir(),
23  'google-java-format-all-deps.jar',
24);
25const javaFilesCommand = 'find ./ReactAndroid -name "*.java"';
26
27function _download(url, downloadPath, resolve, reject, redirectCount) {
28  https.get(url, response => {
29    switch (response.statusCode) {
30      case 302: //Permanent Redirect
31        if (redirectCount === 0) {
32          throw new Error(
33            `Unhandled response code (HTTP${response.statusCode}) while retrieving google-java-format binary from ${url}`,
34          );
35        }
36
37        _download(
38          response.headers.location,
39          downloadPath,
40          resolve,
41          reject,
42          redirectCount - 1,
43        );
44        break;
45      case 200: //OK
46        const file = fs.createWriteStream(downloadPath);
47
48        response.pipe(file);
49        file.on('finish', () => file.close(() => resolve()));
50        break;
51      default:
52        reject(
53          `Unhandled response code (HTTP${response.statusCode}) while retrieving google-java-format binary from ${url}`,
54        );
55    }
56  });
57}
58
59function download(url, downloadPath) {
60  return new Promise((resolve, reject) => {
61    _download(url, downloadPath, resolve, reject, 1);
62  });
63}
64
65function filesWithLintingIssues() {
66  const proc = exec(
67    `java -jar ${googleJavaFormatPath} --dry-run $(${javaFilesCommand})`,
68    {silent: true},
69  );
70
71  if (proc.code !== 0) {
72    throw new Error(proc.stderr);
73  }
74
75  return proc.stdout.split('\n').filter(x => x);
76}
77
78function unifiedDiff(file) {
79  const lintedProc = exec(
80    `java -jar ${googleJavaFormatPath} --set-exit-if-changed ${file}`,
81    {silent: true},
82  );
83
84  //Exit code 1 indicates lint violations, which is what we're expecting
85  if (lintedProc.code !== 1) {
86    throw new Error(lintedProc.stderr);
87  }
88
89  const diffProc = lintedProc.exec(`diff -U 0 ${file} -`, {silent: true});
90
91  //Exit code 0 if inputs are the same, 1 if different, 2 if trouble.
92  if (diffProc.code !== 0 && diffProc.code !== 1) {
93    throw new Error(diffProc.stderr);
94  }
95
96  return {
97    file,
98    diff: diffProc.stdout,
99  };
100}
101
102function extractRangeInformation(range) {
103  //eg;
104  //  @@ -54 +54,2 @@
105  //  @@ -1,3 +1,9 @@
106
107  const regex = /^@@ [-+](\d+,?\d+) [-+](\d+,?\d+) @@$/;
108  const match = regex.exec(range);
109
110  if (match) {
111    const original = match[1].split(',');
112    const updated = match[2].split(',');
113
114    return {
115      original: {
116        line: parseInt(original[0], 10),
117        lineCount: parseInt(original[1], 10) || 1,
118      },
119      updated: {
120        line: parseInt(updated[0], 10),
121        lineCount: parseInt(updated[1], 10) || 1,
122      },
123    };
124  }
125}
126
127function parseChanges(file, diff) {
128  let group = null;
129  const groups = [];
130
131  diff.split('\n').forEach(line => {
132    const range = extractRangeInformation(line);
133
134    if (range) {
135      group = {
136        range,
137        description: [line],
138      };
139      groups.push(group);
140    } else if (group) {
141      group.description.push(line);
142    }
143  });
144
145  return groups.map(x => ({
146    file,
147    line: x.range.original.line,
148    lineCount: x.range.original.lineCount,
149    description: x.description.join('\n'),
150  }));
151}
152
153async function main() {
154  const {argv} = yargs
155    .scriptName('lint-java')
156    .usage('Usage: $0 [options]')
157    .command(
158      '$0',
159      'Downloads the google-java-format package and reformats Java source code to comply with Google Java Style.\n\nSee https://github.com/google/google-java-format',
160    )
161    .option('check', {
162      type: 'boolean',
163      description:
164        'Outputs a list of files with lint violations.\nExit code is set to 1 if there are violations, otherwise 0.\nDoes not reformat lint issues.',
165    })
166    .option('diff', {
167      type: 'boolean',
168      description:
169        'Outputs a diff of the lint fix changes in json format.\nDoes not reformat lint issues.',
170    });
171
172  await download(googleJavaFormatUrl, googleJavaFormatPath);
173
174  if (argv.check) {
175    const files = filesWithLintingIssues();
176
177    files.forEach(x => console.log(x));
178
179    process.exit(files.length === 0 ? 0 : 1);
180
181    return;
182  }
183
184  if (argv.diff) {
185    const suggestions = filesWithLintingIssues()
186      .map(unifiedDiff)
187      .filter(x => x)
188      .map(x => parseChanges(x.file, x.diff))
189      .reduce((accumulator, current) => accumulator.concat(current), []);
190
191    console.log(JSON.stringify(suggestions));
192
193    return;
194  }
195
196  const proc = exec(
197    `java -jar ${googleJavaFormatPath} --set-exit-if-changed --replace $(${javaFilesCommand})`,
198  );
199
200  process.exit(proc.code);
201}
202
203(async () => {
204  await main();
205})();
206