Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import package detector from analysis-model #1237

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/main/java/edu/hm/hafner/util/CSharpNamespaceDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package edu.hm.hafner.util;

import java.util.regex.Pattern;

/**
* Detects the namespace of a C# workspace file.
*
* @author Ullrich Hafner
*/
class CSharpNamespaceDetector extends PackageDetector {
private static final Pattern NAMESPACE_PATTERN = Pattern.compile("^\\s*namespace\\s+([^{]*)\\s*\\{?\\s*$");

CSharpNamespaceDetector(final FileSystemFacade fileSystem) {
super(fileSystem);
}

@Override
public boolean accepts(final String fileName) {
return fileName.endsWith(".cs");
}

@Override
Pattern getPattern() {
return NAMESPACE_PATTERN;
}
}

27 changes: 27 additions & 0 deletions src/main/java/edu/hm/hafner/util/JavaPackageDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package edu.hm.hafner.util;

import java.util.regex.Pattern;

/**
* Detects the package name of a Java file.
*
* @author Ullrich Hafner
*/
class JavaPackageDetector extends PackageDetector {
private static final Pattern PACKAGE_PATTERN = Pattern.compile(
"^\\s*package\\s*([a-z]+[.\\w]*)\\s*;.*");

JavaPackageDetector(final FileSystemFacade fileSystem) {
super(fileSystem);
}

@Override
Pattern getPattern() {
return PACKAGE_PATTERN;
}

@Override
public boolean accepts(final String fileName) {
return fileName.endsWith(".java");
}
}
28 changes: 28 additions & 0 deletions src/main/java/edu/hm/hafner/util/KotlinPackageDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package edu.hm.hafner.util;

import java.util.regex.Pattern;

/**
* Detects the package name of a Kotlin file.
*
* @author Bastian Kersting
*/
class KotlinPackageDetector extends PackageDetector {
private static final Pattern PACKAGE_PATTERN = Pattern.compile(
"^\\s*package\\s*([a-z]+[.\\w]*)\\s*.*");

@VisibleForTesting
KotlinPackageDetector(final FileSystemFacade fileSystem) {
super(fileSystem);
}

@Override
Pattern getPattern() {
return PACKAGE_PATTERN;
}

@Override
boolean accepts(final String fileName) {
return fileName.endsWith(".kt");
}
}
110 changes: 110 additions & 0 deletions src/main/java/edu/hm/hafner/util/PackageDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package edu.hm.hafner.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.commons.io.input.BOMInputStream;

import com.google.errorprone.annotations.MustBeClosed;

/**
* Base class for package detectors.
*
* @author Ullrich Hafner
*/
abstract class PackageDetector {
private final FileSystemFacade fileSystem;

/**
* Creates a new instance of {@link PackageDetector}.
*
* @param fileSystem
* file system facade
*/
PackageDetector(final FileSystemFacade fileSystem) {
this.fileSystem = fileSystem;
}

/**
* Detects the package or namespace name of the specified file.
*
* @param fileName
* the file name of the file to scan
* @param charset
* the charset to use when reading the source files
*
* @return the detected package or namespace name
*/
public Optional<String> detectPackageName(final String fileName, final Charset charset) {
try (var stream = fileSystem.openFile(fileName)) {
return detectPackageName(stream, charset);
}
catch (IOException | InvalidPathException ignore) {
// ignore IO errors
}
return Optional.empty();
}

private Optional<String> detectPackageName(final InputStream stream, final Charset charset) throws IOException {
try (var buffer = new BufferedReader(
new InputStreamReader(BOMInputStream.builder().setInputStream(stream).get(), charset))) {
return detectPackageName(buffer.lines());
}
}

/**
* Detects the package or namespace name of the specified input stream. The stream will be closed automatically by
* the caller of this method.
*
* @param lines
* the content of the file to scan
*
* @return the detected package or namespace name
*/
private Optional<String> detectPackageName(final Stream<String> lines) {
Pattern pattern = getPattern();
return lines.map(pattern::matcher)
.filter(Matcher::matches)
.findFirst()
.map(matcher -> matcher.group(1))
.map(String::trim);
}

/**
* Returns the Pattern for the Package Name in this kind of file.
*
* @return the Pattern.
*/
abstract Pattern getPattern();

/**
* Returns whether this classifier accepts the specified file for processing.
*
* @param fileName
* the file name
*
* @return {@code true} if the classifier accepts the specified file for processing.
*/
abstract boolean accepts(String fileName);

/**
* Facade for file system operations. May be replaced by stubs in test cases.
*/
@VisibleForTesting
static class FileSystemFacade {
@MustBeClosed
InputStream openFile(final String fileName) throws IOException, InvalidPathException {
return Files.newInputStream(Paths.get(fileName));
}
}
}
31 changes: 31 additions & 0 deletions src/main/java/edu/hm/hafner/util/PackageDetectorFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package edu.hm.hafner.util;

import edu.hm.hafner.util.PackageDetector.FileSystemFacade;

/**
* Factory to create package detectors.
*
* @author Ullrich Hafner
*/
public final class PackageDetectorFactory {
/**
* Creates a new package detector runner that uses the detectors for Java, Kotlin, and C#.
*
* @return the package detector runner
*/
public static PackageDetectorRunner createPackageDetectors() {
return createPackageDetectors(new FileSystemFacade());
}

@VisibleForTesting
static PackageDetectorRunner createPackageDetectors(final FileSystemFacade facade) {
return new PackageDetectorRunner(
new JavaPackageDetector(facade),
new KotlinPackageDetector(facade),
new CSharpNamespaceDetector(facade));
}

private PackageDetectorFactory() {
// prevents instantiation
}
}
37 changes: 37 additions & 0 deletions src/main/java/edu/hm/hafner/util/PackageDetectorRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package edu.hm.hafner.util;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
* Detects package or namespace names of files in the file system.
*
* @author Ullrich Hafner
*/
public class PackageDetectorRunner {
private final List<PackageDetector> detectors;

PackageDetectorRunner(final PackageDetector... detectors) {
this.detectors = Arrays.asList(detectors);
}

/**
* Detects the package name of the specified file based on several detector strategies.
*
* @param fileName
* the filename of the file to scan
* @param charset
* the charset to use when reading the source files
*
* @return the detected package name or {@link Optional#empty()} if no package name could be detected
*/
public Optional<String> detectPackageName(final String fileName, final Charset charset) {
return detectors.stream()
.filter(detector -> detector.accepts(fileName))
.map(detector -> detector.detectPackageName(fileName, charset))
.flatMap(Optional::stream)
.findFirst();
}
}
59 changes: 59 additions & 0 deletions src/test/java/edu/hm/hafner/util/PackageDetectorRunnerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package edu.hm.hafner.util;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import edu.hm.hafner.util.PackageDetector.FileSystemFacade;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
* Tests the class {@link PackageDetectorRunner}.
*
* @author Ullrich Hafner
*/
class PackageDetectorRunnerTest extends ResourceTest {
@ParameterizedTest(name = "{index} => file={0}, expected package={1}")
@CsvSource({
"MavenJavaTest.txt.java, hudson.plugins.tasks.util",
"ActionBinding.cs, Avaloq.SmartClient.Utilities",
"KotlinTest.txt.kt, edu.hm.kersting",
})
void shouldExtractPackageNames(final String fileName, final String expectedPackage) throws IOException {
assertThat(detect(fileName)).contains(expectedPackage);
}

@ParameterizedTest(name = "{index} => file={0}, no package found")
@ValueSource(strings = {"MavenJavaTest.txt", "empty.txt", "KotlinTest.txt"})
void shouldNotAcceptFile(final String fileName) throws IOException {
assertThat(detect(fileName)).isEmpty();
}

private Optional<String> detect(final String fileName) throws IOException {
try (InputStream stream = asInputStream(fileName)) {
var fileSystem = mock(FileSystemFacade.class);
when(fileSystem.openFile(fileName)).thenReturn(stream);

var detectors = PackageDetectorFactory.createPackageDetectors(fileSystem);

return detectors.detectPackageName(fileName, StandardCharsets.UTF_8);
}
}

@Test
void shouldHandleException() throws IOException {
var fileSystem = mock(FileSystemFacade.class);
when(fileSystem.openFile(anyString())).thenThrow(new IOException("Simulated"));

assertThat(PackageDetectorFactory.createPackageDetectors(fileSystem)
.detectPackageName("file.java", StandardCharsets.UTF_8)).isEmpty();
}
}
18 changes: 18 additions & 0 deletions src/test/resources/edu/hm/hafner/util/ActionBinding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Author: Hafner Ullrich
*/

using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Forms;
using Avaloq.Utilities;
using log4net;

namespace Avaloq.SmartClient.Utilities {
/// <summary>
/// Acts as mediator between an <see cref="Action"/> and a clickable Windows Forms control.
/// </summary>
public class ActionBinding : IDisposable
// Rest of the file is omitted
}
5 changes: 5 additions & 0 deletions src/test/resources/edu/hm/hafner/util/KotlinTest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package edu.hm.kersting

class HelloWorld {
fun main() = println("Hello World")
}
5 changes: 5 additions & 0 deletions src/test/resources/edu/hm/hafner/util/KotlinTest.txt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package edu.hm.kersting

class HelloWorld {
fun main() = println("Hello World")
}
6 changes: 6 additions & 0 deletions src/test/resources/edu/hm/hafner/util/MavenJavaTest.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package hudson.plugins.tasks.util;

/**
* Indicates an orderly abortion of the processing.
*
// Rest of the file is omitted
6 changes: 6 additions & 0 deletions src/test/resources/edu/hm/hafner/util/MavenJavaTest.txt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package hudson.plugins.tasks.util;

/**
* Indicates an orderly abortion of the processing.
*/
// Rest of the file is omitted
Loading