/** * Copyright (C) 2005 - 2014 Eric Van Dewoestine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.eclim.plugin.jdt.command.correct; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import org.eclim.Services; import org.eclim.annotation.Command; import org.eclim.command.CommandLine; import org.eclim.command.Options; import org.eclim.plugin.core.command.AbstractCommand; import org.eclim.plugin.core.command.refactoring.ResourceChangeListener; import org.eclim.plugin.jdt.command.include.ImportUtils; import org.eclim.plugin.jdt.util.JavaUtils; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; import org.eclipse.jdt.core.formatter.CodeFormatter; import org.eclipse.jdt.core.refactoring.CompilationUnitChange; import org.eclipse.jdt.internal.corext.util.JavaModelUtil; import org.eclipse.jdt.internal.ui.text.correction.AssistContext; import org.eclipse.jdt.internal.ui.text.correction.CorrectionMessages; import org.eclipse.jdt.internal.ui.text.correction.ReorgCorrectionsSubProcessor.ClasspathFixCorrectionProposal; import org.eclipse.jdt.internal.ui.text.correction.proposals.NewCUUsingWizardProposal; import org.eclipse.jdt.internal.ui.text.correction.proposals.NewVariableCorrectionProposal; import org.eclipse.jdt.ui.text.java.CompletionProposalComparator; import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; import org.eclipse.jdt.ui.text.java.IProblemLocation; import org.eclipse.jdt.ui.text.java.IQuickFixProcessor; import org.eclipse.jdt.ui.text.java.correction.ASTRewriteCorrectionProposal; import org.eclipse.jdt.ui.text.java.correction.ChangeCorrectionProposal; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.PerformChangeOperation; import org.eclipse.ltk.core.refactoring.RefactoringCore; import org.eclipse.ltk.core.refactoring.RefactoringStatus; import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry; import org.eclipse.ltk.core.refactoring.TextFileChange; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.TextEdit; /** * Handles requests for code correction. * * @author Eric Van Dewoestine */ @Command( name = "java_correct", options = "REQUIRED p project ARG," + "REQUIRED f file ARG," + "REQUIRED l line ARG," + "REQUIRED o offset ARG," + "OPTIONAL e encoding ARG," + "OPTIONAL a apply ARG" ) public class CodeCorrectCommand extends AbstractCommand { private static HashSet<Class<? extends IJavaCompletionProposal>> IGNORE_BY_TYPE = new HashSet<Class<? extends IJavaCompletionProposal>>(); static { IGNORE_BY_TYPE.add(NewCUUsingWizardProposal.class); IGNORE_BY_TYPE.add(ClasspathFixCorrectionProposal.class); } private static HashSet<String> IGNORE_BY_INFO = new HashSet<String>(); static { IGNORE_BY_INFO.add(CorrectionMessages .LocalCorrectionsSubProcessor_InferGenericTypeArguments_description); IGNORE_BY_INFO.add(CorrectionMessages .GetterSetterCorrectionSubProcessor_additional_info); } @Override public Object execute(CommandLine commandLine) throws Exception { String file = commandLine.getValue(Options.FILE_OPTION); String projectName = commandLine.getValue(Options.PROJECT_OPTION); int line = commandLine.getIntValue(Options.LINE_OPTION); int offset = getOffset(commandLine); // JavaUtils refreshes the file when getting it. ICompilationUnit src = JavaUtils.getCompilationUnit(projectName, file); IProblem problem = getProblem(src, line, offset); if(problem == null){ String message = Services.getMessage("error.not.found", file, line); if(commandLine.hasOption(Options.APPLY_OPTION)){ throw new RuntimeException(message); } return message; } List<ChangeCorrectionProposal> proposals = getProposals(src, problem); if(commandLine.hasOption(Options.APPLY_OPTION)){ ChangeCorrectionProposal proposal = proposals.get(commandLine.getIntValue(Options.APPLY_OPTION)); return apply(src, proposal); } HashMap<String,Object> result = new HashMap<String,Object>(); result.put("message", problem.getMessage()); result.put("offset", problem.getSourceStart()); result.put("corrections", getCorrections(proposals)); return result; } /** * Gets the requested problem. * * @param src The source file. * @param line The line number of the error. * @param offset The offset of the error. * @return The IProblem or null if none found. */ private IProblem getProblem(ICompilationUnit src, int line, int offset) throws Exception { IProblem[] problems = JavaUtils.getProblems(src); ArrayList<IProblem> errors = new ArrayList<IProblem>(); for(int ii = 0; ii < problems.length; ii++){ if(problems[ii].getSourceLineNumber() == line){ errors.add(problems[ii]); } } IProblem problem = null; if(errors.size() == 0){ return null; }else if(errors.size() > 0){ for (IProblem p : errors){ if(offset < p.getSourceStart() && offset <= p.getSourceEnd()){ problem = p; } } } if(problem == null){ problem = (IProblem)errors.get(0); } return problem; } /** * Gets possible corrections for the supplied problem. * * @param src The src file. * @param problem The problem. * @return Returns a List of ChangeCorrectionProposal. */ private List<ChangeCorrectionProposal> getProposals( ICompilationUnit src, IProblem problem) throws Exception { IProject project = src.getJavaProject().getProject(); ArrayList<ChangeCorrectionProposal> results = new ArrayList<ChangeCorrectionProposal>(); int length = (problem.getSourceEnd() + 1) - problem.getSourceStart(); AssistContext context = new AssistContext( src, problem.getSourceStart(), length); IProblemLocation[] locations = new IProblemLocation[]{new ProblemLocation(problem)}; IQuickFixProcessor[] processors = JavaUtils.getQuickFixProcessors(src); for(int ii = 0; ii < processors.length; ii++){ if (processors[ii] != null && processors[ii].hasCorrections(src, problem.getID())) { // we currently don't support the ajdt processor since it relies on // PlatformUI.getWorkbench().getActiveWorkbenchWindow() which is null // here. if (processors[ii].getClass().getName().equals( "org.eclipse.ajdt.internal.ui.editor.quickfix.QuickFixProcessor")) { continue; } IJavaCompletionProposal[] proposals = processors[ii].getCorrections(context, locations); if(proposals != null){ for (IJavaCompletionProposal proposal : proposals){ if (!(proposal instanceof ChangeCorrectionProposal)){ continue; } // skip proposal requiring gui dialogs, etc. if (IGNORE_BY_TYPE.contains(proposal.getClass()) || IGNORE_BY_INFO.contains(proposal.getAdditionalProposalInfo())) { continue; } // honor the user's import exclusions if (proposal instanceof ASTRewriteCorrectionProposal){ ImportRewrite rewrite = ((ASTRewriteCorrectionProposal)proposal).getImportRewrite(); if (rewrite != null && ( rewrite.getAddedImports().length != 0 || rewrite.getAddedStaticImports().length != 0)) { boolean exclude = true; for(String fqn : rewrite.getAddedImports()){ if (!ImportUtils.isImportExcluded(project, fqn)){ exclude = false; break; } } for(String fqn : rewrite.getAddedStaticImports()){ if (!ImportUtils.isImportExcluded(project, fqn)){ exclude = false; break; } } if (exclude){ continue; } } } results.add((ChangeCorrectionProposal)proposal); } } } } Collections.sort(results, new CompletionProposalComparator()); return results; } /** * Converts the supplied list of IJavaCompletionProposal(s) to array of * CodeCorrectResult. * * @param proposals List of IJavaCompletionProposal. * @return Array of CodeCorrectResult. */ private List<CodeCorrectResult> getCorrections( List<ChangeCorrectionProposal> proposals) throws Exception { ArrayList<CodeCorrectResult> corrections = new ArrayList<CodeCorrectResult>(); int index = 0; for(ChangeCorrectionProposal proposal : proposals){ String preview = proposal.getAdditionalProposalInfo(); if (preview != null){ preview = preview .replaceAll("<br>", "\n") .replaceAll("<.+?>", "") .replaceAll("<", "<") .replaceAll(">", ">"); } corrections.add(new CodeCorrectResult( index, proposal.getDisplayString(), preview)); index++; } return corrections; } /** * Apply the supplied correction proposal. * * @param src The ICompilationUnit where the change is initiated from. * @param proposal The ChangeCorrectionProposal to apply. * @return A list of changed files or a map containing a list of errors. */ private Object apply(ICompilationUnit src, ChangeCorrectionProposal proposal) throws Exception { Change change = null; try { NullProgressMonitor monitor = new NullProgressMonitor(); change = proposal.getChange(); change.initializeValidationData(monitor); RefactoringStatus status = change.isValid(monitor); if (status.hasFatalError()){ List<String> errors = new ArrayList<String>(); for (RefactoringStatusEntry entry : status.getEntries()){ String message = entry.getMessage(); if (!errors.contains(message) && !message.startsWith("Found potential matches")) { errors.add(message); } } HashMap<String,List<String>> result = new HashMap<String,List<String>>(); result.put("errors", errors); return result; } ResourceChangeListener rcl = new ResourceChangeListener(); IWorkspace workspace = ResourcesPlugin.getWorkspace(); workspace.addResourceChangeListener(rcl); try{ TextEdit[] edits = new TextEdit[0]; if (change instanceof TextFileChange){ TextFileChange fileChange = (TextFileChange)change; fileChange.setSaveMode(TextFileChange.FORCE_SAVE); TextEdit edit = fileChange.getEdit(); if (edit instanceof MultiTextEdit){ edits = ((MultiTextEdit)edit).getChildren(); }else{ edits = new TextEdit[]{edit}; } } PerformChangeOperation changeOperation = new PerformChangeOperation(change); changeOperation.setUndoManager( RefactoringCore.getUndoManager(), proposal.getName()); changeOperation.run(monitor); if (edits.length > 0 && change instanceof CompilationUnitChange && src.equals(((CompilationUnitChange)change).getCompilationUnit())) { for (TextEdit edit : edits){ int offset = edit.getOffset(); int length = edit.getLength(); // for "Create field" and "Create local" the edit length includes // additional existing code that we don't want to reformat. if (proposal instanceof NewVariableCorrectionProposal) { String text = src.getBuffer() .getText(edit.getOffset(), edit.getLength()); int index = text.indexOf('\n'); if (index != -1){ length = index; // include the white space up to the next bit of code while(length < text.length()){ char next = text.charAt(length); if (next == '\t' || next == '\n' || next == ' '){ length += 1; continue; } break; } } } JavaUtils.format( src, CodeFormatter.K_COMPILATION_UNIT, offset, length); } } // if the proposal change touched the imports, then run our import // grouping edit after it. if (proposal instanceof ASTRewriteCorrectionProposal){ ASTRewriteCorrectionProposal astProposal = (ASTRewriteCorrectionProposal)proposal; if (astProposal.getImportRewrite() != null){ TextEdit groupingEdit = ImportUtils.importGroupingEdit(src, getPreferences()); if (groupingEdit != null){ JavaModelUtil.applyEdit(src, groupingEdit, true, null); if (src.isWorkingCopy()) { src.commitWorkingCopy(false, null); } } } } return rcl.getChangedFiles(); }finally{ workspace.removeResourceChangeListener(rcl); } }finally{ if (change != null) { change.dispose(); } } } }