41
\$\begingroup\$

To download the tool and for usage instructions, see the follow up to this question


Description

I have realized that there's a whole bunch of code I want to have reviewed, but copying file by file, selecting the code and pressing Ctrl + K is a slow process, and I want to remember which other parts to include in my question, so I decided to make a tool for it. I thought it could be useful, especially since most of my questions are structured in very similar ways already...

This code is for inputting a couple of files, and outputting a CR question stub, ready to be filled in with details. The code automatically formats the input files to match StackExchange formatting, with four spaces added in front of each line (No more Ctrl + K !!)

It is very likely that you will see more questions from me structured in the same way as this question is. And if you want to use it yourself, feel free to do so.

Related previously existing tools:

Code download

For your convenience, this code can be found on GitHub (Many thanks to @amon who taught me a lot more about how to use git)

Class Summary (10079 bytes in 342 lines in 4 files)

  • CountingStream: OutputStream that keeps track on the number of written bytes to it
  • ReviewPrepareFrame: JFrame for letting user select files that should be up for review
  • ReviewPreparer: The most important class, takes care of most of the work. Expects a List of files in the constructor and an OutputStream when called.
  • TextAreaOutputStream: OutputStream for outputting to a JTextArea.

Code

CountingStream: (approximately 679 bytes in 27 lines)

/**
 * An output stream that keeps track of how many bytes that has been written to it.
 */
public class CountingStream extends FilterOutputStream {
    private final AtomicInteger bytesWritten;

    public CountingStream(OutputStream out) {
        super(out);
        this.bytesWritten = new AtomicInteger();
    }

    @Override
    public void write(int b) throws IOException {
        bytesWritten.incrementAndGet();
        super.write(b);
    }
    public int getBytesWritten() {
        return bytesWritten.get();
    }
}

ReviewPrepareFrame: (approximately 3178 bytes in 109 lines)

public class ReviewPrepareFrame extends JFrame {

    private static final long   serialVersionUID    = 2050188992596669693L;
    private JPanel  contentPane;
    private final JTextArea result = new JTextArea();

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    new ReviewPrepareFrame().setVisible(true);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the frame.
     */
    public ReviewPrepareFrame() {
        setTitle("Prepare code for Code Review");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 450, 300);
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        setContentPane(contentPane);

        JPanel panel = new JPanel();
        contentPane.add(panel, BorderLayout.NORTH);

        final DefaultListModel<File> model = new DefaultListModel<>();
        final JList<File> list = new JList<File>();
        panel.add(list);
        list.setModel(model);

        JButton btnAddFiles = new JButton("Add files");
        btnAddFiles.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JFileChooser dialog = new JFileChooser();
                dialog.setMultiSelectionEnabled(true);
                if (dialog.showOpenDialog(ReviewPrepareFrame.this) == JFileChooser.APPROVE_OPTION) {
                    for (File file : dialog.getSelectedFiles()) {
                        model.addElement(file);
                    }
                }
            }
        });
        panel.add(btnAddFiles);

        JButton btnRemoveFiles = new JButton("Remove files");
        btnRemoveFiles.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                for (File file : new ArrayList<>(list.getSelectedValuesList())) {
                    model.removeElement(file);
                }
            }
        });
        panel.add(btnRemoveFiles);

        JButton performButton = new JButton("Create Question stub with code included");
        performButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent arg0) {
                result.setText("");
                ReviewPreparer preparer = new ReviewPreparer(filesToList(model));
                TextAreaOutputStream outputStream = new TextAreaOutputStream(result);
                preparer.createFormattedQuestion(outputStream);
            }
        });
        contentPane.add(performButton, BorderLayout.SOUTH);
        contentPane.add(result, BorderLayout.CENTER);
    }

    public List<File> filesToList(DefaultListModel<File> model) {
        List<File> files = new ArrayList<>();
        for (int i = 0; i < model.getSize(); i++) {
            files.add(model.get(i));
        }
        return files;
    }


}

ReviewPreparer: (approximately 5416 bytes in 165 lines)

public class ReviewPreparer {
    private final List<File> files;

    public ReviewPreparer(List<File> files) {
        this.files = new ArrayList<>();

        for (File file : files) {
            if (file.getName().lastIndexOf('.') == -1)
                continue;

            if (file.length() < 10)
                continue;

            this.files.add(file);
        }
    }

    public int createFormattedQuestion(OutputStream out) {
        CountingStream counter = new CountingStream(out);
        PrintStream ps = new PrintStream(counter);
        outputHeader(ps);
        outputFileNames(ps);
        outputFileContents(ps);
        outputDependencies(ps);
        outputFooter(ps);
        ps.print("Question Length: ");
        ps.println(counter.getBytesWritten());
        return counter.getBytesWritten();
    }

    private void outputFooter(PrintStream ps) {
        ps.println("#Usage / Test");
        ps.println();
        ps.println();
        ps.println("#Questions");
        ps.println();
        ps.println();
        ps.println();
    }

    private void outputDependencies(PrintStream ps) {
        List<String> dependencies = new ArrayList<>();
        for (File file : files) {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
                String line;
                while ((line = in.readLine()) != null) {
                    if (!line.startsWith("import ")) continue;
                    if (line.startsWith("import java.")) continue;
                    if (line.startsWith("import javax.")) continue;
                    String importStatement = line.substring("import ".length());
                    importStatement = importStatement.substring(0, importStatement.length() - 1); // cut the semicolon
                    dependencies.add(importStatement);
                }
            }
            catch (IOException e) {
                ps.println("Could not read " + file.getAbsolutePath());
                ps.println();
                // this will be handled by another function
            }

        }
        if (!dependencies.isEmpty()) {
            ps.println("#Dependencies");
            ps.println();
            for (String str : dependencies)
                ps.println("- " + str + ": ");
        }
        ps.println();
    }

    private int countLines(File file) throws IOException {
        return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8).size();
    }

    private void outputFileContents(PrintStream ps) {
        ps.println();
        for (File file : files) {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
                ps.printf("**%s:** (approximately %d bytes in %d lines)", className(file), file.length(), countLines(file));
                ps.println();
                ps.println();
                String line;
                int importStatementsFinished = 0;
                while ((line = in.readLine()) != null) {
                    // skip package and import declarations
                    if (line.startsWith("package ")) continue;
                    if (line.startsWith("import ")) {
                        importStatementsFinished = 1;
                        continue;
                    }
                    if (importStatementsFinished >= 0) importStatementsFinished = -1;
                    if (importStatementsFinished == -1 && line.trim().isEmpty()) continue;
                    importStatementsFinished = -2;
                    line = line.replaceAll("    ", "\t"); // replace four spaces with tabs, since that takes less space
                    ps.print("  "); // format as code for StackExchange, this needs to be spaces.
                    ps.println(line);
                }
            }
            catch (IOException e) {
                ps.print("> ");
                e.printStackTrace(ps);
            }
            ps.println();
        }
    }

    private void outputFileNames(PrintStream ps) {
        int totalLength = 0;
        int totalLines = 0;
        for (File file : files) {
            totalLength += file.length();
            try {
                totalLines += countLines(file);
            }
            catch (IOException e) {
                ps.println("Unable to determine line count for " + file.getAbsolutePath());
            }
        }
        ps.printf("###Class Summary (%d bytes in %d lines in %d files)", totalLength, totalLines, files.size());
        ps.println();
        ps.println();
        for (File file : files) {
            ps.println("- " + className(file) + ": ");
        }
    }

    private String className(File file) {
        String str = file.getName();
        return str.substring(0, str.lastIndexOf('.'));
    }

    private void outputHeader(PrintStream ps) {
        ps.println("#Description");
        ps.println();
        ps.println("- Add some [description for what the code does](http://meta.codereview.stackexchange.com/questions/1226/code-should-include-a-description-of-what-the-code-does)");
        ps.println("- Is this a follow-up question? Answer [What has changed, Which question was the previous one, and why you are looking for another review](http://meta.codereview.stackexchange.com/questions/1065/how-to-post-a-follow-up-question)");
        ps.println();
        ps.println("#Code download");
        ps.println();
        ps.println("For convenience, this code can be downloaded from [somewhere](http://github.com repository perhaps?)");
        ps.println();
    }

    public static void main(String[] args) throws IOException {
//      List<File> fileList = Arrays.asList(new File("C:/_zomisnet/_reviewtest").listFiles());
        List<File> fileList = Arrays.asList(new File("./src/net/zomis/reviewprep").listFiles());
        new ReviewPreparer(fileList).createFormattedQuestion(System.out);
    }

}

TextAreaOutputStream: (approximately 806 bytes in 41 lines)

public class TextAreaOutputStream extends OutputStream {

    private final JTextArea textArea;
    private final StringBuilder sb = new StringBuilder();

    public TextAreaOutputStream(final JTextArea textArea) {
        this.textArea = textArea;
    }

    @Override
    public void flush() {
    }

    @Override
    public void close() {
    }

    @Override
    public void write(int b) throws IOException {
        if (b == '\n') {
            final String text = sb.toString() + "\n";
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    textArea.append(text);
                }
            });
            sb.setLength(0);
            return;
        }

        sb.append((char) b);
    }
}

Usage / Test

Input: Run the JFrame, select files that should be reviewed, click the button.

Output: The stub to this question I am currently writing :)

Questions

Looking for general comments on my code. The code is Open-Source in case you want to continue the work I have started. I am currently only treating Java specially since that's what I code the most in, but I believe better support for other languages can be added later (I am not asking for how to do it, or for you to do it, I'm just saying that with some modifications, I believe it is possible).

\$\endgroup\$
16
\$\begingroup\$

I would encourage you to produce more explicit output, particularly with the filenames. If I wanted to reverse the process and scrape the code into files on my machine, using a Python script such as the following…

import json
from lxml import html
import re
import requests

FILENAME_HINT_XPATH = "../preceding-sibling::p[1]/strong/text()"

def code_for_post(site, post):
    r = requests.get('https://api.stackexchange.com/2.1/posts/{1}?site={0}&filter=withbody'.format(site, post))
    j = json.loads(r.text)
    body = j['items'][0]['body']
    tree = html.fromstring(body)

    code_elements = tree.xpath("//pre/code[%s]" % (FILENAME_HINT_XPATH))
    return dict((c.xpath(FILENAME_HINT_XPATH)[0], c.findtext(".")) for c in code_elements)

def write_files(code):
    extension = '.java'     # <-- Yuck, due to @Simon.
    for filename_hint, content in code.iteritems():
        filename = re.sub(r'[^A-Za-z0-9]', '', filename_hint) + extension
        with open(filename, 'w') as f:
            print >>f, content

write_files(code_for_post('codereview', 41198))

… then I would have to make assumptions about the filename extension.


The invocation method could be improved. Instead of hard-coding a particular directory to look in for the source files, I would suggest…

  • If files are explicitly passed to the program as command-line arguments, use those files.
  • If a directory is specified, then use all files contained therein, excluding files with significant non-ASCII content.
  • If no command-line arguments are used, then operate on the current directory.

It would be nice to be able to say java ReviewPreparer *.java |pbcopy.

\$\endgroup\$
  • 1
    \$\begingroup\$ You mean that I should print out that it's .java files? \$\endgroup\$ – Simon Forsberg Feb 8 '14 at 9:20
  • 1
    \$\begingroup\$ Yes, that would let me remove the # Yuck line. \$\endgroup\$ – 200_success Feb 8 '14 at 9:25
13
\$\begingroup\$

It's much more important to convey the code faithfully than to save a few bytes of output by replacing spaces with tabs:

line = line.replaceAll("    ", "\t"); // replace four spaces with tabs, since that takes less space

In fact, since you used your program to produce this question itself, I suspect that your tool is responsible for introducing the bug in the immediately following line:

ps.print("  "); // format as code for StackExchange, this needs to be spaces.

Markdown requires four leading spaces to create a code block, not two.

\$\endgroup\$
  • \$\begingroup\$ Ouch! Indeed correct. I'm also considering if perhaps replacing tabs with spaces would be better instead? (The only problem is that the code I want to have review tends to be quite big :) ) \$\endgroup\$ – Simon Forsberg Feb 8 '14 at 9:58
  • 3
    \$\begingroup\$ In my opinion, no good can come out of mucking with the content any more than necessary to convey the code faithfully. \$\endgroup\$ – 200_success Feb 8 '14 at 10:13
  • \$\begingroup\$ You have a good point there. It's probably best to simply add four spaces in the beginning of each line and just leave it at that. \$\endgroup\$ – Simon Forsberg Feb 8 '14 at 10:19
  • 1
    \$\begingroup\$ @SimonAndréForsberg +1 to 200_success for bug find, and another +1 for 'don't muck around ...'. (capped at +1 though). Also, you should only muck with whitespace at the beginning/end of lines, unless it is Python, in which case, the only safe modification is adding 4 spaces at front. \$\endgroup\$ – rolfl Feb 8 '14 at 12:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.