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