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

Camelcade / Perl5-IDEA / #525521635

20 Jul 2025 03:45PM UTC coverage: 82.266% (-0.09%) from 82.355%
#525521635

push

github

hurricup
Build 252.23892.248

30936 of 37605 relevant lines covered (82.27%)

0.82 hits per line

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

77.27
/plugin/backend/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.project.Project;
39
import com.intellij.openapi.project.ProjectUtil;
40
import com.intellij.openapi.projectRoots.Sdk;
41
import com.intellij.openapi.roots.ProjectRootManager;
42
import com.intellij.openapi.ui.Messages;
43
import com.intellij.openapi.util.NlsContexts.NotificationContent;
44
import com.intellij.openapi.util.NlsContexts.NotificationTitle;
45
import com.intellij.openapi.util.text.StringUtil;
46
import com.intellij.openapi.vfs.VfsUtilCore;
47
import com.intellij.openapi.vfs.VirtualFile;
48
import com.intellij.psi.search.FilenameIndex;
49
import com.intellij.psi.search.GlobalSearchScope;
50
import com.intellij.psi.search.GlobalSearchScopesCore;
51
import com.intellij.testFramework.LightVirtualFile;
52
import com.intellij.util.FileContentUtilCore;
53
import com.intellij.util.Function;
54
import com.intellij.util.containers.ContainerUtil;
55
import com.intellij.util.xmlb.XmlSerializerUtil;
56
import com.intellij.util.xmlb.annotations.Transient;
57
import com.perl5.PerlBundle;
58
import com.perl5.lang.perl.idea.PerlPathMacros;
59
import com.perl5.lang.perl.idea.actions.PerlActionBase;
60
import com.perl5.lang.perl.idea.execution.PerlCommandLine;
61
import com.perl5.lang.perl.idea.project.PerlProjectManager;
62
import com.perl5.lang.perl.idea.sdk.host.PerlHostData;
63
import com.perl5.lang.perl.idea.sdk.host.os.PerlOsHandler;
64
import com.perl5.lang.perl.util.PerlPluginUtil;
65
import com.perl5.lang.perl.util.PerlRunUtil;
66
import com.perl5.lang.perl.util.PerlSubUtilCore;
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
  public boolean isActual = true;
1✔
84
  public Map<String, Long> myFilesMap = Collections.emptyMap();
1✔
85
  @Transient
1✔
86
  private Task.Backgroundable myParserTask = null;
87
  @Transient
88
  private final @Nullable Project myProject;
89

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

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

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

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

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

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

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

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

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

159
            if (!virtualFile.isValid()) {
1✔
160
              continue;
×
161
            }
162

163
            indicator.setFraction((double)filesCounter / allXSFiles.size());
1✔
164

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

175
        isActual = isActual && (filesCounter == 0 || getDeparsedSubsFile() != null);
1✔
176

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

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

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

212
    return null;
1✔
213
  }
214

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

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

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

237
    throw new IOException("Could not find suitable location for creating a file: " + PerlSubUtilCore.DEPARSED_FILE_NAME);
×
238
  }
239

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

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

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

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

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

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

272
    LOG.info("Deparsing: " + commandLine.getCommandLineString());
1✔
273

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

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

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

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

344
                myFilesMap = newFilesMap;
1✔
345
                isActual = true;
1✔
346

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

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

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

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

391
  public static PerlXSubsState getInstance(@NotNull Project project) {
392
    return project.getService(PerlXSubsState.class);
1✔
393
  }
394
}
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