Skip to content

Commit

Permalink
Development: Improve endpoint analysis (#9236)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jan-Thurner authored and MichaelOwenDyer committed Sep 3, 2024
1 parent d6099e9 commit bff9c88
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 67 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/analysis-of-endpoint-connections.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ name: Analysis of Endpoint Connections

on:
workflow_dispatch:
push:
pull_request:
types:
- opened
- synchronize
paths:
- 'src/main/java/**'
- 'src/main/webapp/**'

# Keep in sync with build.yml and test.yml and codeql-analysis.yml
env:
Expand All @@ -20,7 +26,7 @@ jobs:
with:
fetch-depth: 0

- name: Set up JDK 21
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '${{ env.java }}'
Expand Down Expand Up @@ -59,7 +65,7 @@ jobs:
with:
fetch-depth: 0

- name: Set up JDK 21
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

public class EndpointAnalyzer {

private static String EndpointAnalysisResultPath = "endpointAnalysisResult.json";
private static String ENDPOINT_ANALYSIS_RESULT_PATH = "endpointAnalysisResult.json";

private static final Logger logger = LoggerFactory.getLogger(EndpointAnalyzer.class);
private static final Logger log = LoggerFactory.getLogger(EndpointAnalyzer.class);

public static void main(String[] args) {
analyzeEndpoints();
Expand Down Expand Up @@ -60,10 +60,13 @@ private static void analyzeEndpoints() {
for (EndpointInformation endpoint : endpointClass.endpoints()) {

String endpointURI = endpoint.buildComparableEndpointUri();
List<RestCallInformation> matchingRestCalls = restCallMap.getOrDefault(endpointURI, new ArrayList<>());
List<RestCallInformation> restCallsWithMatchingURI = restCallMap.getOrDefault(endpointURI, new ArrayList<>());

// Check for wildcard endpoints if no exact match is found
checkForWildcardEndpoints(endpoint, matchingRestCalls, endpointURI, restCallMap);
checkForWildcardEndpoints(endpoint, restCallsWithMatchingURI, endpointURI, restCallMap);

List<RestCallInformation> matchingRestCalls = restCallsWithMatchingURI.stream()
.filter(restCall -> restCall.method().toLowerCase().equals(endpoint.getHttpMethod().toLowerCase())).toList();

if (matchingRestCalls.isEmpty()) {
unusedEndpoints.add(endpoint);
Expand All @@ -75,10 +78,10 @@ private static void analyzeEndpoints() {
}

EndpointAnalysis endpointAnalysis = new EndpointAnalysis(endpointsAndMatchingRestCalls, unusedEndpoints);
mapper.writeValue(new File(EndpointAnalysisResultPath), endpointAnalysis);
mapper.writeValue(new File(ENDPOINT_ANALYSIS_RESULT_PATH), endpointAnalysis);
}
catch (IOException e) {
logger.error("Failed to analyze endpoints", e);
log.error("Failed to analyze endpoints", e);
}
}

Expand Down Expand Up @@ -119,26 +122,26 @@ private static void printEndpointAnalysisResult() {
ObjectMapper mapper = new ObjectMapper();
EndpointAnalysis endpointsAndMatchingRestCalls = null;
try {
endpointsAndMatchingRestCalls = mapper.readValue(new File(EndpointAnalysisResultPath), new TypeReference<EndpointAnalysis>() {
endpointsAndMatchingRestCalls = mapper.readValue(new File(ENDPOINT_ANALYSIS_RESULT_PATH), new TypeReference<EndpointAnalysis>() {
});
}
catch (IOException e) {
logger.error("Failed to deserialize endpoint analysis result", e);
log.error("Failed to deserialize endpoint analysis result", e);
return;
}

endpointsAndMatchingRestCalls.unusedEndpoints().stream().forEach(endpoint -> {
logger.info("=============================================");
logger.info("Endpoint URI: {}", endpoint.buildCompleteEndpointURI());
logger.info("HTTP method: {}", endpoint.httpMethodAnnotation());
logger.info("File path: {}", endpoint.className());
logger.info("Line: {}", endpoint.line());
logger.info("=============================================");
logger.info("No matching REST call found for endpoint: {}", endpoint.buildCompleteEndpointURI());
logger.info("---------------------------------------------");
logger.info("");
log.info("=============================================");
log.info("Endpoint URI: {}", endpoint.buildCompleteEndpointURI());
log.info("HTTP method: {}", endpoint.httpMethodAnnotation());
log.info("File path: {}", endpoint.className());
log.info("Line: {}", endpoint.line());
log.info("=============================================");
log.info("No matching REST call found for endpoint: {}", endpoint.buildCompleteEndpointURI());
log.info("---------------------------------------------");
log.info("");
});

logger.info("Number of endpoints without matching REST calls: {}", endpointsAndMatchingRestCalls.unusedEndpoints().size());
log.info("Number of endpoints without matching REST calls: {}", endpointsAndMatchingRestCalls.unusedEndpoints().size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

import com.fasterxml.jackson.annotation.JsonIgnore;

public record EndpointInformation(String requestMapping, String endpoint, String httpMethodAnnotation, String URI, String className, int line, List<String> otherAnnotations) {
public record EndpointInformation(String requestMapping, String endpoint, String httpMethodAnnotation, String uri, String className, int line, List<String> otherAnnotations) {

public String buildCompleteEndpointURI() {
String buildCompleteEndpointURI() {
StringBuilder result = new StringBuilder();
if (this.requestMapping != null && !this.requestMapping.isEmpty()) {
// Remove quotes from the requestMapping as they are used to define the String in the source code but are not part of the URI
result.append(this.requestMapping.replace("\"", ""));
}
// Remove quotes from the URI as they are used to define the String in the source code but are not part of the URI
result.append(this.URI.replace("\"", ""));
result.append(this.uri.replace("\"", ""));
return result.toString();
}

Expand All @@ -23,7 +23,7 @@ String buildComparableEndpointUri() {
}

@JsonIgnore
public String getHttpMethod() {
String getHttpMethod() {
return switch (this.httpMethodAnnotation) {
case "GetMapping" -> "get";
case "PostMapping" -> "post";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class EndpointParser {

static final String REST_CALL_PARSING_RESULT_PATH = "restCalls.json";

private static final Logger logger = LoggerFactory.getLogger(EndpointParser.class);
private static final Logger log = LoggerFactory.getLogger(EndpointParser.class);

public static void main(String[] args) {
final Path absoluteDirectoryPath = Path.of("../../src/main/java").toAbsolutePath().normalize();
Expand All @@ -48,7 +48,7 @@ public static void main(String[] args) {
filesToParse = paths.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".java")).map(Path::toString).toArray(String[]::new);
}
catch (IOException e) {
logger.error("Error reading files from directory: {}", absoluteDirectoryPath, e);
log.error("Error reading files from directory: {}", absoluteDirectoryPath, e);
}

parseServerEndpoints(filesToParse);
Expand Down Expand Up @@ -190,9 +190,9 @@ else if (annotation instanceof NormalAnnotationExpr normalAnnotationExpr) {
*/
private static void printFilesFailedToParse(List<String> filesFailedToParse) {
if (!filesFailedToParse.isEmpty()) {
logger.warn("Files failed to parse:", filesFailedToParse);
log.warn("Files failed to parse:", filesFailedToParse);
for (String file : filesFailedToParse) {
logger.warn(file);
log.warn(file);
}
}
}
Expand All @@ -211,7 +211,7 @@ private static void writeEndpointsToFile(List<EndpointClassInformation> endpoint
new ObjectMapper().writeValue(new File(ENDPOINT_PARSING_RESULT_PATH), endpointClasses);
}
catch (IOException e) {
logger.error("Failed to write endpoint information to file", e);
log.error("Failed to write endpoint information to file", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class RestCallAnalyzer {

private static final String REST_CALL_ANALYSIS_RESULT_PATH = "restCallAnalysisResult.json";

private static final Logger logger = LoggerFactory.getLogger(RestCallAnalyzer.class);
private static final Logger log = LoggerFactory.getLogger(RestCallAnalyzer.class);

public static void main(String[] args) {
analyzeRestCalls();
Expand Down Expand Up @@ -58,16 +58,19 @@ private static void analyzeRestCalls() {
for (RestCallFileInformation restCallFile : restCalls) {
for (RestCallInformation restCall : restCallFile.restCalls()) {
String restCallURI = restCall.buildComparableRestCallUri();
List<EndpointInformation> matchingEndpoints = endpointMap.getOrDefault(restCallURI, new ArrayList<>());
List<EndpointInformation> endpointsWithMatchingUri = endpointMap.getOrDefault(restCallURI, new ArrayList<>());

checkForWildcardMatches(restCall, matchingEndpoints, restCallURI, endpointMap);
checkForWildcardMatches(restCall, endpointsWithMatchingUri, restCallURI, endpointMap);

if (matchingEndpoints.isEmpty()) {
List<EndpointInformation> endpointsWithMatchingHttpMethod = endpointsWithMatchingUri.stream()
.filter(endpoint -> endpoint.getHttpMethod().toLowerCase().equals(restCall.method().toLowerCase())).toList();

if (endpointsWithMatchingHttpMethod.isEmpty()) {
restCallsWithoutMatchingEndpoint.add(restCall);
}
else {
for (EndpointInformation endpoint : matchingEndpoints) {
restCallsWithMatchingEndpoint.add(new RestCallWithMatchingEndpoint(endpoint, restCall, restCall.fileName()));
for (EndpointInformation endpoint : endpointsWithMatchingHttpMethod) {
restCallsWithMatchingEndpoint.add(new RestCallWithMatchingEndpoint(endpoint, restCall, restCall.filePath()));
}
}
}
Expand All @@ -77,7 +80,7 @@ private static void analyzeRestCalls() {
mapper.writeValue(new File(REST_CALL_ANALYSIS_RESULT_PATH), restCallAnalysis);
}
catch (IOException e) {
logger.error("Failed to analyze REST calls", e);
log.error("Failed to analyze REST calls", e);
}
}

Expand Down Expand Up @@ -124,21 +127,22 @@ private static void printRestCallAnalysisResult() {
});
}
catch (IOException e) {
logger.error("Failed to deserialize rest call analysis results", e);
log.error("Failed to deserialize rest call analysis results", e);
return;
}

restCallsAndMatchingEndpoints.restCallsWithoutMatchingEndpoints().stream().forEach(endpoint -> {
logger.info("=============================================");
logger.info("REST call URI: {}", endpoint.buildCompleteRestCallURI());
logger.info("HTTP method: {}", endpoint.method());
logger.info("File path: {}", endpoint.fileName());
logger.info("Line: {}", endpoint.line());
logger.info("=============================================");
logger.info("No matching endpoint found for REST call: {}", endpoint.buildCompleteRestCallURI());
logger.info("---------------------------------------------");
logger.info("");
log.info("=============================================");
log.info("REST call URI: {}", endpoint.buildCompleteRestCallURI());
log.info("HTTP method: {}", endpoint.method());
log.info("File path: {}", endpoint.filePath());
log.info("Line: {}", endpoint.line());
log.info("=============================================");
log.info("No matching endpoint found for REST call: {}", endpoint.buildCompleteRestCallURI());
log.info("---------------------------------------------");
log.info("");
});

logger.info("Number of REST calls without matching endpoints: {}", restCallsAndMatchingEndpoints.restCallsWithoutMatchingEndpoints().size());
log.info("Number of REST calls without matching endpoints: {}", restCallsAndMatchingEndpoints.restCallsWithoutMatchingEndpoints().size());
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package de.tum.cit.endpointanalysis;

public record RestCallFileInformation(String fileName, RestCallInformation[] restCalls) {
import java.util.List;

public record RestCallFileInformation(String filePath, List<RestCallInformation> restCalls) {
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package de.tum.cit.endpointanalysis;

public record RestCallInformation(String method, String url, int line, String fileName) {
public record RestCallInformation(String method, String url, String filePath, int line) {

public String buildCompleteRestCallURI() {
String buildCompleteRestCallURI() {
return this.url.replace("`", "");
}

public String buildComparableRestCallUri() {
String buildComparableRestCallUri() {
// Replace arguments with placeholder
String result = this.buildCompleteRestCallURI().replaceAll("\\$\\{.*?\\}", ":param:");

// Remove query parameters
result = result.split("\\?")[0];

// Some URIs in the artemis client start with a redundant `/`. To be able to compare them to the endpoint URIs, we remove it.
if (result.startsWith("/")) {
result = result.substring(1);
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface RestCall {
method: string;
url: string;
line: number;
fileName: string;
filePath: string;
}

enum ParsingResultType {
Expand Down Expand Up @@ -38,17 +38,17 @@ class ParsingResult {
}

export class Postprocessor {
static filesWithRestCalls: { fileName: string, restCalls: RestCall[] }[] = [];
static filesWithRestCalls: { filePath: string, restCalls: RestCall[] }[] = [];
private readonly restCalls: RestCall[] = [];
private readonly fileName: string;
private readonly filePath: string;
private readonly ast: TSESTree.Program;

/**
* @param fileName - The name of the file being processed.
* @param filePath - The path of the file being processed.
* @param ast - The abstract syntax tree (AST) of the processed file.
*/
constructor(fileName: string, ast: TSESTree.Program) {
this.fileName = fileName;
constructor(filePath: string, ast: TSESTree.Program) {
this.filePath = filePath;
this.ast = ast;
}

Expand All @@ -61,7 +61,7 @@ export class Postprocessor {
}
});
if (this.restCalls.length > 0) {
Postprocessor.filesWithRestCalls.push( {fileName: this.fileName, restCalls: this.restCalls} );
Postprocessor.filesWithRestCalls.push({ filePath: this.filePath, restCalls: this.restCalls });
}
}

Expand Down Expand Up @@ -108,10 +108,10 @@ export class Postprocessor {
urlEvaluationResult = this.evaluateUrl(node.arguments[0], methodDefinition, node, classBody);
}

const fileName = this.fileName;
const filePath = this.filePath;
if (urlEvaluationResult.resultType === ParsingResultType.EVALUATE_URL_SUCCESS) {
for (let url of urlEvaluationResult.result) {
this.restCalls.push({ method, url, line, fileName });
for (const url of urlEvaluationResult.result) {
this.restCalls.push({ method, url, line, filePath });
}
}
}
Expand Down Expand Up @@ -175,6 +175,7 @@ export class Postprocessor {
}

/**
* Evaluates a template literal AST node to determine its URL value.
* Evaluates a template literal AST node to determine its URL value.
*
* This method evaluates the provided template literal node by calling `evaluateTemplateLiteralExpression`.
Expand Down Expand Up @@ -541,7 +542,7 @@ export class Postprocessor {
simpleTraverse(methodDefinition, {
enter: (node) => {
if (node.type === 'VariableDeclaration') {
for (let decl of node.declarations) {
for (const decl of node.declarations) {
if (decl.id.type === 'Identifier' && decl.id.name === name && decl.init) {
const tempResult = this.evaluateUrl(decl.init, methodDefinition, restCall, classBody);
if (tempResult.resultType === ParsingResultType.EVALUATE_URL_SUCCESS) {
Expand Down Expand Up @@ -587,7 +588,7 @@ export class Postprocessor {
* @returns An array of AST nodes representing the parameters of the constructor.
*/
getConstructorArgumentsFromClassBody(classBody: TSESTree.ClassBody): TSESTree.Parameter[] {
for (let node of classBody.body) {
for (const node of classBody.body) {
if (node.type === 'MethodDefinition' && node.key.type === 'Identifier' && node.key.name === 'constructor') {
return node.value.params;
}
Expand Down Expand Up @@ -615,11 +616,11 @@ export class Postprocessor {
const superClass = Preprocessor.PREPROCESSING_RESULTS.get(this.getClassNameFromClassBody(classBody));
if (superClass) {
const constructorArguments = this.getConstructorArgumentsFromClassBody(classBody.body);
for (let superConstructorCallArguments of superClass.superConstructorCalls) {
for (const superConstructorCallArguments of superClass.superConstructorCalls) {
for (let i = 0; i < superConstructorCallArguments.arguments.length; i++) {
let constructorArgument = constructorArguments[i];
const constructorArgument = constructorArguments[i];
if (superConstructorCallArguments.arguments[i] !== '' && constructorArgument.type === 'TSParameterProperty'
&& constructorArgument.parameter.type === 'Identifier' && constructorArgument.parameter.name === memberExprKey) {
&& constructorArgument.parameter.type === 'Identifier' && constructorArgument.parameter.name === memberExprKey) {
memberExpressionResult.push(superConstructorCallArguments.arguments[i]);
resultType = ParsingResultType.EVALUATE_MEMBER_EXPRESSION_SUCCESS;
}
Expand Down

0 comments on commit bff9c88

Please sign in to comment.