Skip to main content
replaced http://codereview.stackexchange.com/ with https://codereview.stackexchange.com/
Source Link

This is a follow-up question to Tool for creating CodeReview questions.

This is a follow-up question to Tool for creating CodeReview questions.

deleted 30 characters in body; edited title
Source Link
Simon Forsberg
  • 59.7k
  • 9
  • 160
  • 312

Follow-up to tool for posting Code ReviewCodeReview questions

You can now use the tool directly by downloading the jar-file from GitHubthe jar-file from GitHub and running it with one of the following options:

Follow-up to tool for posting Code Review questions

You can now use the tool directly by downloading the jar-file from GitHub and running it with one of the following options:

Follow-up to tool for posting CodeReview questions

You can now use the tool directly by downloading the jar-file from GitHub and running it with one of the following options:

Tweeted twitter.com/#!/StackCodeReview/status/432145231912648704
Source Link
Simon Forsberg
  • 59.7k
  • 9
  • 160
  • 312

Follow-up to tool for posting Code Review questions

#Description

This is a follow-up question to Tool for creating CodeReview questions.

Things that has changed include:

  • Removed replacing four spaces with one tab, all tabs and all spaces in the code itself is now left as-is.
  • Added file extensions to the output.
  • Switched order of lines and bytes as I feel that the number of lines of code is more interesting than the number of bytes.
  • Support for command-line parameters to directly process a directory or a bunch of files, with the support for wildcards. If a directory or wildcard is used, files that don't pass an ASCII-content check gets skipped. If you have specified a file that has a lot of non-ASCII content it is processed anyway.

I am asking for another review because of the things that I have added mostly, see the questions below.

###Class Summary (413 lines in 4 files, making a total of 12134 bytes)

  • CountingStream.java: OutputStream that keeps track on the number of written bytes to it
  • ReviewPrepareFrame.java: JFrame for letting user select files that should be up for review
  • ReviewPreparer.java: 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.java: OutputStream for outputting to a JTextArea.

#Code

The code can also be found on GitHub

CountingStream.java: (27 lines, 679 bytes)

/**
 * 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.java: (112 lines, 3255 bytes)

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) {
        if (args.length == 0) {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    try {
                        new ReviewPrepareFrame().setVisible(true);
                    }
                    catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        else ReviewPreparer.main(args);
    }

    /**
     * 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.java: (233 lines, 7394 bytes)

public class ReviewPreparer {
    public static double detectAsciiness(File input) throws IOException {
        if (input.length() == 0)
            return 0;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(input)))) {
            int read;
            long asciis = 0;
            char[] cbuf = new char[1024];
            while ((read = reader.read(cbuf)) != -1) {
                for (int i = 0; i < read; i++) {
                    char c = cbuf[i];
                    if (c <= 0x7f)
                        asciis++;
                }
            }
            return asciis / (double) input.length();
        }
    }

    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();
                // more detailed handling of this exception 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("#Code");
        ps.println();
        ps.println("This code can also be downloaded from [somewhere](http://github.com repository perhaps?)");
        ps.println();
        for (File file : files) {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
                int lines = -1;
                try {
                    lines = countLines(file);
                }
                catch (IOException e) { 
                }
                ps.printf("**%s:** (%d lines, %d bytes)", file.getName(), lines, file.length());
                
                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()) // skip empty lines directly after import statements 
                        continue;
                    importStatementsFinished = -2;
                    ps.print("    "); // format as code for StackExchange, this needs to be four spaces.
                    ps.println(line);
                }
            }
            catch (IOException e) {
                ps.print("> Unable to read " + file + ": "); // use a block-quote for exceptions
                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 lines in %d files, making a total of %d bytes)", totalLines, files.size(), totalLength);
        ps.println();
        ps.println();
        for (File file : files) {
            ps.println("- " + file.getName() + ": ");
        }
        ps.println();
    }
    
    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();
    }
    
    public static boolean isAsciiFile(File file) {
        try {
            return detectAsciiness(file) >= 0.99;
        }
        catch (IOException e) {
            return true; // if an error occoured, we want it to be added to a list and the error shown in the output
        }
    }
    
    public static void main(String[] args) {
        List<File> files = new ArrayList<>();
        if (args.length == 0)
            files.addAll(fileList("."));
        for (String arg : args) {
            files.addAll(fileList(arg));
        }
        new ReviewPreparer(files).createFormattedQuestion(System.out);
    }
    
    public static List<File> fileList(String pattern) {
        List<File> files = new ArrayList<>();
        
        File file = new File(pattern);
        if (file.exists()) {
            if (file.isDirectory()) {
                for (File f : file.listFiles())
                    if (!f.isDirectory() && isAsciiFile(f))
                        files.add(f);
            }
            else files.add(file);
        }
        else {
            // extract path
            int lastSeparator = pattern.lastIndexOf('\\');
            lastSeparator = Math.max(lastSeparator, pattern.lastIndexOf('/'));
            String path = lastSeparator < 0 ? "." : pattern.substring(0, lastSeparator);
            file = new File(path); 
            
            // path has been extracted, check if path exists
            if (file.exists()) {
                // create a regex for searching for files, such as *.java, Test*.java
                String regex = lastSeparator < 0 ? pattern : pattern.substring(lastSeparator + 1);
                regex = regex.replaceAll("\\.", "\\.").replaceAll("\\?", ".?").replaceAll("\\*", ".*");
                for (File f : file.listFiles()) {
                    // loop through directory, skip directories and filenames that don't match the pattern
                    if (!f.isDirectory() && f.getName().matches(regex) && isAsciiFile(f)) {
                        files.add(f);
                    }
                }
            }
            else System.out.println("Unable to find path " + file);
        }
        return files;
    }
}

TextAreaOutputStream.java: (41 lines, 806 bytes)

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

You can now use the tool directly by downloading the jar-file from GitHub and running it with one of the following options:

  • java -jar ReviewPrepare.jar runs the Swing form to let you choose files using a GUI.
  • java -jar ReviewPrepare.jar . runs the program in the current working directory and outputting to stdout.
  • java -jar ReviewPrepare.jar . > out.txt runs the program in the current working directory and outputting to the file out.txt (I used this to create this question)
  • java -jar ReviewPrepare.jar C:/some/path/*.java > out.txt runs the program in the specified directory, matching all *.java files and outputting to the file out.txt

#Questions

My main concern currently is with the way I implemented the command line parameters, could it be done easier? (Preferably without using an external library as I would like my code to be independent if possible, although library suggestions for this is also welcome) Is there any common file-pattern-argument that I missed?

I'm also a bit concerned with the extensibility of this, right now it feels not extensible at all. What if someone would want to add custom features for the way Python/C#/C++/etc. files are formatted? Then hard-coding the "scan for imports" in the way I have done it doesn't feel quite optimal.

General reviews are also of course welcome.