3cd042efac428a37a5ae72eced425f9050610659
[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.VirtualFile;
37 import com.intellij.openapi.vfs.encoding.ChooseFileEncodingAction;
38 import com.intellij.openapi.vfs.encoding.EncodingManager;
39 import com.intellij.psi.PsiFile;
40 import com.intellij.util.ArrayUtil;
41 import com.intellij.util.SmartList;
42 import org.jetbrains.annotations.Nls;
43 import org.jetbrains.annotations.NonNls;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import java.io.IOException;
48 import java.nio.ByteBuffer;
49 import java.nio.CharBuffer;
50 import java.nio.charset.Charset;
51 import java.util.Arrays;
52 import java.util.List;
53
54 public class LossyEncodingInspection extends LocalInspectionTool {
55   private static final LocalQuickFix CHANGE_ENCODING_FIX = new ChangeEncodingFix();
56   private static final LocalQuickFix RELOAD_ENCODING_FIX = new ReloadInAnotherEncodingFix();
57
58   @Nls
59   @NotNull
60   public String getGroupDisplayName() {
61     return InspectionsBundle.message("group.names.internationalization.issues");
62   }
63
64   @Nls
65   @NotNull
66   public String getDisplayName() {
67     return InspectionsBundle.message("lossy.encoding");
68   }
69
70   @NonNls
71   @NotNull
72   public String getShortName() {
73     return "LossyEncoding";
74   }
75
76   @Nullable
77   public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) {
78     if (InjectedLanguageManager.getInstance(file.getProject()).isInjectedFragment(file)) return null;
79     if (ArrayUtil.find(file.getPsiRoots(), file) != 0) return null;
80     VirtualFile virtualFile = file.getVirtualFile();
81     if (virtualFile == null) return null;
82     String text = file.getText();
83     Charset charset = LoadTextUtil.extractCharsetFromFileContent(file.getProject(), virtualFile, text);
84
85     // no sense in checking transparently decoded file: all characters there are already safely encoded
86     if (charset instanceof Native2AsciiCharset) return null;
87
88     List<ProblemDescriptor> descriptors = new SmartList<ProblemDescriptor>();
89     checkIfCharactersWillBeLostAfterSave(file, manager, isOnTheFly, text, charset, descriptors);
90
91     checkForFileLoadedInWrongEncoding(file, manager, isOnTheFly, virtualFile, charset, descriptors);
92
93     return descriptors.toArray(new ProblemDescriptor[descriptors.size()]);
94   }
95
96   private static void checkForFileLoadedInWrongEncoding(PsiFile file,
97                                                         InspectionManager manager,
98                                                         boolean isOnTheFly,
99                                                         VirtualFile virtualFile,
100                                                         Charset charset, List<ProblemDescriptor> descriptors) {
101     if (!FileDocumentManager.getInstance().isFileModified(virtualFile) // when file is modified, it's too late to reload it
102         && ChooseFileEncodingAction.isEnabled(virtualFile) // can't reload in another encoding, no point trying
103       ) {
104       // check if file was loaded in correct encoding
105       byte[] bytes;
106       try {
107         bytes = virtualFile.contentsToByteArray();
108       }
109       catch (IOException e) {
110         return;
111       }
112       String separator = FileDocumentManager.getInstance().getLineSeparator(virtualFile, file.getProject());
113       String toSave = StringUtil.convertLineSeparators(file.getText(), separator);
114       byte[] bytesToSave = toSave.getBytes(charset);
115       if (!Arrays.equals(bytesToSave, bytes)) {
116         descriptors.add(manager.createProblemDescriptor(file, "File was loaded in a wrong encoding: '"+charset+"'",
117                                                         RELOAD_ENCODING_FIX, ProblemHighlightType.GENERIC_ERROR, isOnTheFly));
118       }
119     }
120   }
121
122   private static void checkIfCharactersWillBeLostAfterSave(PsiFile file,
123                                                            InspectionManager manager,
124                                                            boolean isOnTheFly,
125                                                            String text,
126                                                            Charset charset, List<ProblemDescriptor> descriptors) {
127     int errorCount = 0;
128     int start = -1;
129     for (int i = 0; i <= text.length(); i++) {
130       char c = i == text.length() ? 0 : text.charAt(i);
131       if (i == text.length() || isRepresentable(c, charset)) {
132         if (start != -1) {
133           TextRange range = new TextRange(start, i);
134           String message = InspectionsBundle.message("unsupported.character.for.the.charset", charset);
135           ProblemDescriptor descriptor =
136             manager.createProblemDescriptor(file, range, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly, CHANGE_ENCODING_FIX);
137           descriptors.add(descriptor);
138           start = -1;
139           //do not report too many errors
140           if (errorCount++ > 200) break;
141         }
142       }
143       else if (start == -1) {
144         start = i;
145       }
146     }
147   }
148
149   private static boolean isRepresentable(final char c, final Charset charset) {
150     String str = Character.toString(c);
151     ByteBuffer out = charset.encode(str);
152     CharBuffer buffer = charset.decode(out);
153     return str.equals(buffer.toString());
154   }
155
156   private static class ReloadInAnotherEncodingFix extends ChangeEncodingFix {
157     @NotNull
158     @Override
159     public String getName() {
160       return "Reload in another encoding";
161     }
162
163     @Override
164     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
165       if (FileDocumentManager.getInstance().isFileModified(descriptor.getPsiElement().getContainingFile().getVirtualFile())) return;
166       super.applyFix(project, descriptor);
167     }
168   }
169
170   private static class ChangeEncodingFix implements LocalQuickFix {
171     @NotNull
172     @Override
173     public String getName() {
174       return "Change file encoding";
175     }
176
177     @NotNull
178     @Override
179     public String getFamilyName() {
180       return getName();
181     }
182
183     @Override
184     public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
185       PsiFile psiFile = descriptor.getPsiElement().getContainingFile();
186       VirtualFile virtualFile = psiFile.getVirtualFile();
187       ChooseFileEncodingAction action = new ChooseFileEncodingAction(virtualFile) {
188         @Override
189         protected void chosen(VirtualFile virtualFile, Charset charset) {
190           if (virtualFile != null) {
191             EncodingManager.getInstance().setEncoding(virtualFile, charset);
192           }
193         }
194       };
195       DefaultActionGroup group = action.createGroup(false);
196       DataContext dataContext = DataManager.getInstance().getDataContext();
197       JBPopupFactory.getInstance().createActionGroupPopup(null, group, dataContext, false, false, false, null, 30, null).showInBestPositionFor(dataContext);
198     }
199   }
200 }