diff --git a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java index e430f1962f1..df4db032d17 100644 --- a/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java +++ b/src/main/java/spoon/support/compiler/jdt/PositionBuilder.java @@ -7,7 +7,6 @@ */ package spoon.support.compiler.jdt; -import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.internal.compiler.ast.ASTNode; import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration; import org.eclipse.jdt.internal.compiler.ast.AbstractVariableDeclaration; @@ -43,12 +42,13 @@ import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtModifiable; import spoon.reflect.declaration.CtPackage; +import spoon.reflect.declaration.ModifierKind; import spoon.reflect.factory.CoreFactory; import spoon.reflect.reference.CtTypeReference; import spoon.support.compiler.jdt.ContextBuilder.CastInfo; import spoon.support.reflect.CtExtendedModifier; +import spoon.support.util.internal.lexer.ModifierExtractor; -import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -62,10 +62,12 @@ */ public class PositionBuilder { + private final ModifierExtractor extractor; private final JDTTreeBuilder jdtTreeBuilder; public PositionBuilder(JDTTreeBuilder jdtTreeBuilder) { this.jdtTreeBuilder = jdtTreeBuilder; + this.extractor = new ModifierExtractor(); } SourcePosition buildPosition(int sourceStart, int sourceEnd) { @@ -320,8 +322,8 @@ SourcePosition buildPositionCtElement(CtElement e, ASTNode node) { modifiersSourceEnd = findPrevNonWhitespace(contents, modifiersSourceStart - 1, findPrevWhitespace(contents, modifiersSourceStart - 1, findPrevNonWhitespace(contents, modifiersSourceStart - 1, sourceStart - 1))); - if (e instanceof CtModifiable) { - setModifiersPosition((CtModifiable) e, modifiersSourceStart, modifiersSourceEnd); + if (e instanceof CtModifiable modifiable) { + setModifiersPosition(modifiable, modifiersSourceStart, modifiersSourceEnd); } if (modifiersSourceEnd < modifiersSourceStart) { //there is no modifier @@ -570,44 +572,26 @@ private void setModifiersPosition(CtModifiable e, int start, int end) { char[] contents = jdtTreeBuilder.getContextBuilder().getCompilationUnitContents(); Set modifiers = e.getExtendedModifiers(); - Map explicitModifiersByName = new HashMap<>(); + Map explicitModifiersByKind = new HashMap<>(); for (CtExtendedModifier modifier: modifiers) { if (modifier.isImplicit()) { modifier.setPosition(cf.createPartialSourcePosition(cu)); continue; } - if (explicitModifiersByName.put(modifier.getKind().toString(), modifier) != null) { - throw new SpoonException("The modifier " + modifier.getKind().toString() + " found twice"); + if (explicitModifiersByKind.put(modifier.getKind(), modifier) != null) { + throw new SpoonException("The modifier " + modifier.getKind().toString() + " was found twice"); } } - //move end after the last char - end++; - while (start < end && explicitModifiersByName.size() > 0) { - int o1 = findNextNonWhitespace(contents, end - 1, start); - if (o1 == -1) { - break; - } - int o2 = findNextWhitespace(contents, end - 1, o1); - if (o2 == -1) { - o2 = end; - } - - // this is the index into the modifier char array snippet, so must be +o1 if >-1 - int chevronIndex = ArrayUtils.indexOf(Arrays.copyOfRange(contents, o1, o2), '<'); - if (chevronIndex > 0) { - o2 = o1 + chevronIndex; - } - - String modifierName = String.valueOf(contents, o1, o2 - o1); - CtExtendedModifier modifier = explicitModifiersByName.remove(modifierName); - if (modifier != null) { - modifier.setPosition(cf.createSourcePosition(cu, o1, o2 - 1, jdtTreeBuilder.getContextBuilder().getCompilationUnitLineSeparatorPositions())); - } - start = o2; - } - if (explicitModifiersByName.size() > 0) { - throw new SpoonException("Position of CtExtendedModifiers: [" + String.join(", ", explicitModifiersByName.keySet()) + "] not found in " + String.valueOf(contents, start, end - start)); + extractor.collectModifiers( + contents, + start, + Math.max(start, end) + 1, // move end after the last char, fixup weird end positions + explicitModifiersByKind, + (modStart, modEnd) -> cf.createSourcePosition(cu, modStart, modEnd, cu.getLineSeparatorPositions()) + ); + if (!explicitModifiersByKind.isEmpty()) { + throw new SpoonException("Position of CtExtendedModifiers: " + explicitModifiersByKind.keySet() + " not found in " + String.valueOf(contents, start, end - start)); } } diff --git a/src/main/java/spoon/support/util/internal/lexer/CharRemapper.java b/src/main/java/spoon/support/util/internal/lexer/CharRemapper.java new file mode 100644 index 00000000000..9131aadabac --- /dev/null +++ b/src/main/java/spoon/support/util/internal/lexer/CharRemapper.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.lexer; + +import java.util.Arrays; + +/** + * A helper class to deal with unicode escapes. + */ +class CharRemapper { + private final char[] content; + private final int start; + private final int end; + private int[] positionRemap; + + CharRemapper(char[] content, int start, int end) { + this.content = content; + this.start = start; + this.end = end; + } + + /** + * {@return the sub-array from start to end of the original char array with unicode escapes replaced} + */ + char[] remapContent() { + char[] chars = new char[this.end - this.start]; // approximate + int t = 0; + boolean escape = false; + for (int i = this.start; i < this.end; i++, t++) { + if (!escape && this.content[i] == '\\' && this.end > i + 5 && this.content[i + 1] == 'u') { + int utf16 = parseHex(i + 2); + if (utf16 >= 0) { + chars[t] = (char) utf16; + i += 5; + if (this.positionRemap == null) { + this.positionRemap = createPositionRemap(chars); + } + this.positionRemap[t] = 6; + continue; + } + } + if (this.content[i] == '\\') { + if (escape) { + escape = false; + } else if (this.end > i + 1 && this.content[i + 1] == '\\') { + escape = true; + } + } + chars[t] = this.content[i]; + } + if (t == chars.length) { + return chars; + } + // otherwise, we encountered a unicode sequence + this.positionRemap[0] += this.start; + Arrays.parallelPrefix(this.positionRemap, Integer::sum); + return Arrays.copyOf(chars, t); + } + + int remapPosition(int index) { + if (this.positionRemap == null) { + return index + this.start; + } + if (index == 0) { + return this.start; + } + return this.positionRemap[index - 1]; + } + + private int[] createPositionRemap(char[] chars) { + int[] remap = new int[chars.length]; + Arrays.fill(remap, 1); + return remap; + } + + private int parseHex(int start) { + int result = 0; + for (int i = start; i < start + 4; i++) { + result *= 16; + char c = this.content[i]; + if ('0' <= c && '9' >= c) { + result += c - '0'; + } else { + c |= ' '; // lowercase potential letter + if ('a' <= c && 'f' >= c) { + result += (c - 'a') + 10; + continue; + } + // not a valid symbol, mark result + result = Integer.MIN_VALUE; + } + } + return result; + } +} diff --git a/src/main/java/spoon/support/util/internal/lexer/JavaKeyword.java b/src/main/java/spoon/support/util/internal/lexer/JavaKeyword.java new file mode 100644 index 00000000000..dd3b22edd30 --- /dev/null +++ b/src/main/java/spoon/support/util/internal/lexer/JavaKeyword.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.lexer; + +/** + * Valid Java (contextual) keywords + */ +enum JavaKeyword { + ABSTRACT, + ASSERT, + BOOLEAN, + BREAK, + BYTE, + CASE, + CATCH, + CHAR, + CLASS, + CONTINUE, + DEFAULT, + DO, + DOUBLE, + ELSE, + EXTENDS, + FALSE, + FINAL, + FINALLY, + FLOAT, + FOR, + IF, + IMPLEMENTS, + IMPORT, + INSTANCEOF, + INT, + INTERFACE, + LONG, + NATIVE, + NEW, + NON_SEALED { + @Override + public String toString() { + return "non-sealed"; + } + }, + NULL, + PACKAGE, + PERMITS, + PRIVATE, + PROTECTED, + PUBLIC, + RECORD, + RETURN, + SEALED, + SHORT, + STATIC, + STRICTFP, + SUPER, + SWITCH, + SYNCHRONIZED, + THIS, + THROW, + THROWS, + TRANSIENT, + TRUE, + TRY, + VOID, + VOLATILE, + WHILE, + YIELD; + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/spoon/support/util/internal/lexer/JavaLexer.java b/src/main/java/spoon/support/util/internal/lexer/JavaLexer.java new file mode 100644 index 00000000000..eade7cb962d --- /dev/null +++ b/src/main/java/spoon/support/util/internal/lexer/JavaLexer.java @@ -0,0 +1,399 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.lexer; + +import org.jspecify.annotations.Nullable; +import spoon.support.util.internal.trie.Trie; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static java.lang.Character.isJavaIdentifierPart; + +public class JavaLexer { + private static final char ESCAPE_CHAR = '\\'; + private static final Trie KEYWORD_TRIE = Trie.ofWords( + Arrays.stream(JavaKeyword.values()) + .collect(Collectors.toMap(JavaKeyword::toString, Function.identity())) + ); + private final char[] content; + private final CharRemapper charRemapper; + private int nextPos; + + /** + * Creates a java lexer for the given content and range. + * + * @param content the content to lex. + * @param start the start offset of the range to lex (inclusive) + * @param end the end offset of the range to lex (exclusive) + */ + public JavaLexer(char[] content, int start, int end) { + this.charRemapper = new CharRemapper(content, start, end); + this.content = this.charRemapper.remapContent(); + this.nextPos = 0; + } + + /** + * {@return {@code null} if no more tokens can be lexed in the range of this lexer} + */ + public @Nullable Token lex() { + if (!skipCommentsAndWhitespaces()) { + return null; + } + int pos = this.nextPos; + char c = next(); + return switch (c) { + case '(', ')', '{', '}', '[', ']', ';', ',', '@' -> createToken(TokenType.SEPARATOR, pos); + case '.' -> { + if (hasMore(2) && peek() == '.' && peek(1) == '.') { + skip(2); + } + yield createToken(TokenType.SEPARATOR, pos); + } + case ':' -> { + if (hasMore() && peek() == ':') { + next(); + yield createToken(TokenType.SEPARATOR, pos); + } + yield createToken(TokenType.OPERATOR, pos); + } + // no comment, already skipped + case '/', '=', '*', '^', '%', '!' -> lexSingleOrDoubleOperator(pos, '='); + case '+' -> lexSingleOrDoubleOperator(pos, '=', '+'); + case '-' -> lexSingleOrDoubleOperator(pos, '=', '-', '>'); + case '&' -> lexSingleOrDoubleOperator(pos, '=', '&'); + case '|' -> lexSingleOrDoubleOperator(pos, '=', '|'); + case '>' -> lexAngleBracket(pos, 1, 3, '>'); + case '<' -> lexAngleBracket(pos, 1, 2, '<'); + case '\'' -> lexCharacterLiteral(pos); + case '"' -> lexStringLiteral(pos); + case '~', '?' -> createToken(TokenType.OPERATOR, pos); + default -> { + skip(-1); // reset to previous + yield lexLiteralOrKeywordOrIdentifier(); + } + }; + } + + private Token createToken(TokenType type, int pos) { + return new Token(type, this.charRemapper.remapPosition(pos), this.charRemapper.remapPosition(this.nextPos)); + } + + private Token lexAngleBracket(int pos, int found, int maxFound, char bracket) { + if (hasMore()) { + char peek = peek(); + if (peek == '=') { + next(); + return createToken(TokenType.OPERATOR, pos); + } + if (peek == bracket && found < maxFound) { + next(); + return lexAngleBracket(pos, found + 1, maxFound, bracket); + } + } + return createToken(TokenType.OPERATOR, pos); + } + + private void skipUntilLineBreak() { + while (hasMore()) { + char peek = peek(); + if (peek == '\n' || peek == '\r') { + skipWhitespaces(); + return; + } + next(); + } + } + + private boolean skipUntil(String s) { + char[] chars = s.toCharArray(); + char first = chars[0]; + int pos = this.nextPos; + do { + int index = indexOf(first, pos); + if (index < 0) { + // TODO what if not present? + return false; + } + if (Arrays.equals(this.content, index, index + chars.length, chars, 0, chars.length)) { + this.nextPos = index + chars.length; + return true; + } else { + pos = index + 1; + } + } while (true); + } + + private @Nullable Token lexLiteralOrKeywordOrIdentifier() { + int pos = this.nextPos; + char next = next(); // assuming next is available as `lex` already checks that + if (Character.isJavaIdentifierStart(next)) { + while (hasMore() && isJavaIdentifierPart(peek())) { + next(); + } + Optional match = KEYWORD_TRIE.findMatch(this.content, pos, this.nextPos); + if (match.isPresent()) { + return createToken(TokenType.KEYWORD, pos); + } + // special case: non-sealed is not a valid java identifier, but a keyword + if (isNonSealed(pos)) { + skip("sealed".length() + 1); // move behind + return createToken(TokenType.KEYWORD, pos); + } + return createToken(TokenType.IDENTIFIER, pos); + } else if (Character.isDigit(next)) { + readNumericLiteral(next); + return createToken(TokenType.LITERAL, pos); + } + return null; + } + + private boolean isNonSealed(int pos) { + int requiredForNonSealed = "sealed".length(); + boolean startsWithNonSealed = hasMore(requiredForNonSealed) + && isNon(pos, this.content) + && isDashSealed(content, this.nextPos); + if (startsWithNonSealed) { + return !hasMore(requiredForNonSealed + 1) + || !isJavaIdentifierPart(peek(requiredForNonSealed + 1)); + } + return false; + } + + private void readNumericLiteral(char first) { + if (!hasMore()) { + return; + } + // check if hexadecimal notation + if (first == '0') { + if (peek() == 'x' || peek() == 'X') { + next(); + readHexadecimalsAndUnderscore(); + if (hasMore() && peek() == '.') { + next(); + readHexadecimalFloatingPointLiteral(); + } else { + readHexadecimalsAndUnderscore(); + } + } else if (peek() == 'b' || peek() == 'B') { + next(); + readDigitsOrUnderscore(); + } else { + next(); + readDigitsOrUnderscore(); + } + } else { + readDigitsOrUnderscore(); + if (hasMore() && peek() == '.') { + next(); + readDigitsOrUnderscore(); + } + } + if (hasMore()) { + char peek = peek(); + switch (peek) { + case 'd': + case 'D': + case 'f': + case 'F': + case 'l': + case 'L': + next(); + } + } + } + + private void readDigitsOrUnderscore() { + while (hasMore()) { + char peek = peek(); + if ('0' <= peek && peek <= '9' || peek == '_') { + next(); + } else { + return; + } + } + } + + // the part after the . + private void readHexadecimalFloatingPointLiteral() { + readHexadecimalsAndUnderscore(); + if (hasMore() && peek() == 'p' || peek() == 'P') { + next(); + if (hasMore() && peek() == '+') { + next(); + readDigitsOrUnderscore(); + } + } + } + + private void readHexadecimalsAndUnderscore() { + while (hasMore()) { + char peek = peek(); + if ('0' <= peek && peek <= '9' + || 'A' <= peek && peek <= 'F' + || 'a' <= peek && peek <= 'f' + || peek == '_') { + next(); + } else { + return; + } + } + } + + private @Nullable Token lexStringLiteral(int pos) { + if (hasMore(2) && peek() == '"' && peek(1) == '"') { + skip(2); + return lexTextBlockLiteral(pos); + } + while (hasMore()) { + char peek = peek(); + if (peek == ESCAPE_CHAR) { + next(); + if (hasMore()) { + next(); // assuming the string is correct, we're skipping every escapable char, including " + } + } else if (next() == '"') { + return createToken(TokenType.LITERAL, pos); + } + } + return null; + } + + private @Nullable Token lexTextBlockLiteral(int pos) { + while (hasMore(2)) { + char peek = peek(); + if (peek == ESCAPE_CHAR) { + next(); + if (hasMore()) { + next(); // assuming the string is correct, we're skipping every escapable char, including " + } + } else if (peek() == '"' && peek(1) == '"' && peek(2) == '"') { + skip(3); + return createToken(TokenType.LITERAL, pos); + } else { + next(); + } + } + return null; + } + + private @Nullable Token lexCharacterLiteral(int startPos) { + while (hasMore()) { + char peek = peek(); + if (peek == ESCAPE_CHAR) { + skip(2); + continue; // "unsafe" check, assuming there is a closing ' + } else if (peek == '\'') { + next(); + return createToken(TokenType.LITERAL, startPos); + } + next(); + } + return null; + } + + private Token lexSingleOrDoubleOperator(int startPos, char nextForDouble) { + if (hasMore() && peek() == nextForDouble) { + next(); + } + return createToken(TokenType.OPERATOR, startPos); + } + + private Token lexSingleOrDoubleOperator(int startPos, char... anyNext) { + if (hasMore()) { + char peek = peek(); + for (char c : anyNext) { + if (peek == c) { + next(); + break; + } + } + } + return createToken(TokenType.OPERATOR, startPos); + } + + private void skip(int i) { + this.nextPos += i; + } + + private char peek() { + return peek(0); + } + + private char peek(int offset) { + return this.content[this.nextPos + offset]; + } + + private char next() { + return this.content[this.nextPos++]; + } + + private boolean hasMore() { + return hasMore(0); + } + + private boolean hasMore(int i) { + return this.nextPos + i < this.content.length; + } + + private static boolean isNon(int pos, char[] content) { + int p = pos; + return content.length - p >= 3 && content[p++] == 'n' && content[p++] == 'o' && content[p] == 'n'; + } + + private static boolean isDashSealed(char[] content, int pos) { + int p = pos; + return content[p++] == '-' + && content[p++] == 's' + && content[p++] == 'e' + && content[p++] == 'a' + && content[p++] == 'l' + && content[p++] == 'e' + && content[p] == 'd'; + } + + private boolean skipWhitespaces() { + while (hasMore() && Character.isWhitespace(peek())) { + next(); + } + return hasMore(); + } + + private boolean skipCommentsAndWhitespaces() { + boolean retry; + do { + retry = false; + skipWhitespaces(); + if (hasMore(2) && peek() == '/') { + if (peek(1) == '/') { + skipUntilLineBreak(); + retry = true; + } else if (peek(1) == '*') { + if (!skipUntil("*/")) { + this.nextPos = this.content.length; // comment does not end, but the content does + return false; + } + retry = true; + } + } + } while (retry); + return hasMore(); + } + + int indexOf(char c, int start) { + for (int i = start; i < this.content.length; i++) { + if (this.content[i] == c) { + return i; + } + } + return -1; + } + +} diff --git a/src/main/java/spoon/support/util/internal/lexer/ModifierExtractor.java b/src/main/java/spoon/support/util/internal/lexer/ModifierExtractor.java new file mode 100644 index 00000000000..6c8f064f159 --- /dev/null +++ b/src/main/java/spoon/support/util/internal/lexer/ModifierExtractor.java @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.lexer; + +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.declaration.ModifierKind; +import spoon.support.reflect.CtExtendedModifier; +import spoon.support.util.internal.trie.Trie; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ModifierExtractor { + private static final Trie MODIFIER_TRIE = Trie.ofWords( + Arrays.stream(ModifierKind.values()) + .collect(Collectors.toMap(ModifierKind::toString, Function.identity())) + ); + + /** + * Collects modifiers from the content array into the modifiers map. + * + * @param content the source code to extract modifiers from + * @param start the start offset when searching + * @param end the end offset when searching + * @param modifiers the map to insert the modifier data into + * @param createSourcePosition the function to create a source position object from location data + */ + public void collectModifiers( + char[] content, + int start, + int end, + Map modifiers, + BiFunction createSourcePosition + ) { + JavaLexer lexer = new JavaLexer(content, start, end); + while (!modifiers.isEmpty()) { + Token lex = lexer.lex(); + if (lex == null) { + return; + } + if (lex.type() != TokenType.KEYWORD) { + continue; + } + char[] decodedContent = new CharRemapper(content, lex.start(), lex.end()).remapContent(); + Optional match = MODIFIER_TRIE.findMatch(decodedContent); + if (match.isPresent()) { + CtExtendedModifier modifier = modifiers.remove(match.get()); + if (modifier != null) { + modifier.setPosition(createSourcePosition.apply(lex.start(), lex.end() - 1)); + } + } + } + } +} diff --git a/src/main/java/spoon/support/util/internal/lexer/Token.java b/src/main/java/spoon/support/util/internal/lexer/Token.java new file mode 100644 index 00000000000..9922fe2a9c3 --- /dev/null +++ b/src/main/java/spoon/support/util/internal/lexer/Token.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.lexer; + +/** + * @param start the inclusive start position + * @param end the exclusive end position + */ +public record Token(TokenType type, int start, int end) { + private static final int COLUMN_WIDTH = 8; + + /** + * {@return the number of chars this token covers} + */ + public int length() { + return end - start; + } + + /** + * {@return the value this token represents for the original content} + * + * @param content the original content + */ + public String valueForContent(char[] content) { + return new String(content, this.start, this.end - this.start); + } + + /** + * {@return a formatted string representing this token, given the original content} + * + * @param content the original content + */ + public String formatted(char[] content) { + String type = " ".repeat(10 - this.type.name().length()) + this.type.name(); + String s = String.valueOf(this.start); + String start = padFailsafe(COLUMN_WIDTH, s); + String e = String.valueOf(this.end); + String end = padFailsafe(COLUMN_WIDTH, e); + return "Token[type: " + type + ", start: " + start + ", end: " + end + ", content: " + valueForContent(content); + } + + + private static String padFailsafe(int width, String content) { + int count = width - content.length(); + return " ".repeat(Math.max(0, count)) + content; + } +} diff --git a/src/main/java/spoon/support/util/internal/lexer/TokenType.java b/src/main/java/spoon/support/util/internal/lexer/TokenType.java new file mode 100644 index 00000000000..e5286a95c92 --- /dev/null +++ b/src/main/java/spoon/support/util/internal/lexer/TokenType.java @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.lexer; + +public enum TokenType { + IDENTIFIER, + KEYWORD, + LITERAL, + SEPARATOR, + OPERATOR +} diff --git a/src/main/java/spoon/support/util/internal/trie/SimpleTrie.java b/src/main/java/spoon/support/util/internal/trie/SimpleTrie.java new file mode 100644 index 00000000000..9bf21993bbf --- /dev/null +++ b/src/main/java/spoon/support/util/internal/trie/SimpleTrie.java @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.trie; + +import java.util.Optional; + +record SimpleTrie( + char min, + char max, + Node root +) implements Trie { + + SimpleTrie(char min, char max) { + this(min, max, new Node<>(createFollows(min, max), null)); + } + + @Override + public Optional findMatch(char[] input) { + return findMatch(input, 0, input.length); + } + + @Override + public Optional findMatch(char[] input, int start, int end) { + checkBounds(input.length, start, end); + if (input.length == 0) { + return Optional.empty(); + } + Node current = root; + int i = start; + do { + char c = input[i]; + if (c < min || c > max) { + return Optional.empty(); + } + Node advance = current.advance(c - min); + if (advance == null) { + return Optional.empty(); + } + current = advance; + } while (++i < end); + return Optional.of(current).map(node -> node.value); + } + + private static void checkBounds(int length, int start, int end) { + if (start > end || end > length || start < 0) { + throw new IndexOutOfBoundsException(String.format("array of length %s, start %s, end %s", length, start, end)); + } + } + + void insert(Node prev, String name, T value) { + Node current = prev; + for (int i = 0; i < name.length() - 1; i++) { + int c = name.charAt(i) - min; + Node follow = current.follows[c]; + if (follow == null) { + follow = new Node<>(createFollows(min, max), null); + } + current.follows[c] = follow; + current = follow; + } + Node end = new Node<>(createFollows(min, max), value); + current.follows[name.charAt(name.length() - 1) - min] = end; + } + + @SuppressWarnings("unchecked") + private static Node[] createFollows(char min, char max) { + return new Node[max - min + 1]; + } + + private record Node( + Node[] follows, + T value + ) { + + Node advance(int index) { + return follows[index]; + } + } +} diff --git a/src/main/java/spoon/support/util/internal/trie/Trie.java b/src/main/java/spoon/support/util/internal/trie/Trie.java new file mode 100644 index 00000000000..2d77be07304 --- /dev/null +++ b/src/main/java/spoon/support/util/internal/trie/Trie.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: (MIT OR CECILL-C) + * + * Copyright (C) 2006-2023 INRIA and contributors + * + * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon. + */ +package spoon.support.util.internal.trie; + +import java.util.Arrays; +import java.util.IntSummaryStatistics; +import java.util.Map; +import java.util.Optional; + +import static java.util.Comparator.comparingInt; + +/** + * A stateless and immutable trie that maps strings to values. + * + * @param the value type + */ +public interface Trie { + + /** + * Creates a new immutable trie with the given mappings + * + * @param words the mappings + * @param the value type + * @return a new trie + */ + static Trie ofWords(Map words) { + IntSummaryStatistics statistics = words.keySet().stream() + .flatMapToInt(String::chars) + .summaryStatistics(); + char min = (char) statistics.getMin(); + char max = (char) statistics.getMax(); + String[] wordArray = words.keySet().toArray(String[]::new); + Arrays.sort(wordArray, comparingInt(String::length)); + SimpleTrie trie = new SimpleTrie<>(min, max); + for (String word : wordArray) { + trie.insert(trie.root(), word, words.get(word)); + } + return trie; + } + + /** + * Finds the value for the given char array. + * + * @param input the array representing the key. + * @return the value mapped to by the given input. + */ + default Optional findMatch(char[] input) { + return findMatch(input, 0, input.length); + } + + /** + * Finds the value for the range of given char array. + * + * @param input the array representing the key + * @param start the start offset of the range + * @param end the end offset of the range + * @return the value mapped to by the given input + */ + Optional findMatch(char[] input, int start, int end); +} diff --git a/src/test/java/spoon/reflect/ast/AstCheckerTest.java b/src/test/java/spoon/reflect/ast/AstCheckerTest.java index da7b5d51450..a3dab95eec4 100644 --- a/src/test/java/spoon/reflect/ast/AstCheckerTest.java +++ b/src/test/java/spoon/reflect/ast/AstCheckerTest.java @@ -116,6 +116,7 @@ public void testExecutableReference() { public void testAvoidSetCollectionSavedOnAST() { final Launcher launcher = new Launcher(); launcher.getEnvironment().setNoClasspath(true); + launcher.getEnvironment().setComplianceLevel(17); launcher.addInputResource("src/main/java"); launcher.buildModel(); diff --git a/src/test/java/spoon/support/util/internal/lexer/JavaLexerTest.java b/src/test/java/spoon/support/util/internal/lexer/JavaLexerTest.java new file mode 100644 index 00000000000..1eb5e7b1274 --- /dev/null +++ b/src/test/java/spoon/support/util/internal/lexer/JavaLexerTest.java @@ -0,0 +1,158 @@ +package spoon.support.util.internal.lexer; + +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 static org.assertj.core.api.Assertions.assertThat; + +public class JavaLexerTest { + + @ParameterizedTest + @ValueSource(strings = { + "\"Simple\"", + "\"\\\\\"", + "\"\\n\"", + "\"\"", + "\"\"\"\n\\\"\"\"\n\"\"\"", + "\"\"\"\n\\\"\"\"\n \"\"\"", + }) + void testEscapedString(String raw) { + // contract: escaping in string literals/text blocks is handled correctly + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first.type()).isEqualTo(TokenType.LITERAL); + assertThat(first.end()).isEqualTo(raw.length()); + assertThat(javaLexer.lex()).isNull(); // EOF + } + + @ParameterizedTest + @ValueSource(strings = { + "0", + "00", + "10", + "0x0", + "0X0", + "0xF", + "0xF", + "0X.0p+0f", + "0X1f.0eEfaP+0123d", + "0b0L", + "1f", + "1D", + }) + void testNumericLiterals(String raw) { + // contract: numeric literals are lexed correctly + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first.type()).isEqualTo(TokenType.LITERAL); + assertThat(first.end()).isEqualTo(raw.length()); + assertThat(javaLexer.lex()).isNull(); // EOF + } + + @ParameterizedTest + @CsvSource(textBlock = """ + non,1 + non-,2 + non-se,3 + non-sea,3 + non-seal,3 + non-seale,3 + non-sealef,3 + non-sealed2,3 + """) + void testNonSealedInvalid(String raw, int tokens) { + // contract: almost "non-sealed" falls back correctly to multiple tokens + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first.type()).isEqualTo(TokenType.IDENTIFIER); + Token second = javaLexer.lex(); + if (tokens == 1) { + assertThat(second).isNull(); + return; + } + assertThat(second.type()).isEqualTo(TokenType.OPERATOR); + Token third = javaLexer.lex(); + if (tokens == 2) { + assertThat(third).isNull(); + return; + } + assertThat(third.type()).isEqualTo(TokenType.IDENTIFIER); + assertThat(javaLexer.lex()).isNull(); + } + + @Test + void testNonSealedSeparatedByWhitespace() { + // contract: white spaces are significant in "non-sealed" keyword + String raw = "non - sealed"; + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first.type()).isEqualTo(TokenType.IDENTIFIER); + Token second = javaLexer.lex(); + assertThat(second).isNotNull(); + assertThat(second.type()).isEqualTo(TokenType.OPERATOR); + Token third = javaLexer.lex(); + assertThat(third).isNotNull(); + // sealed is a keyword in this case + assertThat(third.type()).isEqualTo(TokenType.KEYWORD); + assertThat(javaLexer.lex()).isNull(); + } + + @ParameterizedTest + @ValueSource(strings = { + "non-sealed", + "non-sealed ?", + "non-sealed?", + "non-sealed-", + }) + void testNonSealed(String raw) { + // contract: "non-sealed" with separators/operators is still "non-sealed" + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first.type()).isEqualTo(TokenType.KEYWORD); + } + + @ParameterizedTest + @ValueSource(strings = { + ">", + "<", + ">>", + "<<", + ">>>", + ">=", + "<=", + ">>=", + "<<=", + ">>>=", + "->" + }) + void testValidAngleBracketOperators(String raw) { + // contract: variants of operations including brackets are lexed correctly + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first).isNotNull(); + assertThat(first.type()).isEqualTo(TokenType.OPERATOR); + assertThat(javaLexer.lex()).isNull(); + } + + @ParameterizedTest + @ValueSource(strings = { + ">>>>", + "<<<", + ">>>>=", + "<<<=", + "->>" + }) + void testInvalidAngleBracketOperators(String raw) { + // contract: too many angle brackets result in multiple operators + JavaLexer javaLexer = new JavaLexer(raw.toCharArray(), 0, raw.length()); + Token first = javaLexer.lex(); + assertThat(first).isNotNull(); + assertThat(first.type()).isEqualTo(TokenType.OPERATOR); + Token second = javaLexer.lex(); + assertThat(second).isNotNull(); + assertThat(second.type()).isEqualTo(TokenType.OPERATOR); + assertThat(javaLexer.lex()).isNull(); + } +} diff --git a/src/test/java/spoon/support/util/internal/lexer/ModifierExtractorTest.java b/src/test/java/spoon/support/util/internal/lexer/ModifierExtractorTest.java new file mode 100644 index 00000000000..8f6663d44b5 --- /dev/null +++ b/src/test/java/spoon/support/util/internal/lexer/ModifierExtractorTest.java @@ -0,0 +1,244 @@ +package spoon.support.util.internal.lexer; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import spoon.reflect.cu.CompilationUnit; +import spoon.reflect.cu.SourcePosition; +import spoon.reflect.declaration.ModifierKind; +import spoon.support.reflect.CtExtendedModifier; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class ModifierExtractorTest { + private static final Map LOOKUP = Arrays.stream(ModifierKind.values()) + .collect(Collectors.toMap(ModifierKind::toString, Function.identity())); + private static final List DELIMITER = List.of( + " ", + "(", ")", + "[", "]", + "{", "}", + ";", ",", + ".", "...", + "@", + "::", + "=", ">", "<", "!", "~", "?", ":", "->", + "==", ">=", "<=", "!=", "&&", "||", "++", "--", + "+", "-", "*", "/", "&", "|", "^", "%", "<<", ">>", ">>>", + "+=", "-=", "*=", "/=", "&=", "|=", "^=", "%=", "<<=", ">>=", ">>>=" + ); + + @ParameterizedTest + @MethodSource("modifiers") + void testSingleModifier(String input) { + // arrange + ModifierExtractor extractor = new ModifierExtractor(); + char[] chars = input.toCharArray(); + ModifierKind kind = LOOKUP.get(input); + CtExtendedModifier extended = CtExtendedModifier.explicit(kind); + Map modifier = new HashMap<>(Map.of( + kind, extended + )); + + // act + extractor.collectModifiers(chars, 0, chars.length, modifier, SimpleSourcePosition::new); + + // assert + assertThat(modifier).isEmpty(); + assertThat(extended.getPosition()).isEqualTo(new SimpleSourcePosition(0, input.length() - 1)); + } + + static Stream modifiers() { + return Arrays.stream(ModifierKind.values()) + .map(ModifierKind::toString) + .map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("mixedContents") + void testMixedDelimitedModifiers(String input) { + // arrange + ModifierExtractor extractor = new ModifierExtractor(); + char[] chars = input.toCharArray(); + Map allModifiers = Arrays.stream(ModifierKind.values()) + .collect(Collectors.toMap(Function.identity(), CtExtendedModifier::explicit)); + + // act + extractor.collectModifiers(chars, 0, input.length(), allModifiers, SimpleSourcePosition::new); + + // assert + assertThat(allModifiers).isEmpty(); + } + + static Stream mixedContents() { + Random random = new Random(42); + return Stream.generate(() -> { + StringBuilder builder = new StringBuilder(); + includeAllModifiers(builder, random); + return builder.toString(); + }) + .filter(string -> !(string.contains("//") || string.contains("/*"))) // oops, we created a comment + .limit(10) + .map(Arguments::of); + + } + + static void includeAllModifiers(StringBuilder builder, Random random) { + List modifiers = Arrays.asList(ModifierKind.values()); + Collections.shuffle(modifiers, random); + addDelimiters(builder, random); + for (ModifierKind modifier : modifiers) { + builder.append(modifier); + addDelimiters(builder, random); + } + } + + static void addDelimiters(StringBuilder builder, Random random) { + int count = random.nextInt(2) + 1; // [1, 3) + for (int i = 0; i < count; i++) { + builder.append(DELIMITER.get(random.nextInt(DELIMITER.size()))); + } + } + + @ParameterizedTest + @ValueSource(strings = { + "/**/public", + "public/**/", + "/**/public/**/", + "public/*final*/", + "public//final", + "//\npublic", + }) + void testWithComments(String input) { + // arrange + ModifierExtractor extractor = new ModifierExtractor(); + char[] chars = input.toCharArray(); + Map allModifiers = Arrays.stream(ModifierKind.values()) + .collect(Collectors.toMap(Function.identity(), CtExtendedModifier::explicit)); + + // act + extractor.collectModifiers(chars, 0, input.length(), allModifiers, SimpleSourcePosition::new); + + // assert + assertThat(allModifiers) + .doesNotContainKeys(ModifierKind.PUBLIC) // public should be removed + .containsKey(ModifierKind.FINAL); // final shouldn't be removed + } + + @ParameterizedTest + @ValueSource(strings = { + "try", + "synchronize", // not synchronized + "class", + "int", + "apublic", + "publica", + "non", + }) + void testNonModifiers(String input) { + // arrange + ModifierExtractor extractor = new ModifierExtractor(); + char[] chars = input.toCharArray(); + Map allModifiers = Arrays.stream(ModifierKind.values()) + .collect(Collectors.toMap(Function.identity(), CtExtendedModifier::explicit)); + List foundStartPositions = new ArrayList<>(); + BiFunction createAndAdd = (start, end) -> { + SimpleSourcePosition position = new SimpleSourcePosition(start, end); + foundStartPositions.add(position); + return position; + }; + + // act + extractor.collectModifiers(chars, 0, input.length(), allModifiers, createAndAdd); + + // assert + assertThat(foundStartPositions).isEmpty(); + } + + static class SimpleSourcePosition implements SourcePosition { + private final int start; + private final int end; + + SimpleSourcePosition(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimpleSourcePosition that = (SimpleSourcePosition) o; + return start == that.start && end == that.end; + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + // just ignore all these methods + + @Override + public boolean isValidPosition() { + return false; + } + + @Override + public File getFile() { + return null; + } + + @Override + public CompilationUnit getCompilationUnit() { + return null; + } + + @Override + public int getLine() { + return 0; + } + + @Override + public int getEndLine() { + return 0; + } + + @Override + public int getColumn() { + return 0; + } + + @Override + public int getEndColumn() { + return 0; + } + + @Override + public int getSourceEnd() { + return 0; + } + + @Override + public int getSourceStart() { + return 0; + } + } + +} diff --git a/src/test/java/spoon/test/api/APITest.java b/src/test/java/spoon/test/api/APITest.java index 2ae9f745d55..9e2a8364506 100644 --- a/src/test/java/spoon/test/api/APITest.java +++ b/src/test/java/spoon/test/api/APITest.java @@ -273,6 +273,7 @@ public void testPrintNotAllSourcesInCommandLine() { launcher.run(new String[] { "-i", "./src/main/java", // "-o", "./target/print-not-all/command", // + "--compliance", "17", // "-f", "spoon.Launcher:spoon.template.AbstractTemplate" }); diff --git a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java index b30e0a13609..ec1f519f777 100644 --- a/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java +++ b/src/test/java/spoon/test/architecture/SpoonArchitectureEnforcerTest.java @@ -78,7 +78,7 @@ public class SpoonArchitectureEnforcerTest { @BeforeAll static void beforeAll() { Launcher launcher = new Launcher(); - launcher.getEnvironment().setComplianceLevel(11); + launcher.getEnvironment().setComplianceLevel(17); launcher.addInputResource("src/main/java/"); spoonSrcMainModel = launcher.buildModel(); spoonSrcMainFactory = launcher.getFactory(); @@ -341,6 +341,7 @@ public boolean matches(CtClass element) { public void testInterfacesAreCtScannable() { // contract: all non-leaf interfaces of the metamodel should be visited by CtInheritanceScanner Launcher interfaces = new Launcher(); + interfaces.getEnvironment().setComplianceLevel(17); interfaces.addInputResource("src/main/java/spoon/support"); interfaces.addInputResource("src/main/java/spoon/reflect/declaration"); interfaces.addInputResource("src/main/java/spoon/reflect/code"); @@ -424,6 +425,8 @@ public void testSpecPackage() { officialPackages.add("spoon.support.template"); officialPackages.add("spoon.support.util"); officialPackages.add("spoon.support.util.internal"); + officialPackages.add("spoon.support.util.internal.lexer"); + officialPackages.add("spoon.support.util.internal.trie"); officialPackages.add("spoon.support.visitor.clone"); officialPackages.add("spoon.support.visitor.equals"); officialPackages.add("spoon.support.visitor.java.internal"); diff --git a/src/test/java/spoon/test/imports/ImportScannerTest.java b/src/test/java/spoon/test/imports/ImportScannerTest.java index 1a9525fa5ed..db75be07433 100644 --- a/src/test/java/spoon/test/imports/ImportScannerTest.java +++ b/src/test/java/spoon/test/imports/ImportScannerTest.java @@ -56,7 +56,7 @@ public class ImportScannerTest { - @ModelTest("./src/main/java/spoon/") + @ModelTest(value = "./src/main/java/spoon/", complianceLevel = 17) public void testImportOnSpoon(Launcher launcher, Factory factory, CtModel model) throws IOException { File targetDir = new File("./target/import-test"); launcher.getEnvironment().setSourceOutputDirectory(targetDir); diff --git a/src/test/java/spoon/test/imports/ImportTest.java b/src/test/java/spoon/test/imports/ImportTest.java index 2e0be0a815d..1ba60269d65 100644 --- a/src/test/java/spoon/test/imports/ImportTest.java +++ b/src/test/java/spoon/test/imports/ImportTest.java @@ -1575,7 +1575,7 @@ public void testFQNJavadoc() { } } - @ModelTest(value = "./src/main/java/spoon/", autoImport = true) + @ModelTest(value = "./src/main/java/spoon/", autoImport = true, complianceLevel = 17) public void testImportOnSpoon(Launcher launcher, CtModel model, Factory factory) throws IOException { PrettyPrinter prettyPrinter = new DefaultJavaPrettyPrinter(launcher.getEnvironment()); diff --git a/src/test/java/spoon/test/intercession/IntercessionTest.java b/src/test/java/spoon/test/intercession/IntercessionTest.java index 9d5477121af..6277b8ca0f9 100644 --- a/src/test/java/spoon/test/intercession/IntercessionTest.java +++ b/src/test/java/spoon/test/intercession/IntercessionTest.java @@ -207,6 +207,7 @@ public void testSettersAreAllGood() { } final Launcher launcher = new Launcher(); + launcher.getEnvironment().setComplianceLevel(17); launcher.addInputResource("./src/main/java/spoon/reflect/"); launcher.addInputResource("./src/main/java/spoon/support/"); launcher.getModelBuilder().setSourceClasspath((String[]) classpath.toArray(new String[]{})); diff --git a/src/test/java/spoon/test/sourcePosition/SourcePositionTest.java b/src/test/java/spoon/test/sourcePosition/SourcePositionTest.java index 252f3883e21..d5758e96fe1 100644 --- a/src/test/java/spoon/test/sourcePosition/SourcePositionTest.java +++ b/src/test/java/spoon/test/sourcePosition/SourcePositionTest.java @@ -20,12 +20,11 @@ import java.util.List; import org.junit.jupiter.api.Test; -import spoon.Launcher; +import org.junit.jupiter.api.function.Executable; import spoon.reflect.CtModel; import spoon.reflect.code.CtInvocation; import spoon.reflect.cu.CompilationUnit; import spoon.reflect.cu.SourcePosition; -import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtModifiable; import spoon.reflect.declaration.CtType; @@ -34,7 +33,6 @@ import spoon.reflect.reference.CtTypeReference; import spoon.reflect.visitor.Filter; import spoon.reflect.visitor.filter.TypeFilter; -import spoon.support.reflect.CtExtendedModifier; import spoon.support.reflect.cu.CompilationUnitImpl; import spoon.support.reflect.cu.position.BodyHolderSourcePositionImpl; import spoon.support.reflect.cu.position.DeclarationSourcePositionImpl; @@ -44,6 +42,8 @@ import spoon.testing.utils.ModelTest; import spoon.testing.utils.ModelUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -77,7 +77,7 @@ public void equalPositionsHaveSameHashcode() throws Exception { private Factory factoryFor(String packageName, String className) throws Exception { return build(packageName, className).getFactory(); } - + @Test public void testSourcePositionOfSecondPrimitiveType() throws Exception { /* @@ -127,6 +127,25 @@ public String getOriginalSourceCode() { public void testSourcePositionWhenCommentInAnnotation(CtModel model) { // contract: comment characters as element values in annotations should not break position assignment to modifiers List list = model.getElements(new TypeFilter<>(CtClassImpl.class)); - assertEquals(4,list.get(0).getPosition().getLine()); + assertEquals(4, list.get(0).getPosition().getLine()); + } + + @ModelTest( + value = "./src/test/resources/spoon/test/sourcePosition/ModifierSourcePositions.java", + complianceLevel = 17 + ) + void testModifiersHaveSourcePosition(CtModel model) { + // contract: all explicit modifiers should have a valid position + Executable[] executables = model.getElements(new TypeFilter<>(CtModifiable.class)).stream() + .flatMap(modifiable -> modifiable.getExtendedModifiers().stream()) + .filter(modifier -> !modifier.isImplicit()) + .map(mod -> (Executable) () -> assertThat(mod).matches( + m -> m.getPosition().isValidPosition(), + "is valid source position" + ) + ) + .toArray(Executable[]::new); + assertEquals(8, executables.length, "unexpected length of modifiers found"); + assertAll(executables); } } \ No newline at end of file diff --git a/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java b/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java index f1b29a1ce42..9316c6acc42 100644 --- a/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java +++ b/src/test/java/spoon/testing/assertions/codegen/AssertJCodegen.java @@ -14,6 +14,7 @@ import org.assertj.core.api.ListAssert; import org.assertj.core.api.MapAssert; import org.assertj.core.api.ObjectAssert; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import spoon.Launcher; @@ -79,6 +80,7 @@ record AssertModelPair(CtClass assertClass, CtInterface modelInterface) { } @Test + @Disabled @Tag("codegen") void generateCode() throws IOException { Launcher launcher = new Launcher(); diff --git a/src/test/resources/spoon/test/sourcePosition/ModifierSourcePositions.java b/src/test/resources/spoon/test/sourcePosition/ModifierSourcePositions.java new file mode 100644 index 00000000000..5cd163f2cd2 --- /dev/null +++ b/src/test/resources/spoon/test/sourcePosition/ModifierSourcePositions.java @@ -0,0 +1,17 @@ +package spoon.test.sourcePosition; + +import org.jspecify.annotations.Nullable; + +import java.nio.file.Files; + +class ModifierSourcePositions { + public@Deprecated /**/static void method(final@Nullable AutoCloseable o) { + final@Nullable String s; + fin\u0061l@Nullable String s; + try(final var var = Files.newBufferedReader(null);final var v2 = o) { + + } catch (final Exception e) { + + } + } +}