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

Camelcade / Perl5-IDEA / #525521565

02 Jun 2025 09:13AM UTC coverage: 82.339% (+0.007%) from 82.332%
#525521565

push

github

hurricup
Added language attributes to HTML templates

30858 of 37477 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.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.PerlUtil;
67
import org.jetbrains.annotations.NotNull;
68
import org.jetbrains.annotations.Nullable;
69
import org.jetbrains.annotations.VisibleForTesting;
70

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

213
    return null;
1✔
214
  }
215

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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