From 50ea1053c998c039bfc976e4f43c05c19d91a191 Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Thu, 14 Mar 2024 12:42:27 +1100 Subject: [PATCH 1/4] Add support for `TEXTBLOCK` directives Closes #101 --- CHANGELOG.md | 5 ++ .../antlr/ast/node/TextLiteralNodeImpl.java | 18 +++++- .../delphi/file/DefaultDelphiFile.java | 12 ++++ .../integradev/delphi/file/DelphiFile.java | 5 ++ .../preprocessor/DelphiPreprocessor.java | 36 +++++++++++ .../preprocessor/TextBlockLineEndingMode.java | 25 ++++++++ .../TextBlockLineEndingModeRegistry.java | 64 +++++++++++++++++++ .../CompilerDirectiveParserImpl.java | 13 ++++ .../directive/TextBlockDirectiveImpl.java | 42 ++++++++++++ .../api/directive/ParameterDirective.java | 3 +- .../api/directive/TextBlockDirective.java | 30 +++++++++ .../ast/node/TextLiteralNodeImplTest.java | 18 +++++- .../TextBlockLineEndingModeRegistryTest.java | 48 ++++++++++++++ .../CompilerDirectiveParserTest.java | 25 ++++++++ 14 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingMode.java create mode 100644 delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistry.java create mode 100644 delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/TextBlockDirectiveImpl.java create mode 100644 delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/TextBlockDirective.java create mode 100644 delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c242c3e3c..f873d427b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Support for the `TEXTBLOCK` directive. +- **API:** `CompilerDirectiveParser` can now return a new `TextBlockDirective` type. + ### Changed - Performance improvements. diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImpl.java index 1ff4f4cd1..22682121d 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImpl.java @@ -19,6 +19,7 @@ package au.com.integradev.delphi.antlr.ast.node; import au.com.integradev.delphi.antlr.ast.visitors.DelphiParserVisitor; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingMode; import java.util.ArrayDeque; import java.util.Deque; import java.util.stream.Collectors; @@ -104,9 +105,24 @@ private String createMultilineValue() { String last = lines.removeLast(); String indentation = readLeadingWhitespace(last); + var registry = getAst().getDelphiFile().getTextBlockLineEndingModeRegistry(); + TextBlockLineEndingMode lineEndingMode = registry.getLineEndingMode(getTokenIndex()); + String lineEnding; + + switch (lineEndingMode) { + case CR: + lineEnding = "\r"; + break; + case LF: + lineEnding = "\n"; + break; + default: + lineEnding = "\r\n"; + } + return lines.stream() .map(line -> StringUtils.removeStart(line, indentation)) - .collect(Collectors.joining("\n")); + .collect(Collectors.joining(lineEnding)); } private static String readLeadingWhitespace(String input) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DefaultDelphiFile.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DefaultDelphiFile.java index 5c215c184..134e2b690 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DefaultDelphiFile.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DefaultDelphiFile.java @@ -19,6 +19,7 @@ package au.com.integradev.delphi.file; import au.com.integradev.delphi.preprocessor.CompilerSwitchRegistry; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingModeRegistry; import java.io.File; import java.util.List; import org.sonar.plugins.communitydelphi.api.ast.DelphiAst; @@ -32,6 +33,7 @@ class DefaultDelphiFile implements DelphiFile { private List tokens; private List comments; private CompilerSwitchRegistry switchRegistry; + private TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry; private TypeFactory typeFactory; DefaultDelphiFile() { @@ -68,6 +70,11 @@ public CompilerSwitchRegistry getCompilerSwitchRegistry() { return switchRegistry; } + @Override + public TextBlockLineEndingModeRegistry getTextBlockLineEndingModeRegistry() { + return textBlockLineEndingModeRegistry; + } + @Override public TypeFactory getTypeFactory() { return typeFactory; @@ -97,6 +104,11 @@ void setCompilerSwitchRegistry(CompilerSwitchRegistry switchRegistry) { this.switchRegistry = switchRegistry; } + void setTextBlockLineEndingModeRegistry( + TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry) { + this.textBlockLineEndingModeRegistry = textBlockLineEndingModeRegistry; + } + void setTypeFactory(TypeFactory typeFactory) { this.typeFactory = typeFactory; } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DelphiFile.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DelphiFile.java index ea074f314..9ace8efdc 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DelphiFile.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/file/DelphiFile.java @@ -30,6 +30,7 @@ import au.com.integradev.delphi.preprocessor.CompilerSwitchRegistry; import au.com.integradev.delphi.preprocessor.DelphiPreprocessor; import au.com.integradev.delphi.preprocessor.DelphiPreprocessorFactory; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingModeRegistry; import au.com.integradev.delphi.preprocessor.search.SearchPath; import au.com.integradev.delphi.utils.DelphiUtils; import java.io.File; @@ -61,6 +62,8 @@ public interface DelphiFile { CompilerSwitchRegistry getCompilerSwitchRegistry(); + TextBlockLineEndingModeRegistry getTextBlockLineEndingModeRegistry(); + TypeFactory getTypeFactory(); interface DelphiInputFile extends DelphiFile { @@ -143,6 +146,8 @@ private static void setupFile( delphiFile.setTypeFactory(config.getTypeFactory()); delphiFile.setAst(createAST(delphiFile, preprocessor.getTokenStream(), config)); delphiFile.setCompilerSwitchRegistry(preprocessor.getCompilerSwitchRegistry()); + delphiFile.setTextBlockLineEndingModeRegistry( + preprocessor.getTextBlockLineEndingModeRegistry()); delphiFile.setSourceCodeLines(readLines(sourceFile, fileStream.getEncoding())); delphiFile.setTokens(preprocessor.getRawTokens()); delphiFile.setComments(extractComments(delphiFile.getTokens())); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java index 7d4ee67bf..ec22eb0eb 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java @@ -54,6 +54,7 @@ import org.sonar.plugins.communitydelphi.api.directive.CompilerDirectiveParser; import org.sonar.plugins.communitydelphi.api.directive.ConditionalDirective; import org.sonar.plugins.communitydelphi.api.directive.SwitchDirective.SwitchKind; +import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective.LineEndingKind; import org.sonar.plugins.communitydelphi.api.token.DelphiToken; import org.sonar.plugins.communitydelphi.api.type.TypeFactory; @@ -67,6 +68,7 @@ public class DelphiPreprocessor { private final Deque parentDirective; private final Map currentSwitches; private final CompilerSwitchRegistry switchRegistry; + private final TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry; private final boolean processingIncludeFile; private DelphiTokenStream tokenStream; @@ -82,6 +84,10 @@ public class DelphiPreprocessor { caseInsensitiveSet(config.getDefinitions()), new EnumMap<>(SwitchKind.class), new CompilerSwitchRegistry(), + new TextBlockLineEndingModeRegistry( + platform == Platform.WINDOWS + ? TextBlockLineEndingMode.CRLF + : TextBlockLineEndingMode.LF), 0, false); } @@ -93,12 +99,14 @@ private DelphiPreprocessor( Set definitions, Map currentSwitches, CompilerSwitchRegistry switchRegistry, + TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry, int tokenIndexStart, boolean processingIncludeFile) { this.lexer = lexer; this.config = config; this.platform = platform; this.switchRegistry = switchRegistry; + this.textBlockLineEndingModeRegistry = textBlockLineEndingModeRegistry; this.definitions = definitions; this.directives = new ArrayList<>(); this.parentDirective = new ArrayDeque<>(); @@ -239,6 +247,7 @@ private List processIncludeFile(String filename, Path includePath, Delphi definitions, currentSwitches, switchRegistry, + textBlockLineEndingModeRegistry, location.getIndex(), true); @@ -309,6 +318,29 @@ private void registerCurrentCompilerSwitches() { } } + public void handleTextBlock(LineEndingKind lineEndingKind, int tokenIndex) { + TextBlockLineEndingMode lineEndingMode; + switch (lineEndingKind) { + case CR: + lineEndingMode = TextBlockLineEndingMode.CR; + break; + case LF: + lineEndingMode = TextBlockLineEndingMode.LF; + break; + case CRLF: + lineEndingMode = TextBlockLineEndingMode.CRLF; + break; + default: + lineEndingMode = nativeLineEnding(); + break; + } + textBlockLineEndingModeRegistry.registerLineEndingMode(lineEndingMode, tokenIndex); + } + + private TextBlockLineEndingMode nativeLineEnding() { + return platform == Platform.WINDOWS ? TextBlockLineEndingMode.CRLF : TextBlockLineEndingMode.LF; + } + public DelphiTokenStream getTokenStream() { return tokenStream; } @@ -317,6 +349,10 @@ public CompilerSwitchRegistry getCompilerSwitchRegistry() { return switchRegistry; } + public TextBlockLineEndingModeRegistry getTextBlockLineEndingModeRegistry() { + return textBlockLineEndingModeRegistry; + } + public TypeFactory getTypeFactory() { return config.getTypeFactory(); } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingMode.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingMode.java new file mode 100644 index 000000000..b0e88d512 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingMode.java @@ -0,0 +1,25 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.preprocessor; + +public enum TextBlockLineEndingMode { + CR, + LF, + CRLF +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistry.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistry.java new file mode 100644 index 000000000..4df2e95c4 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistry.java @@ -0,0 +1,64 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.preprocessor; + +import java.util.ArrayList; +import java.util.List; + +public class TextBlockLineEndingModeRegistry { + private final TextBlockLineEndingMode initialLineEndingMode; + private final List registrations; + + TextBlockLineEndingModeRegistry(TextBlockLineEndingMode initialLineEndingMode) { + this.initialLineEndingMode = initialLineEndingMode; + this.registrations = new ArrayList<>(); + } + + void registerLineEndingMode(TextBlockLineEndingMode lineEndingMode, int startIndex) { + registrations.add(new LineEndingModeRegistration(lineEndingMode, startIndex)); + } + + public TextBlockLineEndingMode getLineEndingMode(int tokenIndex) { + for (int i = registrations.size() - 1; i >= 0; --i) { + LineEndingModeRegistration registration = registrations.get(i); + if (tokenIndex >= registration.getStartIndex()) { + return registration.getLineEndingMode(); + } + } + return initialLineEndingMode; + } + + private static final class LineEndingModeRegistration { + private final TextBlockLineEndingMode lineEndingMode; + private final int startIndex; + + public LineEndingModeRegistration(TextBlockLineEndingMode lineEndingMode, int startIndex) { + this.lineEndingMode = lineEndingMode; + this.startIndex = startIndex; + } + + public TextBlockLineEndingMode getLineEndingMode() { + return lineEndingMode; + } + + public int getStartIndex() { + return startIndex; + } + } +} diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java index 0ac0a56dd..d066544cf 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java @@ -38,6 +38,8 @@ import org.sonar.plugins.communitydelphi.api.directive.ParameterDirective.ParameterKind; import org.sonar.plugins.communitydelphi.api.directive.ResourceDirective; import org.sonar.plugins.communitydelphi.api.directive.SwitchDirective.SwitchKind; +import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective; +import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective.LineEndingKind; import org.sonar.plugins.communitydelphi.api.directive.WarnDirective; import org.sonar.plugins.communitydelphi.api.directive.WarnDirective.WarnParameterValue; import org.sonar.plugins.communitydelphi.api.token.DelphiToken; @@ -121,6 +123,8 @@ private CompilerDirective createDirective(String name) { return createResourceDirective(); case WARN: return createWarnDirective(); + case TEXTBLOCK: + return createTextBlockDirective(); default: return new ParameterDirectiveImpl(token, kind); } @@ -200,6 +204,15 @@ private WarnDirective createWarnDirective() { return new WarnDirectiveImpl(token, identifier, value); } + private TextBlockDirective createTextBlockDirective() { + String parameter = readDirectiveParameter(); + LineEndingKind lineEndingKind = EnumUtils.getEnumIgnoreCase(LineEndingKind.class, parameter); + if (lineEndingKind == null) { + return null; + } + return new TextBlockDirectiveImpl(token, lineEndingKind); + } + private IfOptDirective createIfOptDirective() { char character = currentChar(); while (Character.isWhitespace(character)) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/TextBlockDirectiveImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/TextBlockDirectiveImpl.java new file mode 100644 index 000000000..19f691394 --- /dev/null +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/TextBlockDirectiveImpl.java @@ -0,0 +1,42 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.preprocessor.directive; + +import au.com.integradev.delphi.preprocessor.DelphiPreprocessor; +import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective; +import org.sonar.plugins.communitydelphi.api.token.DelphiToken; + +public class TextBlockDirectiveImpl extends ParameterDirectiveImpl implements TextBlockDirective { + private final LineEndingKind lineEndingKind; + + TextBlockDirectiveImpl(DelphiToken token, LineEndingKind lineEndingKind) { + super(token, ParameterKind.TEXTBLOCK); + this.lineEndingKind = lineEndingKind; + } + + @Override + public LineEndingKind getLineEndingKind() { + return lineEndingKind; + } + + @Override + public void execute(DelphiPreprocessor preprocessor) { + preprocessor.handleTextBlock(lineEndingKind, getToken().getIndex()); + } +} diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/ParameterDirective.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/ParameterDirective.java index 9a8ce5dce..9f54d646c 100644 --- a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/ParameterDirective.java +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/ParameterDirective.java @@ -55,7 +55,8 @@ enum ParameterKind { RESOURCE("resource", 'r'), RTTI("rtti"), UNDEF("undef"), - WARN("warn"); + WARN("warn"), + TEXTBLOCK("textblock"); private final String name; private final String shortName; diff --git a/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/TextBlockDirective.java b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/TextBlockDirective.java new file mode 100644 index 000000000..98d836a21 --- /dev/null +++ b/delphi-frontend/src/main/java/org/sonar/plugins/communitydelphi/api/directive/TextBlockDirective.java @@ -0,0 +1,30 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.communitydelphi.api.directive; + +public interface TextBlockDirective extends ParameterDirective { + enum LineEndingKind { + NATIVE, + CR, + LF, + CRLF + } + + LineEndingKind getLineEndingKind(); +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImplTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImplTest.java index e2a3ed0a8..0ffa82f5a 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImplTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/antlr/ast/node/TextLiteralNodeImplTest.java @@ -19,8 +19,15 @@ package au.com.integradev.delphi.antlr.ast.node; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import au.com.integradev.delphi.antlr.DelphiLexer; +import au.com.integradev.delphi.antlr.ast.DelphiAstImpl; +import au.com.integradev.delphi.file.DelphiFile; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingMode; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingModeRegistry; import org.antlr.runtime.CommonToken; import org.junit.jupiter.api.Test; import org.sonar.plugins.communitydelphi.api.ast.DelphiNode; @@ -35,11 +42,20 @@ void testMultilineImage() { + " Baz\n" + " '''"; + TextBlockLineEndingModeRegistry registry = mock(); + when(registry.getLineEndingMode(anyInt())).thenReturn(TextBlockLineEndingMode.CRLF); + + DelphiFile delphiFile = mock(); + when(delphiFile.getTextBlockLineEndingModeRegistry()).thenReturn(registry); + TextLiteralNodeImpl node = new TextLiteralNodeImpl(DelphiLexer.TkTextLiteral); + node.setParent(new DelphiAstImpl(delphiFile, mock())); node.addChild(createNode(DelphiLexer.TkMultilineString, image)); assertThat(node.getImage()).isEqualTo(image); - assertThat(node.getValue()).isEqualTo(node.getImageWithoutQuotes()).isEqualTo("Foo\nBar\nBaz"); + assertThat(node.getValue()) + .isEqualTo(node.getImageWithoutQuotes()) + .isEqualTo("Foo\r\nBar\r\nBaz"); assertThat(node.isMultiline()).isTrue(); } diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistryTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistryTest.java new file mode 100644 index 000000000..9035cf200 --- /dev/null +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/TextBlockLineEndingModeRegistryTest.java @@ -0,0 +1,48 @@ +/* + * Sonar Delphi Plugin + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package au.com.integradev.delphi.preprocessor; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class TextBlockLineEndingModeRegistryTest { + @ParameterizedTest + @EnumSource(TextBlockLineEndingMode.class) + void testEmptyRegistryShouldReturnInitialLineEndingMode(TextBlockLineEndingMode mode) { + var registry = new TextBlockLineEndingModeRegistry(mode); + assertThat(registry.getLineEndingMode(123)).isEqualTo(mode); + } + + @Test + void testGetLineEndingMode() { + var registry = new TextBlockLineEndingModeRegistry(TextBlockLineEndingMode.CRLF); + + registry.registerLineEndingMode(TextBlockLineEndingMode.CR, 2); + registry.registerLineEndingMode(TextBlockLineEndingMode.LF, 4); + + assertThat(registry.getLineEndingMode(1)).isEqualTo(TextBlockLineEndingMode.CRLF); + assertThat(registry.getLineEndingMode(2)).isEqualTo(TextBlockLineEndingMode.CR); + assertThat(registry.getLineEndingMode(3)).isEqualTo(TextBlockLineEndingMode.CR); + assertThat(registry.getLineEndingMode(4)).isEqualTo(TextBlockLineEndingMode.LF); + assertThat(registry.getLineEndingMode(5)).isEqualTo(TextBlockLineEndingMode.LF); + } +} diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java index 2435837c2..3e162363c 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java @@ -43,6 +43,8 @@ import org.sonar.plugins.communitydelphi.api.directive.ResourceDirective; import org.sonar.plugins.communitydelphi.api.directive.SwitchDirective; import org.sonar.plugins.communitydelphi.api.directive.SwitchDirective.SwitchKind; +import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective; +import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective.LineEndingKind; import org.sonar.plugins.communitydelphi.api.directive.UndefineDirective; import org.sonar.plugins.communitydelphi.api.token.DelphiToken; @@ -88,6 +90,29 @@ void testCreateResourceDirective() { assertThat(((ResourceDirective) directive).getPredicates()).containsExactly("foo", "bar"); } + @Test + void testCreateTextBlockDirective() { + CompilerDirective directive = parse("{$TEXTBLOCK NATIVE}"); + assertThat(directive).isInstanceOf(TextBlockDirective.class); + assertThat(((TextBlockDirective) directive).getLineEndingKind()) + .isEqualTo(LineEndingKind.NATIVE); + + directive = parse("{$TEXTBLOCK CR}"); + assertThat(directive).isInstanceOf(TextBlockDirective.class); + assertThat(((TextBlockDirective) directive).getLineEndingKind()).isEqualTo(LineEndingKind.CR); + + directive = parse("{$TEXTBLOCK LF}"); + assertThat(directive).isInstanceOf(TextBlockDirective.class); + assertThat(((TextBlockDirective) directive).getLineEndingKind()).isEqualTo(LineEndingKind.LF); + + directive = parse("{$TEXTBLOCK CRLF}"); + assertThat(directive).isInstanceOf(TextBlockDirective.class); + assertThat(((TextBlockDirective) directive).getLineEndingKind()).isEqualTo(LineEndingKind.CRLF); + + directive = parse("{$TEXTBLOCK UNKNOWN}"); + assertThat(directive).isNull(); + } + @Test void testCreateIfDirective() { CompilerDirective directive = parse("{$if True}"); From fa18f612dab673755715fa6ba8b815698d1a81f0 Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Thu, 14 Mar 2024 14:56:03 +1100 Subject: [PATCH 2/4] Handle native integer types as weak aliases in Delphi 12+ Closes #81 --- CHANGELOG.md | 3 + .../delphi/checks/verifier/CheckVerifier.java | 6 + .../checks/verifier/CheckVerifierImpl.java | 21 +++- .../PlatformDependentCastCheckTest.java | 115 ++++++++++++------ .../PlatformDependentTruncationCheckTest.java | 67 ++++++---- .../type/factory/TypeAliasGenerator.java | 20 +-- .../delphi/type/factory/TypeFactoryImpl.java | 24 +++- .../delphi/type/TypeFactoryTest.java | 66 ++++++++-- 8 files changed, 237 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f873d427b..7f0bd4946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for the `TEXTBLOCK` directive. - **API:** `CompilerDirectiveParser` can now return a new `TextBlockDirective` type. +- **API:** `CheckVerifier::withCompilerVersion` method. +- **API:** `CheckVerifier::withToolchain` method. ### Changed +- `NativeInt` and `NativeUInt` are now treated as weak aliases in Delphi 12+. - Performance improvements. ### Fixed diff --git a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifier.java b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifier.java index 90c06d3a9..6c7bfc72d 100644 --- a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifier.java +++ b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifier.java @@ -20,6 +20,8 @@ import au.com.integradev.delphi.builders.DelphiTestFile; import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; +import au.com.integradev.delphi.compiler.CompilerVersion; +import au.com.integradev.delphi.compiler.Toolchain; import org.sonar.plugins.communitydelphi.api.check.DelphiCheck; public interface CheckVerifier { @@ -29,6 +31,10 @@ static CheckVerifier newVerifier() { CheckVerifier withCheck(DelphiCheck check); + CheckVerifier withCompilerVersion(CompilerVersion compilerVersion); + + CheckVerifier withToolchain(Toolchain toolchain); + CheckVerifier withUnitScopeName(String unitScope); CheckVerifier withUnitAlias(String alias, String unitName); diff --git a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java index 7e9163290..cc188066d 100644 --- a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java +++ b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java @@ -27,7 +27,9 @@ import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; import au.com.integradev.delphi.check.DelphiCheckContextImpl; import au.com.integradev.delphi.check.MasterCheckRegistrar; +import au.com.integradev.delphi.compiler.CompilerVersion; import au.com.integradev.delphi.compiler.Platform; +import au.com.integradev.delphi.compiler.Toolchain; import au.com.integradev.delphi.file.DelphiFile.DelphiInputFile; import au.com.integradev.delphi.preprocessor.DelphiPreprocessorFactory; import au.com.integradev.delphi.preprocessor.directive.CompilerDirectiveParserImpl; @@ -80,6 +82,8 @@ public class CheckVerifierImpl implements CheckVerifier { private DelphiCheck check; private DelphiTestFile testFile; + private CompilerVersion compilerVersion = DelphiProperties.COMPILER_VERSION_DEFAULT; + private Toolchain toolchain = DelphiProperties.COMPILER_TOOLCHAIN_DEFAULT; private final Set unitScopeNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); private final Map unitAliases = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); private final List searchPathUnits = new ArrayList<>(); @@ -92,6 +96,18 @@ public CheckVerifier withCheck(DelphiCheck check) { return this; } + @Override + public CheckVerifier withCompilerVersion(CompilerVersion compilerVersion) { + this.compilerVersion = compilerVersion; + return this; + } + + @Override + public CheckVerifier withToolchain(Toolchain toolchain) { + this.toolchain = toolchain; + return this; + } + @Override public CheckVerifier withUnitScopeName(String unitScopeName) { unitScopeNames.add(unitScopeName); @@ -248,10 +264,7 @@ private List execute() { SymbolTable symbolTable = SymbolTable.builder() .preprocessorFactory(new DelphiPreprocessorFactory(Platform.WINDOWS)) - .typeFactory( - new TypeFactoryImpl( - DelphiProperties.COMPILER_TOOLCHAIN_DEFAULT, - DelphiProperties.COMPILER_VERSION_DEFAULT)) + .typeFactory(new TypeFactoryImpl(toolchain, compilerVersion)) .standardLibraryPath(standardLibraryPath) .sourceFiles(List.of(file.getSourceCodeFile().toPath())) .unitAliases(unitAliases) diff --git a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentCastCheckTest.java b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentCastCheckTest.java index 7c9a1a537..c28cbc7c7 100644 --- a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentCastCheckTest.java +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentCastCheckTest.java @@ -20,13 +20,20 @@ import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; import au.com.integradev.delphi.checks.verifier.CheckVerifier; -import org.junit.jupiter.api.Test; +import au.com.integradev.delphi.compiler.CompilerVersion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class PlatformDependentCastCheckTest { - @Test - void testPointerIntegerCastsShouldAddIssue() { + private static final String VERSION_ALEXANDRIA = "VER350"; + private static final String VERSION_ATHENS = "VER360"; + + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testPointerIntegerCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -40,10 +47,12 @@ void testPointerIntegerCastsShouldAddIssue() { .verifyIssues(); } - @Test - void testObjectIntegerCastsShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testObjectIntegerCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -57,10 +66,12 @@ void testObjectIntegerCastsShouldAddIssue() { .verifyIssues(); } - @Test - void testInterfaceIntegerCastsShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testInterfaceIntegerCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -74,10 +85,12 @@ void testInterfaceIntegerCastsShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntIntegerCastsShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntIntegerCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -91,10 +104,12 @@ void testNativeIntIntegerCastsShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntPointerCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntPointerCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -108,10 +123,12 @@ void testNativeIntPointerCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testNativeIntObjectCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntObjectCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -125,10 +142,12 @@ void testNativeIntObjectCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testNativeIntInterfaceCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntInterfaceCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -142,10 +161,12 @@ void testNativeIntInterfaceCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testIntegerLiteralCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testIntegerLiteralCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -159,10 +180,12 @@ void testIntegerLiteralCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testHexadecimalLiteralCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testHexadecimalLiteralCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -176,10 +199,12 @@ void testHexadecimalLiteralCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testBinaryLiteralCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testBinaryLiteralCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -193,10 +218,12 @@ void testBinaryLiteralCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testTObjectStringCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testTObjectStringCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -210,10 +237,12 @@ void testTObjectStringCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testTObjectRecordCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testTObjectRecordCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("type") @@ -230,10 +259,12 @@ void testTObjectRecordCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testRecordIntegerCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testRecordIntegerCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("type") @@ -249,10 +280,12 @@ void testRecordIntegerCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testIntegerStringCastsShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testIntegerStringCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -266,10 +299,12 @@ void testIntegerStringCastsShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntStringCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntStringCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -283,10 +318,12 @@ void testNativeIntStringCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testIntegerArrayCastsShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testIntegerArrayCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -300,10 +337,12 @@ void testIntegerArrayCastsShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntArrayCastsShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntArrayCastsShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -317,10 +356,12 @@ void testNativeIntArrayCastsShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testStrongAliasCastsShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testStrongAliasCastsShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentCastCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") diff --git a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentTruncationCheckTest.java b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentTruncationCheckTest.java index 37b39266e..fe181088a 100644 --- a/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentTruncationCheckTest.java +++ b/delphi-checks/src/test/java/au/com/integradev/delphi/checks/PlatformDependentTruncationCheckTest.java @@ -20,13 +20,20 @@ import au.com.integradev.delphi.builders.DelphiTestUnitBuilder; import au.com.integradev.delphi.checks.verifier.CheckVerifier; -import org.junit.jupiter.api.Test; +import au.com.integradev.delphi.compiler.CompilerVersion; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class PlatformDependentTruncationCheckTest { - @Test - void testIntegerToNativeIntAssignmentShouldNotAddIssue() { + private static final String VERSION_ALEXANDRIA = "VER350"; + private static final String VERSION_ATHENS = "VER360"; + + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testIntegerToNativeIntAssignmentShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -39,10 +46,12 @@ void testIntegerToNativeIntAssignmentShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testInt64ToNativeIntAssignmentShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testInt64ToNativeIntAssignmentShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -55,10 +64,12 @@ void testInt64ToNativeIntAssignmentShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntToIntegerAssignmentShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntToIntegerAssignmentShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -71,10 +82,12 @@ void testNativeIntToIntegerAssignmentShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntToI64AssignmentShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntToI64AssignmentShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -87,10 +100,12 @@ void testNativeIntToI64AssignmentShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testNativeIntToNativeIntAssignmentShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntToNativeIntAssignmentShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendImpl("procedure Foo;") @@ -103,10 +118,12 @@ void testNativeIntToNativeIntAssignmentShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testIntegerArgumentToNativeIntParameterShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testIntegerArgumentToNativeIntParameterShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("procedure Bar(Nat: NativeInt);") @@ -119,10 +136,12 @@ void testIntegerArgumentToNativeIntParameterShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testInt64ArgumentToNativeIntParameterShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testInt64ArgumentToNativeIntParameterShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("procedure Bar(Nat: NativeInt);") @@ -135,10 +154,12 @@ void testInt64ArgumentToNativeIntParameterShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntArgumentToIntegerParameterShouldAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntArgumentToIntegerParameterShouldAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("procedure Bar(Nat: NativeInt);") @@ -151,10 +172,12 @@ void testNativeIntArgumentToIntegerParameterShouldAddIssue() { .verifyIssues(); } - @Test - void testNativeIntArgumentToI64ParameterShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntArgumentToI64ParameterShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("procedure Bar(I64: Int64);") @@ -167,10 +190,12 @@ void testNativeIntArgumentToI64ParameterShouldNotAddIssue() { .verifyNoIssues(); } - @Test - void testNativeIntArgumentToNativeIntParameterShouldNotAddIssue() { + @ParameterizedTest + @ValueSource(strings = {VERSION_ALEXANDRIA, VERSION_ATHENS}) + void testNativeIntArgumentToNativeIntParameterShouldNotAddIssue(String versionSymbol) { CheckVerifier.newVerifier() .withCheck(new PlatformDependentTruncationCheck()) + .withCompilerVersion(CompilerVersion.fromVersionSymbol(versionSymbol)) .onFile( new DelphiTestUnitBuilder() .appendDecl("procedure Bar(Nat: NativeInt);") diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeAliasGenerator.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeAliasGenerator.java index f7b2cba48..438fb7607 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeAliasGenerator.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeAliasGenerator.java @@ -22,10 +22,12 @@ import static net.bytebuddy.implementation.MethodCall.invoke; import static net.bytebuddy.matcher.ElementMatchers.named; +import com.google.common.base.Suppliers; import java.lang.reflect.Constructor; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; import net.bytebuddy.ByteBuddy; import net.bytebuddy.NamingStrategy; import net.bytebuddy.dynamic.DynamicType; @@ -78,16 +80,13 @@ final class TypeAliasGenerator { UnresolvedType.class, UnknownType.class); - private final Map, Class> cache; - - public TypeAliasGenerator() { - this.cache = new HashMap<>(); - } + private static final Supplier, Class>> cache = + Suppliers.memoize(TypeAliasGenerator::generateCache); public AliasType generate(String aliasImage, Type aliased, boolean strong) { for (Class typeInterface : TYPE_INTERFACES) { if (typeInterface.isAssignableFrom(aliased.getClass())) { - var clazz = cache.computeIfAbsent(typeInterface, TypeAliasGenerator::generateAliasClass); + var clazz = cache.get().get(typeInterface); try { Constructor constructor = clazz.getDeclaredConstructor(String.class, typeInterface, boolean.class); @@ -100,6 +99,13 @@ public AliasType generate(String aliasImage, Type aliased, boolean strong) { throw new AssertionError("Unhandled class could not be aliased: " + aliased.getClass()); } + private static Map, Class> generateCache() { + return TYPE_INTERFACES.stream() + .collect( + Collectors.toUnmodifiableMap( + interfaceType -> interfaceType, TypeAliasGenerator::generateAliasClass)); + } + @SuppressWarnings("unchecked") private static Class generateAliasClass( Class interfaceType) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java index 6782994b4..6475efa65 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java @@ -73,6 +73,7 @@ public class TypeFactoryImpl implements TypeFactory { private static final CompilerVersion VERSION_4 = CompilerVersion.fromVersionSymbol("VER120"); private static final CompilerVersion VERSION_2009 = CompilerVersion.fromVersionNumber("20.0"); private static final CompilerVersion VERSION_XE8 = CompilerVersion.fromVersionNumber("29.0"); + private static final CompilerVersion VERSION_ATHENS = CompilerVersion.fromVersionNumber("36.0"); private static final AtomicLong ANONYMOUS_STRUCT_COUNTER = new AtomicLong(); private final Toolchain toolchain; @@ -122,6 +123,11 @@ private boolean isLong64Bit() { && toolchain.platform != Platform.WINDOWS; } + private boolean isNativeIntWeakAlias() { + // See: https://docwiki.embarcadero.com/Libraries/Athens/en/System.NativeInt + return compilerVersion.compareTo(VERSION_ATHENS) >= 0; + } + private boolean isStringUnicode() { // See: http://bit.ly/new-string-type-unicodestring return compilerVersion.compareTo(VERSION_2009) >= 0; @@ -204,8 +210,8 @@ private void addVariant(IntrinsicType intrinsic, int size, boolean ole) { intrinsicTypes.put(intrinsic, new VariantTypeImpl(intrinsic.fullyQualifiedName(), size, ole)); } - private void addWeakAlias(IntrinsicType alias, IntrinsicType concrete) { - intrinsicTypes.put(alias, getIntrinsic(concrete)); + private void addWeakAlias(IntrinsicType intrinsic, IntrinsicType aliased) { + intrinsicTypes.put(intrinsic, weakAlias(intrinsic.fullyQualifiedName(), getIntrinsic(aliased))); } private void createIntrinsicTypes() { @@ -244,8 +250,18 @@ private void createIntrinsicTypes() { addWeakAlias(IntrinsicType.LONGWORD, IntrinsicType.CARDINAL); } - addInteger(IntrinsicType.NATIVEINT, nativeIntegerSize(), true); - addInteger(IntrinsicType.NATIVEUINT, nativeIntegerSize(), false); + if (isNativeIntWeakAlias()) { + if (toolchain.architecture == Architecture.X64) { + addWeakAlias(IntrinsicType.NATIVEINT, IntrinsicType.INT64); + addWeakAlias(IntrinsicType.NATIVEUINT, IntrinsicType.UINT64); + } else { + addWeakAlias(IntrinsicType.NATIVEINT, IntrinsicType.INTEGER); + addWeakAlias(IntrinsicType.NATIVEUINT, IntrinsicType.CARDINAL); + } + } else { + addInteger(IntrinsicType.NATIVEINT, nativeIntegerSize(), true); + addInteger(IntrinsicType.NATIVEUINT, nativeIntegerSize(), false); + } addChar(IntrinsicType.ANSICHAR, 1); addChar(IntrinsicType.WIDECHAR, 2); diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/TypeFactoryTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/TypeFactoryTest.java index e0a81e5c8..8f2fb7e26 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/TypeFactoryTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/TypeFactoryTest.java @@ -25,12 +25,14 @@ import au.com.integradev.delphi.compiler.Toolchain; import au.com.integradev.delphi.type.factory.TypeFactoryImpl; import java.util.stream.Stream; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.sonar.plugins.communitydelphi.api.type.IntrinsicType; +import org.sonar.plugins.communitydelphi.api.type.Type; import org.sonar.plugins.communitydelphi.api.type.TypeFactory; class TypeFactoryTest { @@ -40,6 +42,8 @@ class TypeFactoryTest { private static final String VERSION_2009 = "VER200"; private static final String VERSION_XE7 = "VER280"; private static final String VERSION_XE8 = "VER290"; + private static final String VERSION_ALEXANDRIA = "VER350"; + private static final String VERSION_ATHENS = "VER360"; static class RealSizeArgumentsProvider implements ArgumentsProvider { @Override @@ -118,8 +122,7 @@ public Stream provideArguments(ExtensionContext context) { @ParameterizedTest(name = "Real should be {1} bytes in {0}") @ArgumentsSource(RealSizeArgumentsProvider.class) void testSizeOfReal(String versionSymbol, int size) { - TypeFactory typeFactory = - new TypeFactoryImpl(Toolchain.DCC32, CompilerVersion.fromVersionSymbol(versionSymbol)); + TypeFactory typeFactory = typeFactory(Toolchain.DCC32, versionSymbol); assertThat(typeFactory.getIntrinsic(IntrinsicType.REAL).size()).isEqualTo(size); } @@ -134,8 +137,7 @@ void testSizeOfExtended(Toolchain toolchain, int size) { @ParameterizedTest(name = "LongInt and LongWord should be {2} bytes on {0} in {1}") @ArgumentsSource(LongSizeArgumentsProvider.class) void testSizeOfLongIntegers(Toolchain toolchain, String versionSymbol, int size) { - TypeFactory typeFactory = - new TypeFactoryImpl(toolchain, CompilerVersion.fromVersionSymbol(versionSymbol)); + TypeFactory typeFactory = typeFactory(toolchain, versionSymbol); assertThat(typeFactory.getIntrinsic(IntrinsicType.LONGINT).size()).isEqualTo(size); assertThat(typeFactory.getIntrinsic(IntrinsicType.LONGWORD).size()).isEqualTo(size); } @@ -143,8 +145,7 @@ void testSizeOfLongIntegers(Toolchain toolchain, String versionSymbol, int size) @ParameterizedTest(name = "NativeInt and NativeUInt should be {2} bytes on {0} in {1}") @ArgumentsSource(NativeSizeArgumentsProvider.class) void testSizeOfNativeIntegers(Toolchain toolchain, String versionSymbol, int size) { - TypeFactory typeFactory = - new TypeFactoryImpl(toolchain, CompilerVersion.fromVersionSymbol(versionSymbol)); + TypeFactory typeFactory = typeFactory(toolchain, versionSymbol); assertThat(typeFactory.getIntrinsic(IntrinsicType.NATIVEINT).size()).isEqualTo(size); assertThat(typeFactory.getIntrinsic(IntrinsicType.NATIVEUINT).size()).isEqualTo(size); } @@ -152,24 +153,65 @@ void testSizeOfNativeIntegers(Toolchain toolchain, String versionSymbol, int siz @ParameterizedTest(name = "Pointers should be {1} bytes on {0}") @ArgumentsSource(PointerSizeArgumentsProvider.class) void testSizeOfPointers(Toolchain toolchain, int size) { - TypeFactory typeFactory = - new TypeFactoryImpl(toolchain, DelphiProperties.COMPILER_VERSION_DEFAULT); + TypeFactory typeFactory = typeFactory(toolchain, VERSION_ALEXANDRIA); assertThat(typeFactory.getIntrinsic(IntrinsicType.POINTER).size()).isEqualTo(size); } @ParameterizedTest(name = "String should be \"{1}\" in {0}") @ArgumentsSource(StringTypeArgumentsProvider.class) void testTypeOfString(String versionSymbol, String signature) { - TypeFactory typeFactory = - new TypeFactoryImpl(Toolchain.DCC32, CompilerVersion.fromVersionSymbol(versionSymbol)); + TypeFactory typeFactory = typeFactory(Toolchain.DCC32, versionSymbol); assertThat(typeFactory.getIntrinsic(IntrinsicType.STRING).getImage()).isEqualTo(signature); } @ParameterizedTest(name = "Char should be \"{1}\" in {0}") @ArgumentsSource(CharTypeArgumentsProvider.class) void testTypeOfChar(String versionSymbol, String signature) { - TypeFactory typeFactory = - new TypeFactoryImpl(Toolchain.DCC32, CompilerVersion.fromVersionSymbol(versionSymbol)); + TypeFactory typeFactory = typeFactory(Toolchain.DCC32, versionSymbol); assertThat(typeFactory.getIntrinsic(IntrinsicType.CHAR).getImage()).isEqualTo(signature); } + + @Test + void testNativeIntegersAreNotWeakAliasesOnAlexandria() { + TypeFactory typeFactory = typeFactory(Toolchain.DCC32, VERSION_ALEXANDRIA); + Type nativeInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEINT); + Type nativeUInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEUINT); + + assertThat(nativeInt.isWeakAlias()).isFalse(); + assertThat(nativeUInt.isWeakAlias()).isFalse(); + + typeFactory = typeFactory(Toolchain.DCC64, VERSION_ALEXANDRIA); + nativeInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEINT); + nativeUInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEUINT); + + assertThat(nativeInt.isWeakAlias()).isFalse(); + assertThat(nativeUInt.isWeakAlias()).isFalse(); + } + + @Test + void testNativeIntegersAreWeakAliasesOnAthens() { + TypeFactory typeFactory = typeFactory(Toolchain.DCC32, VERSION_ATHENS); + Type nativeInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEINT); + Type nativeUInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEUINT); + + assertThat(nativeInt.isWeakAlias()).isTrue(); + assertThat(nativeInt.is(IntrinsicType.INTEGER)).isTrue(); + + assertThat(nativeUInt.isWeakAlias()).isTrue(); + assertThat(nativeUInt.is(IntrinsicType.CARDINAL)).isTrue(); + + typeFactory = typeFactory(Toolchain.DCC64, VERSION_ATHENS); + nativeInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEINT); + nativeUInt = typeFactory.getIntrinsic(IntrinsicType.NATIVEUINT); + + assertThat(nativeInt.isWeakAlias()).isTrue(); + assertThat(nativeInt.is(IntrinsicType.INT64)).isTrue(); + + assertThat(nativeUInt.isWeakAlias()).isTrue(); + assertThat(nativeUInt.is(IntrinsicType.UINT64)).isTrue(); + } + + private static TypeFactory typeFactory(Toolchain toolchain, String versionSymbol) { + return new TypeFactoryImpl(toolchain, CompilerVersion.fromVersionSymbol(versionSymbol)); + } } From 4345db71e97fd2cc71ffb38a0fa1d499e324bf15 Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Thu, 14 Mar 2024 15:45:24 +1100 Subject: [PATCH 3/4] Model the lengths of open arrays as `NativeInt` in Delphi 12+ Closes #142 --- CHANGELOG.md | 1 + .../delphi/type/factory/TypeFactoryImpl.java | 4 ++ .../type/intrinsic/IntrinsicReturnType.java | 45 ++++++++++++- .../type/intrinsic/IntrinsicsInjector.java | 8 +-- .../intrinsic/IntrinsicReturnTypeTest.java | 63 ++++++++++++++++--- 5 files changed, 108 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0bd4946..0508a3977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `NativeInt` and `NativeUInt` are now treated as weak aliases in Delphi 12+. +- The length of open arrays is now modeled as `NativeInt` in Delphi 12+. - Performance improvements. ### Fixed diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java index 6475efa65..8b9f6f0f2 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/factory/TypeFactoryImpl.java @@ -590,4 +590,8 @@ public IntegerType integerFromLiteralValue(BigInteger value) { .findFirst() .orElseThrow(IllegalStateException::new); } + + public CompilerVersion getCompilerVersion() { + return compilerVersion; + } } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnType.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnType.java index 8f9b05888..23f50cfa1 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnType.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnType.java @@ -26,6 +26,7 @@ import static org.sonar.plugins.communitydelphi.api.type.IntrinsicType.UNICODESTRING; import static org.sonar.plugins.communitydelphi.api.type.IntrinsicType.WIDECHAR; +import au.com.integradev.delphi.compiler.CompilerVersion; import au.com.integradev.delphi.type.TypeImpl; import au.com.integradev.delphi.type.factory.ArrayOption; import au.com.integradev.delphi.type.factory.TypeFactoryImpl; @@ -40,6 +41,8 @@ import org.sonar.plugins.communitydelphi.api.type.TypeFactory; public abstract class IntrinsicReturnType extends TypeImpl { + private static final CompilerVersion VERSION_ATHENS = CompilerVersion.fromVersionNumber("36.0"); + @Override public String getImage() { return "<" + getClass().getSimpleName() + ">"; @@ -53,6 +56,10 @@ public int size() { public abstract Type getReturnType(List arguments); + public static Type length(TypeFactory typeFactory) { + return new LengthReturnType(typeFactory); + } + public static Type high(TypeFactory typeFactory) { return new HighLowReturnType(typeFactory); } @@ -89,11 +96,45 @@ public static Type argumentByIndex(int index) { return new ArgumentByIndexReturnType(index); } + private static final class LengthReturnType extends IntrinsicReturnType { + private final Type byteType; + private final Type integerType; + private final Type openArraySizeType; + + private LengthReturnType(TypeFactory typeFactory) { + this.byteType = typeFactory.getIntrinsic(IntrinsicType.BYTE); + this.integerType = typeFactory.getIntrinsic(IntrinsicType.INTEGER); + this.openArraySizeType = + typeFactory.getIntrinsic( + ((TypeFactoryImpl) typeFactory).getCompilerVersion().compareTo(VERSION_ATHENS) >= 0 + ? IntrinsicType.NATIVEINT + : IntrinsicType.INTEGER); + } + + @Override + public Type getReturnType(List arguments) { + Type type = arguments.get(0); + if (type.is(IntrinsicType.SHORTSTRING)) { + return byteType; + } else if (type.isOpenArray()) { + return openArraySizeType; + } else { + return integerType; + } + } + } + private static final class HighLowReturnType extends IntrinsicReturnType { private final Type integerType; + private final Type openArraySizeType; private HighLowReturnType(TypeFactory typeFactory) { this.integerType = typeFactory.getIntrinsic(IntrinsicType.INTEGER); + this.openArraySizeType = + typeFactory.getIntrinsic( + ((TypeFactoryImpl) typeFactory).getCompilerVersion().compareTo(VERSION_ATHENS) >= 0 + ? IntrinsicType.NATIVEINT + : IntrinsicType.INTEGER); } @Override @@ -104,7 +145,9 @@ public Type getReturnType(List arguments) { type = ((ClassReferenceType) type).classType(); } - if (type.isArray() || type.isString()) { + if (type.isOpenArray()) { + type = openArraySizeType; + } else if (type.isArray() || type.isString()) { type = integerType; } diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicsInjector.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicsInjector.java index daaccf58c..92ed9affe 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicsInjector.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/type/intrinsic/IntrinsicsInjector.java @@ -212,10 +212,10 @@ private void buildRoutines() { routine("Insert").param(LIKE_DYNAMIC_ARRAY).varParam(LIKE_DYNAMIC_ARRAY).param(type(INTEGER)); routine("IsConstValue").param(TypeFactory.untypedType()).returns(type(BOOLEAN)); routine("IsManagedType").param(ANY_CLASS_REFERENCE).returns(type(BOOLEAN)); - routine("Length").param(type(SHORTSTRING)).returns(type(BYTE)); - routine("Length").param(type(ANSISTRING)).returns(type(INTEGER)); - routine("Length").param(type(UNICODESTRING)).returns(type(INTEGER)); - routine("Length").param(ANY_ARRAY).returns(type(INTEGER)); + routine("Length").param(type(SHORTSTRING)).returns(IntrinsicReturnType.length(typeFactory)); + routine("Length").param(type(ANSISTRING)).returns(IntrinsicReturnType.length(typeFactory)); + routine("Length").param(type(UNICODESTRING)).returns(IntrinsicReturnType.length(typeFactory)); + routine("Length").param(ANY_ARRAY).returns(IntrinsicReturnType.length(typeFactory)); routine("Lo").param(type(INTEGER)).returns(type(BYTE)); routine("Low") .varParam(TypeFactory.untypedType()) diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnTypeTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnTypeTest.java index 926478495..7b0832341 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnTypeTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/type/intrinsic/IntrinsicReturnTypeTest.java @@ -21,9 +21,10 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.mock; +import au.com.integradev.delphi.compiler.CompilerVersion; +import au.com.integradev.delphi.compiler.Toolchain; import au.com.integradev.delphi.type.factory.ArrayOption; import au.com.integradev.delphi.type.factory.TypeFactoryImpl; -import au.com.integradev.delphi.utils.types.TypeFactoryUtils; import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; @@ -34,23 +35,69 @@ import org.sonar.plugins.communitydelphi.api.type.TypeFactory; class IntrinsicReturnTypeTest { - private static final TypeFactory TYPE_FACTORY = TypeFactoryUtils.defaultFactory(); + private static final TypeFactory TYPE_FACTORY = + new TypeFactoryImpl(Toolchain.DCC64, CompilerVersion.fromVersionNumber("35.0")); + private static final TypeFactory TYPE_FACTORY_ATHENS = + new TypeFactoryImpl(Toolchain.DCC64, CompilerVersion.fromVersionNumber("36.0")); + + @Test + void testLength() { + Type shortString = TYPE_FACTORY.getIntrinsic(IntrinsicType.SHORTSTRING); + Type ansiString = TYPE_FACTORY.getIntrinsic(IntrinsicType.ANSISTRING); + Type wideString = TYPE_FACTORY.getIntrinsic(IntrinsicType.WIDESTRING); + Type unicodeString = TYPE_FACTORY.getIntrinsic(IntrinsicType.UNICODESTRING); + Type fixedArray = + ((TypeFactoryImpl) TYPE_FACTORY).array(null, ansiString, Set.of(ArrayOption.FIXED)); + Type dynamicArray = + ((TypeFactoryImpl) TYPE_FACTORY).array(null, ansiString, Set.of(ArrayOption.DYNAMIC)); + Type openArray = + ((TypeFactoryImpl) TYPE_FACTORY).array(null, ansiString, Set.of(ArrayOption.OPEN)); + + var length35 = (IntrinsicReturnType) IntrinsicReturnType.length(TYPE_FACTORY); + assertThat(length35.getReturnType(List.of(shortString)).is(IntrinsicType.BYTE)).isTrue(); + assertThat(length35.getReturnType(List.of(ansiString)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length35.getReturnType(List.of(wideString)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length35.getReturnType(List.of(unicodeString)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length35.getReturnType(List.of(fixedArray)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length35.getReturnType(List.of(dynamicArray)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length35.getReturnType(List.of(openArray)).is(IntrinsicType.INTEGER)).isTrue(); + + var length36 = (IntrinsicReturnType) IntrinsicReturnType.length(TYPE_FACTORY_ATHENS); + assertThat(length36.getReturnType(List.of(shortString)).is(IntrinsicType.BYTE)).isTrue(); + assertThat(length36.getReturnType(List.of(ansiString)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length36.getReturnType(List.of(wideString)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length36.getReturnType(List.of(unicodeString)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length36.getReturnType(List.of(fixedArray)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length36.getReturnType(List.of(dynamicArray)).is(IntrinsicType.INTEGER)).isTrue(); + assertThat(length36.getReturnType(List.of(openArray)).is(IntrinsicType.NATIVEINT)).isTrue(); + } @Test void testHighLow() { Type smallInt = TYPE_FACTORY.getIntrinsic(IntrinsicType.SMALLINT); Type integer = TYPE_FACTORY.getIntrinsic(IntrinsicType.INTEGER); + Type nativeInt = TYPE_FACTORY.getIntrinsic(IntrinsicType.NATIVEINT); Type string = TYPE_FACTORY.getIntrinsic(IntrinsicType.UNICODESTRING); Type array = ((TypeFactoryImpl) TYPE_FACTORY).array(null, string, Set.of(ArrayOption.DYNAMIC)); + Type openArray = ((TypeFactoryImpl) TYPE_FACTORY).array(null, string, Set.of(ArrayOption.OPEN)); Type classType = mock(StructType.class); Type classReference = TYPE_FACTORY.classOf("Foo", classType); - var high = (IntrinsicReturnType) IntrinsicReturnType.high(TYPE_FACTORY); - assertThat(high.getReturnType(List.of(smallInt)).is(smallInt)).isTrue(); - assertThat(high.getReturnType(List.of(integer)).is(integer)).isTrue(); - assertThat(high.getReturnType(List.of(string)).is(integer)).isTrue(); - assertThat(high.getReturnType(List.of(array)).is(integer)).isTrue(); - assertThat(high.getReturnType(List.of(classReference))).isSameAs(classType); + var high35 = (IntrinsicReturnType) IntrinsicReturnType.high(TYPE_FACTORY); + assertThat(high35.getReturnType(List.of(smallInt)).is(smallInt)).isTrue(); + assertThat(high35.getReturnType(List.of(integer)).is(integer)).isTrue(); + assertThat(high35.getReturnType(List.of(string)).is(integer)).isTrue(); + assertThat(high35.getReturnType(List.of(array)).is(integer)).isTrue(); + assertThat(high35.getReturnType(List.of(openArray)).is(integer)).isTrue(); + assertThat(high35.getReturnType(List.of(classReference))).isSameAs(classType); + + var high36 = (IntrinsicReturnType) IntrinsicReturnType.high(TYPE_FACTORY_ATHENS); + assertThat(high36.getReturnType(List.of(smallInt)).is(smallInt)).isTrue(); + assertThat(high36.getReturnType(List.of(integer)).is(integer)).isTrue(); + assertThat(high36.getReturnType(List.of(string)).is(integer)).isTrue(); + assertThat(high36.getReturnType(List.of(array)).is(integer)).isTrue(); + assertThat(high36.getReturnType(List.of(openArray)).is(nativeInt)).isTrue(); + assertThat(high36.getReturnType(List.of(classReference))).isSameAs(classType); } @Test From a9b01a9539bf5bbd2b49a738954a5007e962eac5 Mon Sep 17 00:00:00 2001 From: Jonah Jeleniewski Date: Thu, 14 Mar 2024 18:26:20 +1100 Subject: [PATCH 4/4] Add support for multiline strings in compiler directives --- CHANGELOG.md | 1 + .../checks/verifier/CheckVerifierImpl.java | 4 +- .../delphi/executor/DelphiChecksExecutor.java | 3 +- .../preprocessor/DelphiPreprocessor.java | 3 +- .../CompilerDirectiveParserImpl.java | 14 +++- .../directive/expression/ExpressionLexer.java | 80 ++++++++++++++++--- .../expression/ExpressionParser.java | 71 +++++++++++++++- .../directive/expression/Expressions.java | 1 + .../directive/expression/Token.java | 1 + .../CompilerDirectiveParserTest.java | 3 +- .../expression/ExpressionLexerTest.java | 4 +- .../expression/ExpressionParserTest.java | 4 +- .../directive/expression/ExpressionsTest.java | 19 ++++- 13 files changed, 185 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0508a3977..3a78a8437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for multiline string literals within compiler directives. - Support for the `TEXTBLOCK` directive. - **API:** `CompilerDirectiveParser` can now return a new `TextBlockDirective` type. - **API:** `CheckVerifier::withCompilerVersion` method. diff --git a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java index cc188066d..39670aeb0 100644 --- a/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java +++ b/delphi-checks-testkit/src/main/java/au/com/integradev/delphi/checks/verifier/CheckVerifierImpl.java @@ -280,7 +280,9 @@ private List execute() { var sensorContext = SensorContextTester.create(FileUtils.getTempDirectory()); sensorContext.settings().setProperty(DelphiProperties.TEST_TYPE_KEY, "Test.TTestSuite"); - var compilerDirectiveParser = new CompilerDirectiveParserImpl(Platform.WINDOWS); + var compilerDirectiveParser = + new CompilerDirectiveParserImpl( + Platform.WINDOWS, file.getTextBlockLineEndingModeRegistry()); var checkRegistrar = mock(MasterCheckRegistrar.class); when(checkRegistrar.getRuleKey(check)) diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/executor/DelphiChecksExecutor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/executor/DelphiChecksExecutor.java index f60c14d76..ba83cd346 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/executor/DelphiChecksExecutor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/executor/DelphiChecksExecutor.java @@ -57,7 +57,8 @@ public DelphiChecksExecutor( @Override public void execute(Context context, DelphiInputFile delphiFile) { Platform platform = delphiProjectHelper.getToolchain().platform; - CompilerDirectiveParser compilerDirectiveParser = new CompilerDirectiveParserImpl(platform); + CompilerDirectiveParser compilerDirectiveParser = + new CompilerDirectiveParserImpl(platform, delphiFile.getTextBlockLineEndingModeRegistry()); Function createCheckContext = check -> new DelphiCheckContextImpl( diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java index ec22eb0eb..06f7f3aa7 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/DelphiPreprocessor.java @@ -51,7 +51,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.plugins.communitydelphi.api.directive.CompilerDirective; -import org.sonar.plugins.communitydelphi.api.directive.CompilerDirectiveParser; import org.sonar.plugins.communitydelphi.api.directive.ConditionalDirective; import org.sonar.plugins.communitydelphi.api.directive.SwitchDirective.SwitchKind; import org.sonar.plugins.communitydelphi.api.directive.TextBlockDirective.LineEndingKind; @@ -152,7 +151,7 @@ private void processToken(Token token) { tokenIndex++; if (token.getType() == DelphiLexer.TkCompilerDirective) { - CompilerDirectiveParser parser = new CompilerDirectiveParserImpl(platform); + var parser = new CompilerDirectiveParserImpl(platform, getTextBlockLineEndingModeRegistry()); DelphiToken directiveToken = new DelphiTokenImpl(token); parser.parse(directiveToken).ifPresent(this::processDirective); } else if (!parentDirective.isEmpty()) { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java index d066544cf..42564b245 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserImpl.java @@ -22,6 +22,7 @@ import static au.com.integradev.delphi.preprocessor.directive.CompilerDirectiveParserImpl.DirectiveBracketType.PAREN; import au.com.integradev.delphi.compiler.Platform; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingModeRegistry; import au.com.integradev.delphi.preprocessor.directive.expression.Expression; import au.com.integradev.delphi.preprocessor.directive.expression.ExpressionLexer; import au.com.integradev.delphi.preprocessor.directive.expression.ExpressionLexer.ExpressionLexerError; @@ -46,7 +47,6 @@ public class CompilerDirectiveParserImpl implements CompilerDirectiveParser { private static final ExpressionLexer EXPRESSION_LEXER = new ExpressionLexer(); - private static final ExpressionParser EXPRESSION_PARSER = new ExpressionParser(); private static final char END_OF_INPUT = '\0'; @@ -56,6 +56,7 @@ enum DirectiveBracketType { } private final Platform platform; + private final TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry; // Parser state private String data; @@ -65,8 +66,10 @@ enum DirectiveBracketType { private DelphiToken token; private DirectiveBracketType directiveBracketType; - public CompilerDirectiveParserImpl(Platform platform) { + public CompilerDirectiveParserImpl( + Platform platform, TextBlockLineEndingModeRegistry textBlockLineEndingModeRegistry) { this.platform = platform; + this.textBlockLineEndingModeRegistry = textBlockLineEndingModeRegistry; } @Override @@ -300,12 +303,17 @@ private Expression readExpression() { try { var tokens = EXPRESSION_LEXER.lex(input.toString()); - return EXPRESSION_PARSER.parse(tokens); + return expressionParser().parse(tokens); } catch (ExpressionLexerError | ExpressionParserError e) { throw new CompilerDirectiveParserError(e, token); } } + private ExpressionParser expressionParser() { + int index = token.getIndex(); + return new ExpressionParser(textBlockLineEndingModeRegistry.getLineEndingMode(index)); + } + private boolean isEndOfDirective(char character) { boolean result = false; diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexer.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexer.java index 546666a83..ebedfdf7d 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexer.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexer.java @@ -87,8 +87,12 @@ public List lex(String data) { } private char peekChar() { - if (position < data.length()) { - return data.charAt(position); + return peekChar(0); + } + + private char peekChar(int offset) { + if (position + offset < data.length()) { + return data.charAt(position + offset); } return END_OF_INPUT; } @@ -123,7 +127,7 @@ private Token readToken() { } else if (SYNTAX_CHARACTERS.containsKey(character)) { return readSyntaxToken(); } else if (character == '\'') { - return readSingleQuoteString(); + return readString(); } else { throw new ExpressionLexerError("Unexpected character: '" + character + "'"); } @@ -182,25 +186,83 @@ private Token readIdentifier() { return new Token(type, text); } - private Token readSingleQuoteString() { - getChar(); + private Token readString() { + Token result = readMultilineString(); + if (result == null) { + result = readSingleLineString(); + } + return result; + } + + private Token readSingleLineString() { StringBuilder value = new StringBuilder(); + value.append(getChar()); + char character; while ((character = getChar()) != END_OF_INPUT) { + value.append(character); if (character == '\'') { - if (peekChar() != '\'') { + if (peekChar() == '\'') { + value.append(getChar()); + } else { break; } - getChar(); - } else { - value.append(character); } } return new Token(TokenType.STRING, value.toString()); } + private Token readMultilineString() { + int lookahead = lookaheadMultilineString(); + if (lookahead == 0) { + return null; + } + + String value = data.substring(position, position + lookahead); + position += lookahead; + + return new Token(TokenType.MULTILINE_STRING, value); + } + + private int lookaheadMultilineString() { + int startQuotes = lookaheadSingleQuotes(0); + if (startQuotes >= 3 && (startQuotes % 2 == 1) && isNewLine(peekChar(startQuotes))) { + int i = startQuotes; + while (true) { + switch (peekChar(++i)) { + case '\'': + int quotes = Math.min(startQuotes, lookaheadSingleQuotes(i)); + i += quotes; + if (quotes == startQuotes) { + return i; + } + break; + + case END_OF_INPUT: + return 0; + + default: + // do nothing + } + } + } + return 0; + } + + private int lookaheadSingleQuotes(int i) { + int result = 0; + while (peekChar(i++) == '\'') { + ++result; + } + return result; + } + + private boolean isNewLine(int c) { + return c == '\r' || c == '\n'; + } + private static boolean isHexDigit(char character) { character = Character.toLowerCase(character); return Character.isDigit(character) || (character >= 'a' && character <= 'f'); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParser.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParser.java index 99f0b2f42..951ead576 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParser.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParser.java @@ -39,14 +39,19 @@ import static au.com.integradev.delphi.preprocessor.directive.expression.Token.TokenType.XOR; import static java.util.Objects.requireNonNullElse; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingMode; import au.com.integradev.delphi.preprocessor.directive.expression.Token.TokenType; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; public class ExpressionParser { public static class ExpressionParserError extends RuntimeException { @@ -70,10 +75,16 @@ public static class ExpressionParserError extends RuntimeException { private static final ImmutableSet UNARY_OPERATORS = Sets.immutableEnumSet(PLUS, MINUS, NOT); + private final TextBlockLineEndingMode textBlockLineEndingMode; + // Parser state private List tokens; private int position; + public ExpressionParser(TextBlockLineEndingMode textBlockLineEndingMode) { + this.textBlockLineEndingMode = textBlockLineEndingMode; + } + public Expression parse(List tokens) { this.tokens = tokens; this.position = 0; @@ -169,6 +180,7 @@ private Expression parsePrimary() { Token token = peekToken(); switch (token.getType()) { case STRING: + case MULTILINE_STRING: case INTEGER: case REAL: return parseLiteral(); @@ -191,7 +203,64 @@ private Expression parsePrimary() { private Expression parseLiteral() { Token token = getToken(); - return Expressions.literal(token.getType(), token.getText()); + + String text; + switch (token.getType()) { + case STRING: + text = evaluateString(token.getText()); + break; + case MULTILINE_STRING: + text = evaluateMultilineString(token.getText(), textBlockLineEndingMode); + break; + default: + text = token.getText(); + } + + return Expressions.literal(token.getType(), text); + } + + private static String evaluateString(String text) { + text = text.substring(1, text.length() - 1); + text = text.replace("''", "'"); + return text; + } + + private String evaluateMultilineString(String text, TextBlockLineEndingMode lineEndingMode) { + Deque lines = text.lines().collect(Collectors.toCollection(ArrayDeque::new)); + + lines.removeFirst(); + + String last = lines.removeLast(); + String indentation = readLeadingWhitespace(last); + + String lineEnding; + switch (lineEndingMode) { + case CR: + lineEnding = "\r"; + break; + case LF: + lineEnding = "\n"; + break; + default: + lineEnding = "\r\n"; + } + + return lines.stream() + .map(line -> StringUtils.removeStart(line, indentation)) + .collect(Collectors.joining(lineEnding)); + } + + private static String readLeadingWhitespace(String input) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); ++i) { + char c = input.charAt(i); + if (c <= 0x20 || c == 0x3000) { + result.append(c); + } else { + break; + } + } + return result.toString(); } private Expression parseIdentifier() { diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Expressions.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Expressions.java index 51f6aa6ce..0f2435a2e 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Expressions.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Expressions.java @@ -186,6 +186,7 @@ private static ExpressionValue createValue(TokenType type, String text) { case REAL: return createReal(doubleFromTextWithDigitSeparators(text)); case STRING: + case MULTILINE_STRING: return createString(text); default: throw new AssertionError("Unhandled literal expression type: " + type.name()); diff --git a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Token.java b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Token.java index 247bbe53d..7bb049ef6 100644 --- a/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Token.java +++ b/delphi-frontend/src/main/java/au/com/integradev/delphi/preprocessor/directive/expression/Token.java @@ -25,6 +25,7 @@ enum TokenType { REAL, IDENTIFIER, STRING, + MULTILINE_STRING, EQUALS, NOT_EQUALS, LESS_THAN, diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java index 3e162363c..0616ae7f5 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/CompilerDirectiveParserTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; import au.com.integradev.delphi.antlr.DelphiLexer; import au.com.integradev.delphi.antlr.ast.token.DelphiTokenImpl; @@ -53,7 +54,7 @@ class CompilerDirectiveParserTest { @BeforeEach void setup() { - parser = new CompilerDirectiveParserImpl(Platform.WINDOWS); + parser = new CompilerDirectiveParserImpl(Platform.WINDOWS, mock()); } @Test diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexerTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexerTest.java index 0c820925f..76cacf3bf 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexerTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionLexerTest.java @@ -93,7 +93,9 @@ static class StringTokensArgumentsProvider implements ArgumentsProvider { public Stream provideArguments(ExtensionContext context) { return Stream.of( Arguments.of("'My string'", TokenType.STRING), - Arguments.of("'Escaped '' single-quotes'", TokenType.STRING)); + Arguments.of("'Escaped '' single-quotes'", TokenType.STRING), + Arguments.of("'''\nMy\nmultiline\nstring\n'''", TokenType.MULTILINE_STRING), + Arguments.of("'''''\nMy\nmultiline\nstring\n'''''", TokenType.MULTILINE_STRING)); } } diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParserTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParserTest.java index 57e37a76d..8f2133059 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParserTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionParserTest.java @@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingMode; import au.com.integradev.delphi.preprocessor.directive.expression.ExpressionParser.ExpressionParserError; import au.com.integradev.delphi.preprocessor.directive.expression.Expressions.BinaryExpression; import au.com.integradev.delphi.preprocessor.directive.expression.Expressions.InvocationExpression; @@ -32,7 +33,7 @@ class ExpressionParserTest { private static final ExpressionLexer LEXER = new ExpressionLexer(); - private static final ExpressionParser PARSER = new ExpressionParser(); + private static final ExpressionParser PARSER = new ExpressionParser(TextBlockLineEndingMode.CRLF); @Test void testRelational() { @@ -61,6 +62,7 @@ void testUnary() { @Test void testLiterals() { assertThat(parse("'My string'")).isInstanceOf(LiteralExpression.class); + assertThat(parse("'''\nMy\nmultiline\nstring\n'''")).isInstanceOf(LiteralExpression.class); assertThat(parse("123")).isInstanceOf(LiteralExpression.class); assertThat(parse("123.45")).isInstanceOf(LiteralExpression.class); } diff --git a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionsTest.java b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionsTest.java index 6738d0397..f1b57331c 100644 --- a/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionsTest.java +++ b/delphi-frontend/src/test/java/au/com/integradev/delphi/preprocessor/directive/expression/ExpressionsTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.when; import au.com.integradev.delphi.preprocessor.DelphiPreprocessor; +import au.com.integradev.delphi.preprocessor.TextBlockLineEndingMode; import au.com.integradev.delphi.preprocessor.directive.expression.Expression.ExpressionValue; import au.com.integradev.delphi.preprocessor.directive.expression.Token.TokenType; import au.com.integradev.delphi.utils.types.TypeFactoryUtils; @@ -91,12 +92,23 @@ public Stream provideArguments(ExtensionContext context) { } } + static class StringArgumentsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + Arguments.of("'My string'", "My string"), + Arguments.of("'Escaped '' single-quotes'", "Escaped ' single-quotes"), + Arguments.of("'''\nMy\nmultiline\nstring\n'''", "My\r\nmultiline\r\nstring"), + Arguments.of("'''''\nMy\nmultiline\nstring\n'''''", "My\r\nmultiline\r\nstring")); + } + } + static class StringConcatenationArgumentsProvider implements ArgumentsProvider { @Override public Stream provideArguments(ExtensionContext context) { return Stream.of( - Arguments.of("'abc' + '123", "abc123"), - Arguments.of("'abc' + '", "abc"), + Arguments.of("'abc' + '123'", "abc123"), + Arguments.of("'abc' + ''", "abc"), Arguments.of("'abc' + 123", UNKNOWN)); } } @@ -285,6 +297,7 @@ void setup() { @ArgumentsSource(NumericExpressionArgumentsProvider.class) @ArgumentsSource(IntegerMathArgumentsProvider.class) @ArgumentsSource(RealMathArgumentsProvider.class) + @ArgumentsSource(StringArgumentsProvider.class) @ArgumentsSource(StringConcatenationArgumentsProvider.class) @ArgumentsSource(EqualityArgumentsProvider.class) @ArgumentsSource(ComparisonArgumentsProvider.class) @@ -325,7 +338,7 @@ private void assertValue(String data, Object expected) { ExpressionLexer lexer = new ExpressionLexer(); List tokens = lexer.lex(data); - ExpressionParser parser = new ExpressionParser(); + ExpressionParser parser = new ExpressionParser(TextBlockLineEndingMode.CRLF); Expression expression = parser.parse(tokens); ExpressionValue value = expression.evaluate(preprocessor);