do not inspect nonphysical files
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInspection / LossyEncodingInspection.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /*
18  * Created by IntelliJ IDEA.
19  * User: cdr
20  * Date: Aug 6, 2007
21  * Time: 3:09:55 PM
22  */
23 package com.intellij.codeInspection;
24
25 import com.intellij.ide.DataManager;
26 import com.intellij.lang.injection.InjectedLanguageManager;
27 import com.intellij.lang.properties.charset.Native2AsciiCharset;
28 import com.intellij.openapi.actionSystem.DataContext;
29 import com.intellij.openapi.actionSystem.DefaultActionGroup;
30 import com.intellij.openapi.fileEditor.FileDocumentManager;
31 import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.ui.popup.JBPopupFactory;
34 import com.intellij.openapi.util.TextRange;
35 import com.intellij.openapi.util.text.StringUtil;
36 import com.intellij.openapi.vfs.LocalFileSystem;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.openapi.vfs.encoding.ChooseFileEncodingAction;
39 import com.intellij.openapi.vfs.encoding.EncodingManager;
40 import com.intellij.psi.PsiFile;
41 import com.intellij.util.ArrayUtil;
42 import com.intellij.util.SmartList;
43 import org.jetbrains.annotations.Nls;
44 import org.jetbrains.annotations.NonNls;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47
48 import java.io.IOException;
49 import java.nio.ByteBuffer;
50 import java.nio.CharBuffer;
51 import java.nio.charset.Charset;
52 import java.util.Arrays;
53 import java.util.List;
54
55 public class LossyEncodingInspection extends LocalInspectionTool {
56   private static final LocalQuickFix CHANGE_ENCODING_FIX = new ChangeEncodingFix();
57   private static final LocalQuickFix RELOAD_ENCODING_FIX = new ReloadInAnotherEncodingFix();
58
59   @Nls
60   @NotNull
61   public String getGroupDisplayName() {
62     return InspectionsBundle.message("group.names.internationalization.issues");
63   }
64
65   @Nls
66   @NotNull
67   public String getDisplayName() {
68     return InspectionsBundle.message("lossy.encoding");
69   }
70
71   @NonNls
72   @NotNull
73   public String getShortName() {
74     return "LossyEncoding";
75   }
76
77   @Nullable
78   public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) {
79     if (InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) return null;
80     if (ArrayUtil.find(file.getPsiRoots(), file) != 0) return null;
81     if (!file.isPhysical()) return null;
82     VirtualFile virtualFile = file.getVirtualFile();
83     if (virtualFile == null || virtualFile.getFileSystem() != LocalFileSystem.getInstance()) return null;
84     String text = file.getText();
85     Charset charset = LoadTextUtil.extractCharsetFromFileContent(file.getProject(), virtualFile, text);
86
87     // no sense in checking transparently decoded file: all characters there are already safely encoded
88     if (charset instanceof Native2AsciiCharset) return null;
89
90     List<ProblemDescriptor> descriptors = new SmartList<ProblemDescriptor>();
91     checkIfCharactersWillBeLostAfterSave(file, manager, isOnTheFly, text, charset, descriptors);
92     checkFileLoadedInWrongEncoding(file, manager, isOnTheFly, virtualFile, charset, descriptors);
93
94     return descriptors.toArray(new ProblemDescriptor[descriptors.size()]);
95   }
96
97   private static void checkFileLoadedInWrongEncoding(PsiFile file,
98                                                      InspectionManager manager,
99                                                      boolean isOnTheFly,
100                                                      VirtualFile virtualFile,
101                                                      Charset charset, List<ProblemDescriptor> descriptors) {
102     if (!FileDocumentManager.getInstance().isFileModified(virtualFile) // when file is modified, it's too late to reload it
103         && ChooseFileEncodingAction.isEnabled(virtualFile) // can't reload in another encoding, no point trying
104       ) {
105       // check if file was loaded in correct encoding
106       byte[] bytes;
107       try {
108         bytes = virtualFile.contentsToByteArray();
109       }
110       catch (IOException e) {
111         return;
112       }
113       String separator = FileDocumentManager.getInstance().getLineSeparator(virtualFile, file.getProject());
114       String toSave = StringUtil.convertLineSeparators(file.getText(), separator);
115       byte[] bytesToSave = toSave.getBytes(charset);
116       if (!Arrays.equals(bytesToSave, bytes)) {
117         descriptors.add(manager.createProblemDescriptor(file, "File was loaded in a wrong encoding: '"+charset+"'",
118                                                         RELOAD_ENCODING_FIX, ProblemHighlightType.GENERIC_ERROR, isOnTheFly));
119       }
120     }
121   }
122
123   private static void checkIfCharactersWillBeLostAfterSave(PsiFile file,
124                                                            InspectionManager manager,
125                                                            boolean isOnTheFly,
126                                                            String text,
127                                                            Charset charset, List<ProblemDescriptor> descriptors) {
128     int errorCount = 0;
129     int start = -1;
130     for (int i = 0; i <= text.length(); i++) {
131       char c = i == text.length() ? 0 : text.charAt(i);
132       if (i == text.length() || isRepresentable(c, charset)) {
133         if (start != -1) {
134           TextRange range = new TextRange(start, i);
135           String message = InspectionsBundle.message("unsupported.character.for.the.charset", charset);
136           ProblemDescriptor descriptor =
137             manager.createProblemDescriptor(file, range, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly, CHANGE_ENCODING_FIX);
138           descriptors.add(descriptor);
139           start = -1;
140           //do not report too many errors
141           if (errorCount++ > 200) break;
142         }
143       }
144       else if (start == -1) {
145         start = i;
146       }
147     }
148   }
149
150   private static boolean isRepresentable(final char c, final Charset charset) {
151     String str = Character.toString(c);
152     ByteBuffer out = charset.encode(str);
153     CharBuffer buffer = charset.decode(out);
154     return str.equals(buffer.toString());
155   }
156
157   private static class ReloadInAnotherEncodingFix extends ChangeEncodingFix {
158     @NotNull
159     @Override
160     public String getName() {
161       return "Reload in another encoding";
162     }
163
164     @Override
165     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
166       if (FileDocumentManager.getInstance().isFileModified(descriptor.getPsiElement().getContainingFile().getVirtualFile())) return;
167       super.applyFix(project, descriptor);
168     }
169   }
170
171   private static class ChangeEncodingFix implements LocalQuickFix {
172     @NotNull
173     @Override
174     public String getName() {
175       return "Change file encoding";
176     }
177
178     @NotNull
179     @Override
180     public String getFamilyName() {
181       return getName();
182     }
183
184     @Override
185     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
186       PsiFile psiFile = descriptor.getPsiElement().getContainingFile();
187       VirtualFile virtualFile = psiFile.getVirtualFile();
188       ChooseFileEncodingAction action = new ChooseFileEncodingAction(virtualFile) {
189         @Override
190         protected void chosen(VirtualFile virtualFile, Charset charset) {
191           if (virtualFile != null) {
192             EncodingManager.getInstance().setEncoding(virtualFile, charset);
193           }
194         }
195       };
196       DefaultActionGroup group = action.createGroup(false);
197       DataContext dataContext = DataManager.getInstance().getDataContext();
198       JBPopupFactory.getInstance().createActionGroupPopup(null, group, dataContext, false, false, false, null, 30, null).showInBestPositionFor(dataContext);
199     }
200   }
201 }