说明

这是创建CodeReview问题的工具的后续问题。

已更改的内容包括:


已删除用一个选项卡替换四个空格,代码本身中的所有选项卡和所有空格现在都保持原样。
在输出中添加了文件扩展名。
我觉得数字和行的顺序切换代码行比字节数更有趣。
支持命令行参数直接处理目录或一堆文件,并支持通配符。如果使用目录或通配符,则未通过ASCII内容检查的文件将被跳过。如果您指定的文件中包含大量非ASCII内容,则无论如何都会对其进行处理。

由于我添加的内容最多,我要求再次审核,请参见以下问题。 br />
类摘要(4个文件中的413行,总计12134字节)


CountingStream.java:OutputStream跟踪要写入的字节数它
ReviewPrepareFrame.java:JFrame,用于让用户选择应进行审查的文件。
ReviewPreparer.java:最重要的类,负责大部分工作。
TextAreaOutputStream.java:用于输出到JTextArea的OutputStream。

代码

代码也可以在GitHub上找到

CountingStream.java:(27行,679字节)

/**
 * 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行,3255字节)

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行,7394字节)

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行,806字节)

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);
    }
}


用法/测试

您现在可以通过从GitHub下载jar文件直接使用该工具并使用以下选项之一运行它:



java -jar ReviewPrepare.jar运行Swing表单,使您可以使用GUI选择文件。

java -jar ReviewPrepare.jar .在当前工作目录中运行该程序并输出到stdout。

java -jar ReviewPrepare.jar . > out.txt在Windows中运行该程序当前工作目录并输出到文件out.txt(我用这个来创建此问题)

java -jar ReviewPrepare.jar C:/some/path/*.java > out.txt在指定目录中运行程序,匹配所有* .java文件并输出到文件out.txt


问题

我目前主要关心的是实现命令行参数的方式,是否可以更轻松地完成? (最好不要使用外部库,因为我希望我的代码尽可能独立,尽管也欢迎这样做的库建议)我错过了任何常见的文件模式参数吗?

我我还有点担心它的可扩展性,现在感觉根本无法扩展。如果有人想为Python / C#/ C ++ / etc方式添加自定义功能该怎么办。文件是格式化的吗?然后以我做过的方式对“扫描导入文件”进行硬编码并不十分理想。

当然也欢迎进行一般评论。

评论

StackExchange上是否有模板,如何构造问题/答案?

#1 楼

一般

现在您的帖子如此整洁,答案也将变得更加整洁。

GUI Bugs

当我运行GUI,它不允许我从文件浏览器中选择目录。它也从“文档”目录开始,最好做以下两件事之一:


从当前目录开始
从上一个使用的目录开始(使用java.util.pefs.Preferences吗?)

您应该添加:

            JFileChooser dialog = new JFileChooser();
            dialog.setCurrentDirectory(".");
            dialog.setMultiSelectionEnabled(true);
            dialog.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);


然后,您还应该支持从选择器。这将使GUI中的行为与命令行更加匹配。

第二个问题是JTextArea显示。它应该具有滚动条,以便您可以在复制/粘贴结果之前检查结果。在查看这些更改时,我发现您正在事件分配线程上执行所有文件IO ...这是一种不好的做法...。

我必须执行以下操作:

// add a scrollPane....
private final JScrollPane scrollPane = new JScrollPane(result);

......

// Inside the constructor:

    final Runnable viewupdater = new Runnable() {
        public void run() {
            result.setText("");
            ReviewPreparer preparer = new ReviewPreparer(filesToList(model));
            TextAreaOutputStream outputStream = new TextAreaOutputStream(result);
            preparer.createFormattedQuestion(outputStream);
            outputStream.flush();
            result.setCaretPosition(0);
        }
    };

    JButton performButton = new JButton("Create Question stub with code included");
    performButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent arg0) {
            Thread worker = new Thread(viewupdater);
            worker.setDaemon(true);
            worker.start();
        }
    });

    scrollPane.setAutoscrolls(true);
    scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
    contentPane.add(scrollPane, BorderLayout.CENTER);
    contentPane.add(performButton, BorderLayout.SOUTH);


在进行此更改时,我注意到您没有对TextAreaOutputStream实例进行任何最佳实践关闭,并且我查看了TextAreaOutputStream代码,并且,这不是正确的解决方案。它正在为每个文件中的每一行创建一个新线程。。。应该删除整个类,并替换为:

    final Runnable viewupdater = new Runnable() {
        public void run() {
            ReviewPreparer preparer = new ReviewPreparer(filesToList(model));
            try (final StringWriter sw = new StringWriter()) {
                preparer.createFormattedQuestion(sw);
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        result.setText(sw.toString());
                        result.setCaretPosition(0);
                    }
                });
            }
        }
    };


请注意,上面的内容如何更改为使用Writer而不是OutputStream .....使用OutputStream用于文本数据的模型是损坏的....用于文本的读取器和写入器,以及用于二进制的流。

这是进入非GUI代码的好方法....

Core引擎

TextareaOutputStream使我意识到,除了埋在ReviewPreparer中的某些部分之外,您所有的方法都是基于流的。

PrintStream代码都应该全部用StringBuilder替换。..无论如何,您都限于CR帖子的大小,并且正在将数据累积到TextArea中。

这也是CountingOutputStream的有趣的赛格威。也不需要....您不是用它来计算文件大小,而是实际的帖子长度。应该用字符而不是字节来度量...。因此,它是一个损坏的类。摆脱掉它。

所以,也摆脱掉PrintStream。 PrintStream是一个同步类,并且比StringBuilder慢得多。将数据追加到StringBuilder还意味着您可以从StringBuilder获得字符长度,而不是从CountingOutputStream获得字节长度。

最后一个观察结果.. outputFileContents(PrintStream ps)方法内部这样做:

        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) {


由于一些原因,它被打破了。...

首先,您不应该使用FileInputStream,但是FileReader。

其次,您具有支持方法countLines(File)

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


此方法再次完全读取文件... ..为什么不将上面的所有大代码替换为:

        try {
            List<String> filelines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
            sb.append(String.format("**%s:** (%d lines, %d bytes)", file.getName(),
                    filelines.size(), file.length()));

            sb.append("\n\n");
            int importStatementsFinished = 0;
            for (String line : filelines) {
                // skip package and import declarations


这样省去了两次读取每个文件的麻烦...

走了,这已经足够了。