• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Camelcade / Perl5-IDEA / #525521543

22 May 2025 09:13AM UTC coverage: 82.34% (+0.1%) from 82.228%
#525521543

push

github

hurricup
Improved localization handling

Still imperfect, but good enough

0 of 2 new or added lines in 1 file covered. (0.0%)

113 existing lines in 7 files now uncovered.

30912 of 37542 relevant lines covered (82.34%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.27
/plugin/core/src/main/java/com/perl5/lang/perl/xsubs/PerlXSubsState.java
1
/*
2
 * Copyright 2015-2025 Alexandr Evstigneev
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
package com.perl5.lang.perl.xsubs;
18

19
import com.intellij.execution.ExecutionException;
20
import com.intellij.execution.process.ProcessOutput;
21
import com.intellij.notification.Notification;
22
import com.intellij.notification.NotificationType;
23
import com.intellij.notification.Notifications;
24
import com.intellij.openapi.actionSystem.AnAction;
25
import com.intellij.openapi.actionSystem.AnActionEvent;
26
import com.intellij.openapi.application.Application;
27
import com.intellij.openapi.application.ApplicationManager;
28
import com.intellij.openapi.application.ReadAction;
29
import com.intellij.openapi.application.WriteAction;
30
import com.intellij.openapi.components.PersistentStateComponent;
31
import com.intellij.openapi.components.State;
32
import com.intellij.openapi.components.Storage;
33
import com.intellij.openapi.diagnostic.Logger;
34
import com.intellij.openapi.progress.ProcessCanceledException;
35
import com.intellij.openapi.progress.ProgressIndicator;
36
import com.intellij.openapi.progress.Task;
37
import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
38
import com.intellij.openapi.progress.util.ReadTask;
39
import com.intellij.openapi.project.Project;
40
import com.intellij.openapi.project.ProjectUtil;
41
import com.intellij.openapi.projectRoots.Sdk;
42
import com.intellij.openapi.roots.ProjectRootManager;
43
import com.intellij.openapi.ui.Messages;
44
import com.intellij.openapi.util.NlsContexts.NotificationContent;
45
import com.intellij.openapi.util.NlsContexts.NotificationTitle;
46
import com.intellij.openapi.util.text.StringUtil;
47
import com.intellij.openapi.vfs.VfsUtilCore;
48
import com.intellij.openapi.vfs.VirtualFile;
49
import com.intellij.psi.search.FilenameIndex;
50
import com.intellij.psi.search.GlobalSearchScope;
51
import com.intellij.psi.search.GlobalSearchScopesCore;
52
import com.intellij.testFramework.LightVirtualFile;
53
import com.intellij.util.FileContentUtilCore;
54
import com.intellij.util.Function;
55
import com.intellij.util.containers.ContainerUtil;
56
import com.intellij.util.xmlb.XmlSerializerUtil;
57
import com.intellij.util.xmlb.annotations.Transient;
58
import com.perl5.PerlBundle;
59
import com.perl5.lang.perl.idea.PerlPathMacros;
60
import com.perl5.lang.perl.idea.actions.PerlActionBase;
61
import com.perl5.lang.perl.idea.execution.PerlCommandLine;
62
import com.perl5.lang.perl.idea.project.PerlProjectManager;
63
import com.perl5.lang.perl.idea.sdk.host.PerlHostData;
64
import com.perl5.lang.perl.idea.sdk.host.os.PerlOsHandler;
65
import com.perl5.lang.perl.util.PerlPluginUtil;
66
import com.perl5.lang.perl.util.PerlRunUtil;
67
import com.perl5.lang.perl.util.PerlUtil;
68
import org.jetbrains.annotations.NotNull;
69
import org.jetbrains.annotations.Nullable;
70
import org.jetbrains.annotations.VisibleForTesting;
71

72
import java.io.IOException;
73
import java.io.OutputStream;
74
import java.nio.charset.StandardCharsets;
75
import java.util.*;
76

77
@State(
78
  name = "Perl5XSubsState",
79
  storages = @Storage(PerlPathMacros.PERL5_PROJECT_SETTINGS_FILE)
80
)
81
public class PerlXSubsState implements PersistentStateComponent<PerlXSubsState> {
82
  private static final Logger LOG = Logger.getInstance(PerlXSubsState.class);
1✔
83
  @Transient
84
  public static final String DEPARSED_FILE_NAME = "_Deparsed_XSubs.pm";
85
  public boolean isActual = true;
1✔
86
  public Map<String, Long> myFilesMap = Collections.emptyMap();
1✔
87
  @Transient
1✔
88
  private Task.Backgroundable myParserTask = null;
89
  @Transient
90
  private final @Nullable Project myProject;
91

92
  @SuppressWarnings("unused")
93
  public PerlXSubsState() {
1✔
94
    myProject = null;
1✔
95
  }
1✔
96

97
  @SuppressWarnings("unused")
98
  public PerlXSubsState(@NotNull Project project) {
1✔
99
    myProject = project;
1✔
100
  }
1✔
101

102
  @Override
103
  public @Nullable PerlXSubsState getState() {
104
    return this;
×
105
  }
106

107
  private @NotNull Project getProject() {
108
    return Objects.requireNonNull(myProject);
1✔
109
  }
110

111
  @Override
112
  public void loadState(@NotNull PerlXSubsState state) {
113
    XmlSerializerUtil.copyBean(state, this);
1✔
114
  }
1✔
115

116
  private Set<VirtualFile> getAllXSFiles(@NotNull Project project) {
117
    PerlProjectManager perlProjectManager = PerlProjectManager.getInstance(project);
1✔
118
    List<VirtualFile> classesRoots = perlProjectManager.getAllLibraryRoots();
1✔
119
    if (classesRoots.isEmpty()) {
1✔
120
      return Collections.emptySet();
×
121
    }
122
    Sdk projectSdk = perlProjectManager.getProjectSdk();
1✔
123
    if (projectSdk == null) {
1✔
124
      return Collections.emptySet();
×
125
    }
126
    PerlOsHandler osHandler = PerlOsHandler.notNullFrom(projectSdk);
1✔
127

128
    GlobalSearchScope classRootsScope =
1✔
129
      GlobalSearchScopesCore.directoriesScope(getProject(), true, classesRoots.toArray(VirtualFile.EMPTY_ARRAY));
1✔
130

131
    Set<VirtualFile> result = new HashSet<>();
1✔
132
    for (VirtualFile virtualFile : FilenameIndex.getAllFilesByExt(project, osHandler.getXSBinaryExtension(), classRootsScope)) {
1✔
133
      if (virtualFile.isValid() && !virtualFile.isDirectory() && !(virtualFile instanceof LightVirtualFile)) {
1✔
134
        String path = virtualFile.getCanonicalPath();
1✔
135
        if (path != null && StringUtil.contains(path, "/auto/")) {
1✔
136
          result.add(virtualFile);
1✔
137
        }
138
      }
139
    }
1✔
140
    return result;
1✔
141
  }
142

143
  public void rescanFiles(@Nullable Runnable callback) {
144
    //noinspection deprecation
145
    ProgressIndicatorUtils.scheduleWithWriteActionPriority(new ReadTask() {
1✔
146
      @Override
147
      public void computeInReadAction(@NotNull ProgressIndicator indicator) throws ProcessCanceledException {
148
        if (!PerlProjectManager.isPerlEnabled(myProject)) {
1✔
149
          return;
1✔
150
        }
151
        int filesCounter = 0;
1✔
152
        indicator.setIndeterminate(false);
1✔
153
        indicator.setText(PerlBundle.message("perl.scanning.xs.changes"));
1✔
154
        if (isActual) {
1✔
155
          Set<VirtualFile> allXSFiles = getAllXSFiles(myProject);
1✔
156
          for (VirtualFile virtualFile : allXSFiles) {
1✔
157
            if (indicator.isCanceled()) {
1✔
UNCOV
158
              return;
×
159
            }
160

161
            if (!virtualFile.isValid()) {
1✔
UNCOV
162
              continue;
×
163
            }
164

165
            indicator.setFraction((double)filesCounter / allXSFiles.size());
1✔
166

167
            if (!isFileUpToDate(virtualFile)) {
1✔
168
              isActual = false;
1✔
169
              break;
1✔
170
            }
171
            else {
172
              filesCounter++;
1✔
173
            }
174
          }
1✔
175
        }
176

177
        isActual = isActual && (filesCounter == 0 || getDeparsedSubsFile() != null);
1✔
178

179
        if (!isActual) {
1✔
180
          showNotification(
1✔
181
            PerlBundle.message("perl.deparsing.change.detected.title"),
1✔
182
            PerlBundle.message("perl.deparsing.change.detected.message"),
1✔
183
            NotificationType.INFORMATION,
184
            notification -> Collections.singletonList(new PerlActionBase(PerlBundle.message("perl.deparsing.action")) {
1✔
185
              @Override
186
              public void actionPerformed(@NotNull AnActionEvent e) {
187
                notification.expire();
×
188
                reparseXSubs();
×
UNCOV
189
              }
×
190
            })
191
          );
192
        }
193
        if (callback != null) {
1✔
194
          callback.run();
1✔
195
        }
196
      }
1✔
197

198
      @Override
199
      public void onCanceled(@NotNull ProgressIndicator indicator) {
200
        rescanFiles(null);
1✔
201
      }
1✔
202
    });
203
  }
1✔
204

205
  @VisibleForTesting
206
  public @Nullable VirtualFile getDeparsedSubsFile() {
207
    for (VirtualFile possibleLocation : getPossibleFileLocations()) {
1✔
208
      var deparsedFile = possibleLocation.findFileByRelativePath(DEPARSED_FILE_NAME);
1✔
209
      if (deparsedFile != null && deparsedFile.isValid()) {
1✔
210
        return deparsedFile;
1✔
211
      }
212
    }
1✔
213

214
    return null;
1✔
215
  }
216

217
  private @NotNull Iterable<VirtualFile> getPossibleFileLocations() {
218
    var contentRoots = PerlUtil.mutableList(ProjectRootManager.getInstance(getProject()).getContentRoots());
1✔
219
    contentRoots.add(ProjectUtil.guessProjectDir(getProject()));
1✔
220
    return ContainerUtil.filter(contentRoots, it -> it != null && it.isValid() && it.exists() && it.isDirectory());
1✔
221
  }
222

223
  private @NotNull VirtualFile getOrCreateDeparsedSubsFile() throws IOException {
224
    var deparsedSubsFile = getDeparsedSubsFile();
1✔
225
    if (deparsedSubsFile != null) {
1✔
UNCOV
226
      return deparsedSubsFile;
×
227
    }
228

229
    for (VirtualFile possibleLocation : getPossibleFileLocations()) {
1✔
230
      try {
231
        return possibleLocation.findOrCreateChildData(this, DEPARSED_FILE_NAME);
1✔
232
      }
233
      catch (IOException e) {
×
234
        LOG.warn("Unable to create " + DEPARSED_FILE_NAME + " in the content root " + possibleLocation +
×
UNCOV
235
                 "cause: " + e.getMessage());
×
236
      }
UNCOV
237
    }
×
238

UNCOV
239
    throw new IOException("Could not find suitable location for creating a file: " + DEPARSED_FILE_NAME);
×
240
  }
241

242
  private boolean isFileUpToDate(@NotNull VirtualFile virtualFile) {
243
    String path = virtualFile.getCanonicalPath();
1✔
244

245
    if (path != null) {
1✔
246
      Long modificationStamp = myFilesMap.get(path);
1✔
247
      return modificationStamp != null && modificationStamp == VfsUtilCore.virtualToIoFile(virtualFile).lastModified();
1✔
248
    }
UNCOV
249
    return false;
×
250
  }
251

252
  public void reparseXSubs() {
253
    if (!PerlProjectManager.isPerlEnabled(myProject)) {
1✔
UNCOV
254
      return;
×
255
    }
256

257
    if (myParserTask != null) {
1✔
UNCOV
258
      Messages.showErrorDialog(
×
259
        myProject,
260
        PerlBundle.message("perl.deparsing.in.progress.message"),
×
UNCOV
261
        PerlBundle.message("perl.deparsing.in.progress.title")
×
262
      );
UNCOV
263
      return;
×
264
    }
265

266
    PerlCommandLine commandLine = PerlRunUtil.getPerlCommandLine(myProject, PerlPluginUtil.getHelperPath("xs_parser_simple.pl"));
1✔
267

268
    if (commandLine == null) {
1✔
269
      LOG.warn("Unable to create deparser command line");
×
UNCOV
270
      return;
×
271
    }
272
    commandLine.withCharset(StandardCharsets.UTF_8).withMissingPackageListener(false);
1✔
273

274
    LOG.info("Deparsing: " + commandLine.getCommandLineString());
1✔
275

276
    myParserTask = new Task.Backgroundable(myProject, PerlBundle.message("perl.deparsing.xsubs"), false) {
1✔
277
      @Override
278
      public void run(@NotNull ProgressIndicator indicator) {
279
        indicator.setIndeterminate(true);
1✔
280
        var project = PerlXSubsState.this.myProject;
1✔
281
        Map<String, Long> newFilesMap = ReadAction.compute(() -> {
1✔
282
          if (project.isDisposed()) {
1✔
UNCOV
283
            return null;
×
284
          }
285
          final Map<String, Long> result = new HashMap<>();
1✔
286
          for (VirtualFile virtualFile : getAllXSFiles(project)) {
1✔
287
            if (virtualFile.isValid()) {
1✔
288
              String filePath = virtualFile.getCanonicalPath();
1✔
289
              if (filePath != null) {
1✔
290
                result.put(filePath, VfsUtilCore.virtualToIoFile(virtualFile)
1✔
291
                  .lastModified());
1✔
292
              }
293
            }
294
          }
1✔
295
          return result;
1✔
296
        });
297
        if (newFilesMap == null) {
1✔
298
          myParserTask = null;
×
UNCOV
299
          return;
×
300
        }
301

302
        ProcessOutput processOutput;
303
        try {
304
          processOutput = PerlHostData.execAndGetOutput(commandLine);
1✔
305
        }
306
        catch (ExecutionException e) {
×
UNCOV
307
          LOG.warn("Error deparsing", e);
×
308

309
          showNotification(
×
310
            PerlBundle.message("perl.deparsing.error.execution"),
×
UNCOV
311
            e.getMessage(),
×
312
            NotificationType.ERROR
313
          );
314
          myParserTask = null;
×
UNCOV
315
          return;
×
316
        }
1✔
317
        final String stdout = processOutput.getStdout();
1✔
318
        String stderr = processOutput.getStderr();
1✔
319
        int exitCode = processOutput.getExitCode();
1✔
320
        LOG.info("Deparsing finished with exit code: " + exitCode +
1✔
321
                 (StringUtil.isEmpty(stderr) ? "" : ". STDERR:\n" + stderr));
1✔
322

323
        if (exitCode != 0) {
1✔
324
          showNotification(
×
UNCOV
325
            PerlBundle.message("perl.deparsing.error.execution"),
×
326
            stderr,
327
            NotificationType.ERROR
328
          );
329
        }
330
        else if (!stdout.isEmpty()) {
1✔
331
          Application application = ApplicationManager.getApplication();
1✔
332
          application.invokeAndWait(
1✔
333
            () -> WriteAction.run(() -> {
1✔
334
              if (project.isDisposed()) {
1✔
UNCOV
335
                return;
×
336
              }
337
              try {
338
                VirtualFile deparsedFile = getOrCreateDeparsedSubsFile();
1✔
339
                deparsedFile.setWritable(true);
1✔
340
                OutputStream outputStream = deparsedFile.getOutputStream(null);
1✔
341
                outputStream.write(stdout.getBytes());
1✔
342
                outputStream.close();
1✔
343
                deparsedFile.setWritable(false);
1✔
344
                FileContentUtilCore.reparseFiles(deparsedFile);
1✔
345

346
                myFilesMap = newFilesMap;
1✔
347
                isActual = true;
1✔
348

349
                showNotification(
1✔
350
                  PerlBundle.message("perl.deparsing.finished"),
1✔
351
                  "",
352
                  NotificationType.INFORMATION
353
                );
354
              }
355
              catch (IOException e) {
×
356
                LOG.warn("Error creating deparsed file", e);
×
357
                showNotification(
×
358
                  PerlBundle.message("perl.deparsing.error.creating.file"),
×
UNCOV
359
                  e.getMessage(),
×
360
                  NotificationType.ERROR
361
                );
362
              }
1✔
363
              // fixme fix modality state
364
            }));
1✔
365
        }
366
        myParserTask = null;
1✔
367
      }
1✔
368
    };
369
    myParserTask.queue();
1✔
370
  }
1✔
371

372
  private void showNotification(@NotNull @NotificationTitle String title,
373
                                @NotNull @NotificationContent String message,
374
                                @NotNull NotificationType type) {
375
    showNotification(title, message, type, null);
1✔
376
  }
1✔
377

378
  private void showNotification(@NotNull @NotificationTitle String title,
379
                                @NotNull @NotificationContent String message,
380
                                @NotNull NotificationType type,
381
                                @Nullable Function<? super Notification, ? extends List<AnAction>> actionsProvider) {
382
    Notification notification = new Notification(PerlBundle.message("perl.deparsing.notification"), title, message, type);
1✔
383

384
    if (actionsProvider != null) {
1✔
385
      List<AnAction> actions = actionsProvider.fun(notification);
1✔
386
      if (actions != null) {
1✔
387
        actions.forEach(notification::addAction);
1✔
388
      }
389
    }
390
    Notifications.Bus.notify(notification, myProject);
1✔
391
  }
1✔
392

393
  public static PerlXSubsState getInstance(@NotNull Project project) {
394
    return project.getService(PerlXSubsState.class);
1✔
395
  }
396
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc