diff --git a/.github/workflows/full-check.yml b/.github/workflows/full-check.yml index 0ce36b8411f..c1a277efd9f 100644 --- a/.github/workflows/full-check.yml +++ b/.github/workflows/full-check.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java-version: [ '11', '17', '21' ] + java-version: [ '11', '17', '21', '22' ] steps: - name: Checkout GWT itself into one directory uses: actions/checkout@v4 diff --git a/.github/workflows/quick-check.yml b/.github/workflows/quick-check.yml index 5dfdfd55cc1..09977bd4d68 100644 --- a/.github/workflows/quick-check.yml +++ b/.github/workflows/quick-check.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java-version: ['11', '17', '21'] + java-version: ['11', '17', '21', '22'] steps: - name: Checkout GWT itself into one directory uses: actions/checkout@v4 @@ -42,12 +42,13 @@ jobs: TZ=America/Los_Angeles \ ANT_OPTS=-Dfile.encoding=UTF8 \ ANT_OPTS=-Xmx2g - ant clean dist doc checkstyle apicheck + ant clean compile.tests dist doc checkstyle apicheck - name: Create pull request comments/annotations for checkstyle from the java 21 build, even on failure if: ${{ always() && github.event_name == 'pull_request' && matrix.java-version == '21' }} env: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REVIEWDOG_SKIP_DOGHOUSE: true run: | set -eux shopt -s globstar diff --git a/build.xml b/build.xml index c10eea6f2c5..cea3af60e29 100755 --- a/build.xml +++ b/build.xml @@ -31,9 +31,9 @@ + location="tools/api-checker/config/gwt211_212userapi.conf"/> + value="${gwt.tools}/api-checker-reference/2.11.0/gwt-dev-modified.jar:${gwt.tools}/api-checker-reference/2.11.0/gwt-user-modified.jar"/> diff --git a/build_tools/doctool/src/com/google/doctool/custom/JavaEmulSummaryDoclet.java b/build_tools/doctool/src/com/google/doctool/custom/JavaEmulSummaryDoclet.java index 1b8a5ce21be..ba0d8eb0f27 100644 --- a/build_tools/doctool/src/com/google/doctool/custom/JavaEmulSummaryDoclet.java +++ b/build_tools/doctool/src/com/google/doctool/custom/JavaEmulSummaryDoclet.java @@ -42,6 +42,7 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -79,13 +80,18 @@ public boolean run(DocletEnvironment env) { pw.println("\n"); getSpecifiedPackages(env).forEach(pack -> { + Optional matchingModuleName = ModuleLayer.boot().modules().stream() + .filter(m -> m.getPackages().contains(pack.getQualifiedName().toString())) + .findFirst(); + pw.format("

Package %s

\n", pack.getQualifiedName().toString().replace('.', '_'), pack.getQualifiedName().toString()); pw.println("
"); - String packURL = JAVADOC_URL + pack.getQualifiedName().toString() - .replace(".", "/") + "/"; + String packURL = JAVADOC_URL + + matchingModuleName.map(m -> m.getName() + "/").orElse("") + + pack.getQualifiedName().toString().replace(".", "/") + "/"; Iterator classesIterator = pack.getEnclosedElements() .stream() diff --git a/dev/build.xml b/dev/build.xml index 1b5a2ac9ffc..0b181dd75a9 100755 --- a/dev/build.xml +++ b/dev/build.xml @@ -68,11 +68,12 @@ - + + - + @@ -129,7 +130,8 @@ src="${gwt.tools.lib}/eclipse/org.eclipse.jdt.core_3.32.0.v20221108-1853.jar"/> - + + @@ -140,7 +142,7 @@ - + @@ -227,9 +229,11 @@ + location="${gwt.tools.lib}/guava/guava-33.0/guava-33.0.0-jre-rebased.jar"/> + - + selectionProperties; + private final ModuleDef module; public StandardLinkerContext(TreeLogger logger, ModuleDef module, ResourceOracle publicResourceOracle, JsOutputOption outputOption) @@ -143,6 +144,7 @@ public StandardLinkerContext(TreeLogger logger, ModuleDef module, this.moduleLastModified = module.lastModified(); this.publicResourceOracle = publicResourceOracle; this.outputOption = outputOption; + this.module = module; // Sort the linkers into the order they should actually run. linkerClasses = new ArrayList>(); @@ -537,6 +539,10 @@ public void produceOutput(TreeLogger logger, ArtifactSet artifacts, } } + public ModuleDef getModule() { + return module; + } + /** * (Re)instantiate all linkers. */ diff --git a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java index edffd14cc7f..b56a65d7f4a 100644 --- a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java +++ b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java @@ -15,6 +15,9 @@ */ package com.google.gwt.core.linker; +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + import com.google.gwt.core.ext.LinkerContext; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; @@ -31,17 +34,27 @@ import com.google.gwt.core.ext.linker.SoftPermutation; import com.google.gwt.core.ext.linker.SymbolData; import com.google.gwt.core.ext.linker.SyntheticArtifact; +import com.google.gwt.core.ext.linker.impl.StandardLinkerContext; +import com.google.gwt.dev.cfg.ResourceLoader; +import com.google.gwt.dev.cfg.ResourceLoaders; import com.google.gwt.dev.util.Util; import com.google.gwt.dev.util.collect.HashMap; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; +import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapConsumerV3; import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGeneratorV3; -import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapGeneratorV3.ExtensionMergeAction; +import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapParseException; +import com.google.gwt.thirdparty.guava.common.collect.Maps; +import com.google.gwt.util.tools.Utility; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.net.URISyntaxException; +import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -162,7 +175,8 @@ public static class SourceMapArtifact extends SyntheticArtifact { private final String sourceRoot; public SourceMapArtifact(int permutationId, int fragment, byte[] js, String sourceRoot) { - super(SymbolMapsLinker.class, permutationId + '/' + sourceMapFilenameForFragment(fragment), js); + super(SymbolMapsLinker.class, permutationId + '/' + + sourceMapFilenameForFragment(fragment), js); this.permutationId = permutationId; this.fragment = fragment; this.js = js; @@ -277,6 +291,7 @@ public ArtifactSet link(TreeLogger logger, LinkerContext context, Event writeSourceMapsEvent = SpeedTracerLogger.start(CompilerEventType.WRITE_SOURCE_MAPS); + StandardLinkerContext stdContext = (StandardLinkerContext) context; for (SourceMapArtifact se : artifacts.find(SourceMapArtifact.class)) { // filename is permutation_id/sourceMap.json String sourceMapString = Util.readStreamAsString(se.getContents(logger)); @@ -312,14 +327,16 @@ public ArtifactSet link(TreeLogger logger, LinkerContext context, totalPrefixLines += op.getNumLines(); } } + // TODO(cromwellian): apply insert and remove edits - sourceMapGenerator.mergeMapSection(totalPrefixLines, 0, sourceMapString, - new ExtensionMergeAction() { - @Override - public Object merge(String extKey, Object oldVal, Object newVal) { - return newVal; - } - }); + if (stdContext.getModule().shouldEmbedSourceMapContents()) { + embedSourcesInSourceMaps(logger, stdContext, artifacts, sourceMapGenerator, + totalPrefixLines, sourceMapString, partialPath); + } else { + sourceMapGenerator.mergeMapSection(totalPrefixLines, 0, sourceMapString, + (extKey, oldVal, newVal) -> newVal); + } + StringWriter stringWriter = new StringWriter(); sourceMapGenerator.appendTo(stringWriter, "sourceMap"); emArt = emitSourceMapString(logger, stringWriter.toString(), partialPath); @@ -335,6 +352,68 @@ public Object merge(String extKey, Object oldVal, Object newVal) { return artifacts; } + private static void embedSourcesInSourceMaps(TreeLogger logger, StandardLinkerContext context, + ArtifactSet artifacts, + SourceMapGeneratorV3 sourceMapGenerator, + int totalPrefixLines, String sourceMapString, + String partialPath) + throws SourceMapParseException { + sourceMapGenerator.setStartingPosition(totalPrefixLines, 0); + SourceMapConsumerV3 section = new SourceMapConsumerV3(); + section.parse(sourceMapString); + section.visitMappings(sourceMapGenerator::addMapping); + + for (Entry entry : section.getExtensions().entrySet()) { + String extensionKey = entry.getKey(); + sourceMapGenerator.addExtension(extensionKey, entry.getValue()); + } + + ResourceLoader resourceLoader = ResourceLoaders.fromContextClassLoader(); + + Map generatedSources = Maps.newHashMap(); + artifacts.find(EmittedArtifact.class) + .forEach(emittedArtifact -> { + if (Visibility.Source == emittedArtifact.getVisibility()) { + generatedSources.put(emittedArtifact.getPartialPath(), emittedArtifact); + } + }); + + for (String sourceFileName : section.getOriginalSources()) { + String content; + InputStream cis = null; + try { + cis = loadSource(logger, sourceFileName, generatedSources, + resourceLoader); + if (isNull(cis)) { + cis = context.getModule().findSourceFile(sourceFileName).openContents(); + } + content = Util.readStreamAsString(cis); + sourceMapGenerator.addSourcesContent(sourceFileName, content); + } catch (UnableToCompleteException | URISyntaxException | IOException e) { + logger.log(TreeLogger.Type.WARN, "Can't write source map " + + partialPath, e); + } finally { + Utility.close(cis); + } + } + } + + private static InputStream loadSource(TreeLogger logger, String sourceFileName, + Map generatedSources, + ResourceLoader resourceLoader) + throws UnableToCompleteException, URISyntaxException, IOException { + if (generatedSources.containsKey(sourceFileName)) { + return generatedSources.get(sourceFileName).getContents(logger); + } else { + // ask the resourceOracle for the file contents and add it + URL resource = resourceLoader.getResource(sourceFileName); + if (nonNull(resource)) { + return resource.openStream(); + } + } + return null; + } + /** * Override to change the manner in which the symbol map is emitted. */ diff --git a/dev/core/src/com/google/gwt/dev/CompilePerms.java b/dev/core/src/com/google/gwt/dev/CompilePerms.java index 3b8a1b042f8..2a65b87b77b 100644 --- a/dev/core/src/com/google/gwt/dev/CompilePerms.java +++ b/dev/core/src/com/google/gwt/dev/CompilePerms.java @@ -380,8 +380,12 @@ private boolean precompileAndCompile(TreeLogger logger, String moduleName, if (precompilation == null) { return false; } + + boolean embedSourcesContent = compilerContext.getModule() + .shouldEmbedSourceMapContents(); + // TODO: move to precompile() after params are refactored - if (!options.shouldSaveSource()) { + if (!options.shouldSaveSource() && !embedSourcesContent) { precompilation.removeSourceArtifacts(logger); } diff --git a/dev/core/src/com/google/gwt/dev/Compiler.java b/dev/core/src/com/google/gwt/dev/Compiler.java index 6e122046bde..31a22f8c197 100644 --- a/dev/core/src/com/google/gwt/dev/Compiler.java +++ b/dev/core/src/com/google/gwt/dev/Compiler.java @@ -193,8 +193,11 @@ public static boolean compile( if (precompilation == null) { return false; } + + boolean embedSourcesContent = compilerContext.getModule() + .shouldEmbedSourceMapContents(); // TODO: move to precompile() after params are refactored - if (!options.shouldSaveSource()) { + if (!options.shouldSaveSource() && !embedSourcesContent) { precompilation.removeSourceArtifacts(branch); } diff --git a/dev/core/src/com/google/gwt/dev/Link.java b/dev/core/src/com/google/gwt/dev/Link.java index d7d00625010..a0e02c09db0 100644 --- a/dev/core/src/com/google/gwt/dev/Link.java +++ b/dev/core/src/com/google/gwt/dev/Link.java @@ -415,7 +415,10 @@ private static void doProduceOutput(TreeLogger logger, ArtifactSet artifacts, linkerContext.produceOutput(logger, artifacts, Visibility.Private, extraFileSet); - if (saveSources) { + boolean embedSourcesContent = linkerContext.getModule() + .shouldEmbedSourceMapContents(); + + if (saveSources && !embedSourcesContent) { // Assume that all source code is available in the compiler's classpath. // (This will have to be adjusted to work with Super Dev Mode.) ResourceLoader loader = ResourceLoaders.fromContextClassLoader(); diff --git a/dev/core/src/com/google/gwt/dev/Precompile.java b/dev/core/src/com/google/gwt/dev/Precompile.java index a81bdc80a0c..8386a61f231 100644 --- a/dev/core/src/com/google/gwt/dev/Precompile.java +++ b/dev/core/src/com/google/gwt/dev/Precompile.java @@ -443,7 +443,7 @@ public boolean run(TreeLogger logger) throws UnableToCompleteException { return false; } // TODO: move to precompile() after params are refactored - if (!options.shouldSaveSource()) { + if (!options.shouldSaveSource() && !module.shouldEmbedSourceMapContents()) { precompilation.removeSourceArtifacts(logger); } Util.writeObjectAsFile(logger, precompilationFile, precompilation); diff --git a/dev/core/src/com/google/gwt/dev/PrecompileOnePerm.java b/dev/core/src/com/google/gwt/dev/PrecompileOnePerm.java index dd679c6948d..9a123d6a5c0 100644 --- a/dev/core/src/com/google/gwt/dev/PrecompileOnePerm.java +++ b/dev/core/src/com/google/gwt/dev/PrecompileOnePerm.java @@ -233,8 +233,11 @@ private boolean precompilePermutation(TreeLogger logger, return false; } + boolean embedSourcesContent = compilerContext.getModule() + .shouldEmbedSourceMapContents(); + // TODO: precompile should do this after we get the parameter passing refactored. - if (!options.shouldSaveSource()) { + if (!options.shouldSaveSource() && embedSourcesContent) { precompilation.removeSourceArtifacts(logger); } diff --git a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java index 9b91b3bb41a..ff68743b463 100644 --- a/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java +++ b/dev/core/src/com/google/gwt/dev/cfg/ModuleDef.java @@ -67,6 +67,9 @@ * XML for unit tests. */ public class ModuleDef implements DepsInfoProvider { + + public static final String EMBED_SOURCE_MAPS = "compiler.embedSourceMaps"; + private static final ResourceFilter NON_JAVA_RESOURCES = new ResourceFilter() { @Override public boolean allows(String path) { @@ -628,6 +631,22 @@ public synchronized void setNameOverride(String nameOverride) { this.nameOverride = nameOverride; } + /** + * Checks if embedding sources content inside sourceMaps json is enabled or not. + * @return the boolean value true/false of compiler.embedSourceMaps + * configuration property + */ + public boolean shouldEmbedSourceMapContents() { + return getProperties() + .getConfigurationProperties() + .stream() + .filter(configurationProperty -> EMBED_SOURCE_MAPS.equals( + configurationProperty.getName())) + .findFirst() + .map(configurationProperty -> Boolean.valueOf(configurationProperty.getValue())) + .orElse(false); + } + void addBindingPropertyDefinedValue(BindingProperty bindingProperty, String token) { bindingProperty.addDefinedValue(bindingProperty.getRootCondition(), token); } diff --git a/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java b/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java index aae771dda59..fdec1b1e4dd 100644 --- a/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java +++ b/dev/core/src/com/google/gwt/dev/javac/testing/impl/JavaResourceBase.java @@ -275,6 +275,31 @@ public class JavaResourceBase { " public Class getClass() { return ___clazz; }", "}"); + // This class must exist for JDT to be able to compile record types. + public static final MockJavaResource OBJECTMETHODS = + createMockJavaResource("java.lang.runtime.ObjectMethods", + "package java.lang.runtime;", + "public class ObjectMethods {}"); + + // We only need Objects.hash() for Records - the real JRE would synthesize these methods on the + // fly using ObjectMethods, but we need to generate the code up front. This implementation is + // wrong, but the important thing is only that it exists for these tests. + public static final MockJavaResource OBJECTS = + createMockJavaResource("java.util.Objects", + "package java.util;", + "public class Objects {", + " public static int hash(Object... values) { return values.hashCode(); }", + "}"); + + public static final MockJavaResource RECORD = + createMockJavaResource("java.lang.Record", + "package java.lang;", + "public abstract class Record {", + " protected Record(){}", + " public abstract int hashCode();", + " public abstract boolean equals(Object other);", + " public abstract String toString();", + "}"); public static final MockJavaResource RUNTIME_EXCEPTION = createMockJavaResource("java.lang.RuntimeException", "package java.lang;", @@ -442,10 +467,11 @@ public static MockJavaResource[] getStandardResources() { AUTOCLOSEABLE, ANNOTATION, ARRAY_LIST, BYTE, BOOLEAN, CHARACTER, CHAR_SEQUENCE, CLASS, CLASS_NOT_FOUND_EXCEPTION, CLONEABLE, COLLECTION, COMPARABLE, DOUBLE, ENUM, EXCEPTION, ERROR, FUNCTIONALINTERFACE, FLOAT, INTEGER, IS_SERIALIZABLE, JAVASCRIPTEXCEPTION, - JAVASCRIPTOBJECT, LIST, LONG, MAP, NO_CLASS_DEF_FOUND_ERROR, NUMBER, OBJECT, - RUNTIME_EXCEPTION, SERIALIZABLE, SHORT, STRING, STRING_BUILDER, SUPPRESS_WARNINGS, SYSTEM, - THROWABLE, SPECIALIZE_METHOD, DO_NOT_AUTOBOX, JSTYPE, JSCONSTRUCTOR, JSPACKAGE, JSPROPERTY, - JSMETHOD, JSIGNORE, JSFUNCTION, JSOVERLAY, JSOPTIONAL}; + JAVASCRIPTOBJECT, LIST, LONG, MAP, NO_CLASS_DEF_FOUND_ERROR, NUMBER, OBJECT, OBJECTMETHODS, + OBJECTS, RECORD, RUNTIME_EXCEPTION, SERIALIZABLE, SHORT, STRING, STRING_BUILDER, + SUPPRESS_WARNINGS, SYSTEM, THROWABLE, SPECIALIZE_METHOD, DO_NOT_AUTOBOX, JSTYPE, + JSCONSTRUCTOR, JSPACKAGE, JSPROPERTY, JSMETHOD, JSIGNORE, JSFUNCTION, JSOVERLAY, + JSOPTIONAL}; } /** diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java index 3663c96a94f..91359c61a1b 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java +++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java @@ -84,6 +84,7 @@ import com.google.gwt.dev.jjs.impl.ImplementCastsAndTypeChecks; import com.google.gwt.dev.jjs.impl.ImplementClassLiteralsAsFields; import com.google.gwt.dev.jjs.impl.ImplementJsVarargs; +import com.google.gwt.dev.jjs.impl.ImplementRecordComponents; import com.google.gwt.dev.jjs.impl.JavaAstVerifier; import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap; import com.google.gwt.dev.jjs.impl.JjsUtils; @@ -1158,6 +1159,8 @@ private UnifiedAst precompile(PrecompilationContext precompilationContext) // Replace calls to native overrides of object methods. ReplaceCallsToNativeJavaLangObjectOverrides.exec(jprogram); + ImplementRecordComponents.exec(jprogram); + FixAssignmentsToUnboxOrCast.exec(jprogram); if (options.isEnableAssertions()) { AssertionNormalizer.exec(jprogram); @@ -1203,6 +1206,7 @@ private String buildEntryMethodHolder(StandardGeneratorContext context, } EntryMethodHolderGenerator entryMethodHolderGenerator = new EntryMethodHolderGenerator(); + context.setCurrentGenerator(EntryMethodHolderGenerator.class); String entryMethodHolderTypeName = entryMethodHolderGenerator.generate(logger, context, module.getCanonicalName()); context.finish(logger); diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/HasJsInfo.java b/dev/core/src/com/google/gwt/dev/jjs/ast/HasJsInfo.java index 05a6f9e8dda..ec24041b3f5 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/ast/HasJsInfo.java +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/HasJsInfo.java @@ -60,6 +60,9 @@ public String computeName(JMember member) { @Override public String computeName(JMember member) { String methodName = member.getName(); + if (member.getEnclosingType() instanceof JRecordType) { + return methodName; + } if (startsWithCamelCase(methodName, "get")) { return Introspector.decapitalize(methodName.substring(3)); } diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java index 2811ac4bdd6..6764157ab48 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java @@ -1267,7 +1267,8 @@ private static Set buildInitialTypeNamesToIndex() { JAVASCRIPTOBJECT, CLASS_LITERAL_HOLDER, "com.google.gwt.core.client.RunAsyncCallback", "com.google.gwt.core.client.impl.AsyncFragmentLoader", "com.google.gwt.core.client.impl.Impl", - "com.google.gwt.core.client.prefetch.RunAsyncCode")); + "com.google.gwt.core.client.prefetch.RunAsyncCode", + "java.util.Objects")); typeNamesToIndex.addAll(CODEGEN_TYPES_SET); return typeNamesToIndex; } diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JRecordType.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JRecordType.java new file mode 100644 index 00000000000..974fece4309 --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JRecordType.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.dev.jjs.ast; + +import com.google.gwt.dev.jjs.SourceInfo; + +/** + * Java record type reference expression. At this time, there is no AST node for components, + * instead using fields, since records can only have fields declared for components. + */ +public class JRecordType extends JClassType { + public JRecordType(SourceInfo info, String name) { + super(info, name, false, true); + } +} diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java b/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java index 15f5b8e2487..5dc68da4322 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java +++ b/dev/core/src/com/google/gwt/dev/jjs/ast/RuntimeConstants.java @@ -43,6 +43,8 @@ public class RuntimeConstants { public static final String CLASS_CREATE_FOR_PRIMITIVE = "Class.createForPrimitive"; public static final String CLASS_CREATE_FOR_INTERFACE = "Class.createForInterface"; + public static final String CLASS_GET_SIMPLE_NAME = "Class.getSimpleName"; + public static final String COLLAPSED_PROPERTY_HOLDER_GET_PERMUTATION_ID = "CollapsedPropertyHolder.getPermutationId"; @@ -97,4 +99,6 @@ public class RuntimeConstants { public static final String RUNTIME_UNIQUE_ID = "Runtime.uniqueId"; public static final String UTIL_MAKE_ENUM_NAME = "Util.makeEnumName"; + + public static final String OBJECTS_HASH = "Objects.hash"; } diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java index 010e320b9ef..91ef6a00afc 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java @@ -82,6 +82,7 @@ import com.google.gwt.dev.jjs.ast.JPrefixOperation; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JProgram; +import com.google.gwt.dev.jjs.ast.JRecordType; import com.google.gwt.dev.jjs.ast.JReferenceType; import com.google.gwt.dev.jjs.ast.JReturnStatement; import com.google.gwt.dev.jjs.ast.JStatement; @@ -225,6 +226,7 @@ import org.eclipse.jdt.internal.compiler.lookup.MethodScope; import org.eclipse.jdt.internal.compiler.lookup.MethodVerifier; import org.eclipse.jdt.internal.compiler.lookup.NestedTypeBinding; +import org.eclipse.jdt.internal.compiler.lookup.RecordComponentBinding; import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding; import org.eclipse.jdt.internal.compiler.lookup.Scope; import org.eclipse.jdt.internal.compiler.lookup.SourceTypeBinding; @@ -2863,12 +2865,6 @@ protected void pushBinaryOp(BinaryExpression x, JBinaryOperator op) { } protected boolean visit(TypeDeclaration x) { - if (x.isRecord()) { - InternalCompilerException exception = - new InternalCompilerException("Records not yet supported"); - exception.addNode(new JClassType(makeSourceInfo(x), intern(x.name), false, false)); - throw exception; - } JDeclaredType type = (JDeclaredType) typeMap.get(x.binding); assert !type.isExternal(); classStack.push(curClass); @@ -3346,13 +3342,13 @@ private List popCallArguments(SourceInfo info, Expression[] argumen List args = pop(arguments); for (int i = 0; i < args.size(); i++) { // Account for varargs parameter. - int parameterIndex = Math.min(i, methodBinding.parameters.length - 1); + int parameterIndex = Math.min(i, methodBinding.original().parameters.length - 1); args.set(i, maybeBoxOrUnbox( args.get(i), arguments[i].implicitConversion, isDoNotAutoBoxParameter(methodBinding, parameterIndex))); } - if (!methodBinding.isVarargs()) { + if (!methodBinding.original().isVarargs()) { return args; } @@ -3362,7 +3358,8 @@ private List popCallArguments(SourceInfo info, Expression[] argumen args = Lists.newArrayListWithCapacity(1); } - TypeBinding[] params = methodBinding.parameters; + TypeBinding[] params = methodBinding.isVarargs() ? methodBinding.parameters : + methodBinding.original().parameters; int varArg = params.length - 1; // See if there's a single varArg which is already an array. @@ -4209,8 +4206,14 @@ private void createField(FieldDeclaration x) { getFieldDisposition(binding), AccessModifier.fromFieldBinding(binding)); } enclosingType.addField(field); - JsInteropUtil.maybeSetJsInteropProperties(field, shouldExport(field), x.annotations); - processSuppressedWarnings(field, x.annotations); + if (x.isARecordComponent) { + // Skip setting jsinterop properties on record component fields + RecordComponentBinding component = ((SourceTypeBinding) binding.declaringClass).getRecordComponent(x.name); + processSuppressedWarnings(field, component.sourceRecordComponent().annotations); + } else { + JsInteropUtil.maybeSetJsInteropProperties(field, shouldExport(field), x.annotations); + processSuppressedWarnings(field, x.annotations); + } typeMap.setField(binding, field); } @@ -4219,7 +4222,7 @@ private void createMembers(TypeDeclaration x) { JDeclaredType type = (JDeclaredType) typeMap.get(binding); SourceInfo info = type.getSourceInfo(); try { - /** + /* * We emulate static initializers and instance initializers as methods. As * in other cases, this gives us: simpler AST, easier to optimize, more * like output JavaScript. Clinit is always in slot 0, init (if it exists) @@ -4274,6 +4277,35 @@ private void createMembers(TypeDeclaration x) { } } + if (x.isRecord()) { + // build implicit record component accessor methods, JDT doesn't declare them + for (JField field : type.getFields()) { + // Create a method binding that corresponds to the method we are creating, jdt won't + // offer us one unless it was defined in source. + char[] fieldName = field.getName().toCharArray(); + MethodBinding recordComponentAccessor = binding.getExactMethod( + fieldName, new TypeBinding[0], curCud.scope); + + // Get the record component, and pass on any annotations meant for the method + JMethod componentMethod = typeMap.get(recordComponentAccessor); + RecordComponentBinding component = binding.getRecordComponent(fieldName); + processAnnotations(component.sourceRecordComponent().annotations, componentMethod); + } + + // At this time, we need to be sure a binding exists, either because the record declared + // its own, or we make one specifically for it. + MethodBinding toStringBinding = binding.getExactMethod( + TO_STRING_METHOD_NAME.toCharArray(), Binding.NO_TYPES, curCud.scope); + typeMap.get(toStringBinding); + TypeBinding[] equalsArgs = {x.scope.getJavaLangObject()}; + MethodBinding equalsBinding = binding.getExactMethod( + EQUALS_METHOD_NAME.toCharArray(), equalsArgs, curCud.scope); + typeMap.get(equalsBinding); + MethodBinding hashcodeBinding = binding.getExactMethod( + HASHCODE_METHOD_NAME.toCharArray(), Binding.NO_TYPES, curCud.scope); + typeMap.get(hashcodeBinding); + } + if (x.memberTypes != null) { for (TypeDeclaration memberType : x.memberTypes) { createMembers(memberType); @@ -4380,17 +4412,16 @@ private void createMethod(AbstractMethodDeclaration x) { } enclosingType.addMethod(method); - processAnnotations(x, method); + processAnnotations(x.annotations, method); typeMap.setMethod(b, method); } - private void processAnnotations(AbstractMethodDeclaration x, - JMethod method) { - maybeAddMethodSpecialization(x, method); - maybeSetInliningMode(x, method); - maybeSetHasNoSideEffects(x, method); - JsInteropUtil.maybeSetJsInteropProperties(method, shouldExport(method), x.annotations); - processSuppressedWarnings(method, x.annotations); + private void processAnnotations(Annotation[] annotations, JMethod method) { + maybeAddMethodSpecialization(annotations, method); + maybeSetInliningMode(annotations, method); + maybeSetHasNoSideEffects(annotations, method); + JsInteropUtil.maybeSetJsInteropProperties(method, shouldExport(method), annotations); + processSuppressedWarnings(method, annotations); } private void processAnnotations(JParameter parameter, Annotation... annotations) { @@ -4398,7 +4429,7 @@ private void processAnnotations(JParameter parameter, Annotation... annotations) processSuppressedWarnings(parameter, annotations); } - private void processSuppressedWarnings(CanHaveSuppressedWarnings x, Annotation... annotations) { + private static void processSuppressedWarnings(CanHaveSuppressedWarnings x, Annotation... annotations) { x.setSuppressedWarnings(JdtUtil.getSuppressedWarnings(annotations)); } @@ -4410,26 +4441,26 @@ private static boolean isUncheckedGenericMethodCall(MessageSend messageSend) { return false; } - private static void maybeSetInliningMode(AbstractMethodDeclaration x, JMethod method) { + private static void maybeSetInliningMode(Annotation[] annotations, JMethod method) { if (JdtUtil.getAnnotationByName( - x.annotations, "javaemul.internal.annotations.DoNotInline") != null) { + annotations, "javaemul.internal.annotations.DoNotInline") != null) { method.setInliningMode(InliningMode.DO_NOT_INLINE); } else if (JdtUtil.getAnnotationByName( - x.annotations, "javaemul.internal.annotations.ForceInline") != null) { + annotations, "javaemul.internal.annotations.ForceInline") != null) { method.setInliningMode(InliningMode.FORCE_INLINE); } } - private static void maybeSetHasNoSideEffects(AbstractMethodDeclaration x, JMethod method) { + private static void maybeSetHasNoSideEffects(Annotation[] annotations, JMethod method) { if (JdtUtil.getAnnotationByName( - x.annotations, "javaemul.internal.annotations.HasNoSideEffects") != null) { + annotations, "javaemul.internal.annotations.HasNoSideEffects") != null) { method.setHasSideEffects(false); } } - private void maybeAddMethodSpecialization(AbstractMethodDeclaration x, JMethod method) { + private void maybeAddMethodSpecialization(Annotation[] annotations, JMethod method) { AnnotationBinding specializeAnnotation = JdtUtil.getAnnotationByName( - x.annotations, "javaemul.internal.annotations.SpecializeMethod"); + annotations, "javaemul.internal.annotations.SpecializeMethod"); if (specializeAnnotation == null) { return; } @@ -4508,8 +4539,12 @@ private void createTypes(TypeDeclaration x) { JDeclaredType type; if (binding.isClass()) { - type = new JClassType( - info, name, binding.isAbstract(), binding.isFinal() || binding.isAnonymousType()); + if (binding.isRecord()) { + type = new JRecordType(info, name); + } else { + type = new JClassType( + info, name, binding.isAbstract(), binding.isFinal() || binding.isAnonymousType()); + } } else if (binding.isInterface() || binding.isAnnotationType()) { type = new JInterfaceType(info, name); } else if (binding.isEnum()) { diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementClassLiteralsAsFields.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementClassLiteralsAsFields.java index 3006bda578b..598218a9ce1 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementClassLiteralsAsFields.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementClassLiteralsAsFields.java @@ -37,6 +37,7 @@ import com.google.gwt.dev.jjs.ast.JParameter; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JProgram; +import com.google.gwt.dev.jjs.ast.JRecordType; import com.google.gwt.dev.jjs.ast.JReferenceType; import com.google.gwt.dev.jjs.ast.JRuntimeTypeReference; import com.google.gwt.dev.jjs.ast.JType; @@ -87,6 +88,7 @@ public class ImplementClassLiteralsAsFields { literalFactoryMethodByTypeClass = new ImmutableMap.Builder() .put(JEnumType.class, ClassLiteralFactoryMethod.CREATE_FOR_ENUM) .put(JClassType.class, ClassLiteralFactoryMethod.CREATE_FOR_CLASS) + .put(JRecordType.class, ClassLiteralFactoryMethod.CREATE_FOR_CLASS) .put(JInterfaceType.class, ClassLiteralFactoryMethod.CREATE_FOR_INTERFACE) .put(JPrimitiveType.class, ClassLiteralFactoryMethod.CREATE_FOR_PRIMITIVE) .build(); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementJsVarargs.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementJsVarargs.java index 9c86dc00cda..14794e867bb 100644 --- a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementJsVarargs.java +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementJsVarargs.java @@ -296,7 +296,7 @@ public boolean visit(JMethod x, Context ctx) { varargsParameter = Iterables.getLast(x.getParams()); varargsIndex = x.getParams().size() - 1; - // JsVarargs parameter can be assumend not null in the implementing method + // JsVarargs parameter can be assumed not null in the implementing method varargsParameter.setType(varargsParameter.getType().strengthenToNonNull()); argumentsCopyVariable = null; @@ -462,8 +462,8 @@ public void endVisit(JMethodCall x, Context ctx) { // Passed as an array to varargs method will result in an apply call, in which case hoist the // qualifier to make sure it is only evaluated once. JExpression instance = x.getInstance(); - if (x.getTarget().needsDynamicDispatch() && !x.isStaticDispatchOnly() - && instance != null && !(instance instanceof JVariableRef)) { + if (x.getTarget().needsDynamicDispatch() && !x.isStaticDispatchOnly() && instance != null + && !(instance instanceof JVariableRef && !instance.hasSideEffects())) { // Move the potentially sideffecting qualifier to a temporary variable so that // the code generation for calls that need .apply don't need to hande the case. SourceInfo sourceInfo = x.getSourceInfo(); diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java new file mode 100644 index 00000000000..c0eee4e2435 --- /dev/null +++ b/dev/core/src/com/google/gwt/dev/jjs/impl/ImplementRecordComponents.java @@ -0,0 +1,249 @@ +/* + * Copyright 2024 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.dev.jjs.impl; + +import com.google.gwt.dev.jjs.SourceInfo; +import com.google.gwt.dev.jjs.ast.JBinaryOperation; +import com.google.gwt.dev.jjs.ast.JBinaryOperator; +import com.google.gwt.dev.jjs.ast.JBooleanLiteral; +import com.google.gwt.dev.jjs.ast.JClassLiteral; +import com.google.gwt.dev.jjs.ast.JClassType; +import com.google.gwt.dev.jjs.ast.JDeclaredType; +import com.google.gwt.dev.jjs.ast.JExpression; +import com.google.gwt.dev.jjs.ast.JField; +import com.google.gwt.dev.jjs.ast.JFieldRef; +import com.google.gwt.dev.jjs.ast.JIfStatement; +import com.google.gwt.dev.jjs.ast.JIntLiteral; +import com.google.gwt.dev.jjs.ast.JLocal; +import com.google.gwt.dev.jjs.ast.JMethod; +import com.google.gwt.dev.jjs.ast.JMethodBody; +import com.google.gwt.dev.jjs.ast.JMethodCall; +import com.google.gwt.dev.jjs.ast.JNewArray; +import com.google.gwt.dev.jjs.ast.JNullLiteral; +import com.google.gwt.dev.jjs.ast.JParameter; +import com.google.gwt.dev.jjs.ast.JPrimitiveType; +import com.google.gwt.dev.jjs.ast.JProgram; +import com.google.gwt.dev.jjs.ast.JRecordType; +import com.google.gwt.dev.jjs.ast.JStringLiteral; +import com.google.gwt.dev.jjs.ast.JThisRef; +import com.google.gwt.dev.jjs.ast.JUnsafeTypeCoercion; +import com.google.gwt.dev.jjs.ast.RuntimeConstants; +import com.google.gwt.thirdparty.guava.common.collect.Lists; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Implements the methods required for a Java Record type, based on its components/fields. + */ +public class ImplementRecordComponents { + + public static void exec(JProgram program) { + new ImplementRecordComponents(program).execImpl(); + } + + private final JProgram program; + private final AutoboxUtils autoboxUtils; + private final JMethod getClassMethod; + private final JMethod getSimpleNameMethod; + private final JClassType javaLangString; + + private ImplementRecordComponents(JProgram program) { + this.program = program; + this.autoboxUtils = new AutoboxUtils(program); + + getClassMethod = program.getIndexedMethod(RuntimeConstants.OBJECT_GET_CLASS); + getSimpleNameMethod = program.getIndexedMethod(RuntimeConstants.CLASS_GET_SIMPLE_NAME); + javaLangString = program.getTypeJavaLangString(); + } + + private void execImpl() { + for (JDeclaredType type : program.getDeclaredTypes()) { + if (type instanceof JRecordType) { + implementRecordComponents((JRecordType) type); + } + } + } + + private void implementRecordComponents(JRecordType type) { + // This is a record type, and any methods that were declared but not referenced now need + // to be defined. These include the field-named method accessors, equals/hashCode and + // toString. If not defined, we'll synthesize them based on the record components. + SourceInfo info = type.getSourceInfo(); + for (JMethod method : type.getMethods()) { + if (method.getBody() != null) { + // If there is a body, that means the record has its own declaration of this method, and + // we should not re-declare it. + continue; + } + + if (method.getName().equals(GwtAstBuilder.TO_STRING_METHOD_NAME) + && method.getParams().isEmpty()) { + implementToString(type, method, info); + } else if (method.getName().equals(GwtAstBuilder.EQUALS_METHOD_NAME) + && method.getParams().size() == 1 + && method.getParams().get(0).getType().equals(program.getTypeJavaLangObject())) { + implementEquals(type, method, info); + } else if (method.getName().equals(GwtAstBuilder.HASHCODE_METHOD_NAME) + && method.getParams().isEmpty()) { + implementHashCode(type, method, info); + } else if (method.getParams().isEmpty()) { + // Check if it has the same name+type as a component/field + Optional matchingField = type.getFields().stream() + .filter(f -> f.getName().equals(method.getName())) + .filter(f -> f.getType().equals(method.getType())) + .findFirst(); + matchingField.ifPresent(f -> implementComponentAccessor(type, method, f)); + } + } + } + + private static void implementComponentAccessor(JRecordType type, JMethod method, JField field) { + // We can pick a more specific source for this than the others, use the "field" itself. + SourceInfo info = field.getSourceInfo(); + + // Create a simple accessor method and bind it, so it can be used anywhere outside this type. + JFieldRef fieldReference = new JFieldRef(info, new JThisRef(info, type), + field, type); + JMethodBody body = new JMethodBody(info); + body.getBlock().addStmt(fieldReference.makeReturnStatement()); + method.setBody(body); + } + + private void implementHashCode(JRecordType type, JMethod method, SourceInfo info) { + final JExpression hashcodeStatement; + if (type.getFields().isEmpty()) { + // No fields, just emit hashcode=0 + hashcodeStatement = new JIntLiteral(info, 0); + } else { + List exprs = Lists.newArrayListWithCapacity(type.getFields().size()); + for (JField field : type.getFields()) { + JFieldRef jFieldRef = new JFieldRef(info, new JThisRef(info, type), field, type); + if (jFieldRef.getType().isPrimitiveType()) { + exprs.add(autoboxUtils.box(jFieldRef, (JPrimitiveType) jFieldRef.getType())); + } else { + exprs.add(jFieldRef); + } + } + JNewArray varargsWrapper = JNewArray.createArrayWithInitializers(info, + program.getTypeJavaLangObjectArray(), exprs); + JMethod hash = program.getIndexedMethod(RuntimeConstants.OBJECTS_HASH); + hashcodeStatement = new JMethodCall(info, null, hash, varargsWrapper); + } + JMethodBody body = new JMethodBody(info); + body.getBlock().addStmt(hashcodeStatement.makeReturnStatement()); + + method.setBody(body); + } + + private void implementEquals(JRecordType type, JMethod method, SourceInfo info) { + JMethodBody body = new JMethodBody(info); + JParameter otherParam = method.getParams().get(0); + + // Equals is built from first == check between this and other, as a fast path for the same + // object and also to ensure that other isn't null. Then MyRecord.class == other.getClass(), + // and now we know they're the same type and can cast safely to access fields for the rest. + JBinaryOperation eq = + new JBinaryOperation(info, JPrimitiveType.BOOLEAN, JBinaryOperator.EQ, + new JThisRef(info, type), + otherParam.createRef(info)); + body.getBlock().addStmt(new JIfStatement(info, eq, + JBooleanLiteral.TRUE.makeReturnStatement(), null)); + + JBinaryOperation sameTypeCheck = + new JBinaryOperation(info, JPrimitiveType.BOOLEAN, JBinaryOperator.NEQ, + new JClassLiteral(info, type), + new JMethodCall(info, otherParam.createRef(info), getClassMethod)); + body.getBlock().addStmt(new JIfStatement(info, sameTypeCheck, + JBooleanLiteral.FALSE.makeReturnStatement(), null)); + + // Create a local to assign to and compare each component + JLocal typedOther = JProgram.createLocal(info, "other", type, true, body); + // We can use an unsafe cast since we know the check will succeed + JUnsafeTypeCoercion uncheckedCast = + new JUnsafeTypeCoercion(info, type, otherParam.createRef(info)); + JBinaryOperation uncheckedAssign = new JBinaryOperation(info, type, JBinaryOperator.ASG, + typedOther.createRef(info), uncheckedCast); + body.getBlock().addStmt(uncheckedAssign.makeStatement()); + + JExpression componentCheck = JBooleanLiteral.TRUE; + JMethod objectEquals = program.getIndexedMethod(RuntimeConstants.OBJECT_EQUALS); + for (JField field : type.getFields()) { + if (!field.isStatic()) { + JFieldRef myField = new JFieldRef(info, new JThisRef(info, type), field, type); + JFieldRef otherField = new JFieldRef(info, typedOther.createRef(info), field, type); + final JBinaryOperation equals; + if (field.getType().isPrimitiveType()) { + equals = new JBinaryOperation(info, JPrimitiveType.BOOLEAN, + JBinaryOperator.EQ, + myField, + otherField); + } else { + // we would like to use Objects.equals here to be more consise, but we would need + // to look up the right impl based on the field - just as simple to insert a null check + // and get it a little closer to all being inlined away + equals = new JBinaryOperation(info, JPrimitiveType.BOOLEAN, JBinaryOperator.AND, + new JBinaryOperation(info, JPrimitiveType.BOOLEAN, JBinaryOperator.NEQ, + myField, JNullLiteral.INSTANCE), + new JMethodCall(info, myField, objectEquals, otherField)); + } + if (componentCheck != JBooleanLiteral.TRUE) { + componentCheck = new JBinaryOperation(info, JPrimitiveType.BOOLEAN, + JBinaryOperator.AND, + componentCheck, + equals); + } else { + componentCheck = equals; + } + } + } + + body.getBlock().addStmt(componentCheck.makeReturnStatement()); + method.setBody(body); + } + + private void implementToString(JRecordType type, JMethod method, SourceInfo info) { + List args = new ArrayList<>(); + + // Concatenate type with []s and component values, assigning to toStrExpr as we append + // more concat operations. Using getClass().getSimpleName() rather than a string literal + // allows the toString implementation to emit the obfuscated class name. + JMethodCall getClass = new JMethodCall(info, new JThisRef(info, type), getClassMethod); + JExpression toStrExpr = new JMethodCall(info, getClass, getSimpleNameMethod); + + args.add(new JStringLiteral(info, "[", javaLangString)); + List fields = type.getFields(); + for (int i = 0; i < fields.size(); i++) { + if (i != 0) { + args.add(new JStringLiteral(info, ", ", javaLangString)); + } + JField field = fields.get(i); + args.add(new JStringLiteral(info, field.getName() + "=", javaLangString)); + args.add(new JFieldRef(info, new JThisRef(info, type), field, type)); + } + args.add(new JStringLiteral(info, "]", javaLangString)); + for (JExpression arg : args) { + toStrExpr = new JBinaryOperation(info, javaLangString, JBinaryOperator.CONCAT, + toStrExpr, + arg); + } + + JMethodBody body = new JMethodBody(info); + body.getBlock().addStmt(toStrExpr.makeReturnStatement()); + method.setBody(body); + } +} diff --git a/dev/core/src/com/google/gwt/dev/js/JsSafeCloner.java b/dev/core/src/com/google/gwt/dev/js/JsSafeCloner.java index 39b7452e912..009c5269f56 100644 --- a/dev/core/src/com/google/gwt/dev/js/JsSafeCloner.java +++ b/dev/core/src/com/google/gwt/dev/js/JsSafeCloner.java @@ -143,13 +143,17 @@ public void endVisit(JsNameOf x, JsContext ctx) { */ @Override public void endVisit(JsNameRef x, JsContext ctx) { - if (x.getQualifier() == null && x.getIdent() == "arguments") { + if (x.getQualifier() == null && "arguments".equals(x.getIdent())) { // References to the arguments object can not be hoisted. successful = false; stack.push(null); } - JsNameRef toReturn = new JsNameRef(x.getSourceInfo(), x.getName()); - + final JsNameRef toReturn; + if (x.getName() == null) { + toReturn = new JsNameRef(x.getSourceInfo(), x.getIdent(), x.getQualifier()); + } else { + toReturn = new JsNameRef(x.getSourceInfo(), x.getName()); + } if (x.getQualifier() != null) { toReturn.setQualifier(stack.pop()); } diff --git a/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameFinderTest.java b/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameFinderTest.java index b878acf3ecf..65926826dc2 100644 --- a/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameFinderTest.java +++ b/dev/core/test/com/google/gwt/dev/javac/GeneratedClassnameFinderTest.java @@ -107,35 +107,41 @@ public void testEnum() { public void testJavacWeirdness() { List classNames = new JavacWeirdnessTester().getGeneratedClasses(); - if (classNames.size() == 3) { - // javac7 - JavacWeirdnessTester$1 doesn't verify, so it's excluded - assertTrue(classNames.get(0) + " should not contain Foo", - classNames.get(0).indexOf("Foo") == -1); - assertTrue(classNames.get(1) + " should contain Foo", - classNames.get(1).indexOf("Foo") != -1); - assertTrue(classNames.get(2) + " should contain Foo", - classNames.get(2).indexOf("Foo") != -1); - } else if (classNames.size() == 4) { + if (classNames.size() == 4) { // javac8: // JavacWeirdnessTester$1 // JavacWeirdnessTester$2 // JavacWeirdnessTester$2Foo // JavacWeirdnessTester$3Foo - assertTrue(classNames.get(0) + " should not contain Foo", - classNames.get(0).indexOf("Foo") == -1); - assertTrue(classNames.get(1) + " should not contain Foo", - classNames.get(1).indexOf("Foo") == -1); - assertTrue(classNames.get(2) + " should contain Foo", - classNames.get(2).indexOf("Foo") != -1); - assertTrue(classNames.get(3) + " should contain Foo", - classNames.get(3).indexOf("Foo") != -1); + assertFalse(classNames.get(0) + " should not contain Foo", + classNames.get(0).contains("Foo")); + assertFalse(classNames.get(1) + " should not contain Foo", + classNames.get(1).contains("Foo")); + assertTrue(classNames.get(2) + " should contain Foo", classNames.get(2).contains("Foo")); + assertTrue(classNames.get(3) + " should contain Foo", classNames.get(3).contains("Foo")); + } else if (classNames.size() == 5) { + // javac22: + // JavacWeirdnessTester$1 + // JavacWeirdnessTester$2 + // JavacWeirdnessTester$1Foo + // JavacWeirdnessTester$2Foo + // JavacWeirdnessTester$3Foo + assertFalse(classNames.get(0) + " should not contain Foo", + classNames.get(0).contains("Foo")); + assertFalse(classNames.get(1) + " should not contain Foo", + classNames.get(1).contains("Foo")); + assertTrue(classNames.get(2) + " should contain Foo", classNames.get(2).contains("Foo")); + assertTrue(classNames.get(3) + " should contain Foo", classNames.get(2).contains("Foo")); + assertTrue(classNames.get(4) + " should contain Foo", classNames.get(3).contains("Foo")); } else { - fail(); + fail("Expected 4 or 5 classes, found " + classNames); } } public void testNamedLocal() { - assertEquals(2, new NamedLocalTester().getGeneratedClasses().size()); + List generatedClasses = new NamedLocalTester().getGeneratedClasses(); + assertTrue(generatedClasses.toString(), + generatedClasses.size() == 2 || generatedClasses.size() == 3); } public void testNested() { diff --git a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java index d6a66d7484d..32ce51b7cf3 100644 --- a/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java +++ b/dev/core/test/com/google/gwt/dev/jjs/JavaAstConstructor.java @@ -155,6 +155,7 @@ public CharSequence getContent() { " static boolean isClassMetadataEnabled() { return true; }", " public boolean desiredAssertionStatus() { return true; }", " public String getName() { return null; }", + " public String getSimpleName() { return null; }", "}" ); } diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitorTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitorTest.java index 1374bd97bfd..fc90562c55c 100644 --- a/dev/core/test/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitorTest.java +++ b/dev/core/test/com/google/gwt/dev/jjs/impl/JChangeTrackingVisitorTest.java @@ -22,6 +22,7 @@ import com.google.gwt.dev.jjs.ast.JDeclaredType; import com.google.gwt.dev.jjs.ast.JField; import com.google.gwt.dev.jjs.ast.JMethod; +import com.google.gwt.dev.jjs.ast.JParameter; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JVariable; @@ -31,6 +32,7 @@ import com.google.gwt.thirdparty.guava.common.collect.Sets; import java.util.Arrays; +import java.util.List; import java.util.Set; /** @@ -46,7 +48,10 @@ public AddParamWhenEnterMethodVisitor(OptimizerContext optimizerCtx) { @Override public boolean enter(JMethod x, Context ctx) { - x.createParameter(SourceOrigin.UNKNOWN, "_newParam_enter", JPrimitiveType.INT); + // Don't add args at the end of varargs methods + if (isNotVarargs(x)) { + x.createParameter(SourceOrigin.UNKNOWN, "_newParam_enter", JPrimitiveType.INT); + } return true; } } @@ -58,10 +63,18 @@ public AddParamWhenExitMethodVisitor(OptimizerContext optimizerCtx) { @Override public void exit(JMethod x, Context ctx) { - x.createParameter(SourceOrigin.UNKNOWN, "_newParam_exit", JPrimitiveType.INT); + // Don't add args at the end of varargs methods + if (isNotVarargs(x)) { + x.createParameter(SourceOrigin.UNKNOWN, "_newParam_exit", JPrimitiveType.INT); + } } } + private static boolean isNotVarargs(JMethod x) { + List params = x.getParams(); + return params.isEmpty() || !params.get(params.size() - 1).isVarargs(); + } + private static final class AddParamsWhenEnterAndExitMethodVisitor extends JChangeTrackingVisitor { public AddParamsWhenEnterAndExitMethodVisitor(OptimizerContext optimizerCtx) { super(optimizerCtx); @@ -69,13 +82,19 @@ public AddParamsWhenEnterAndExitMethodVisitor(OptimizerContext optimizerCtx) { @Override public boolean enter(JMethod x, Context ctx) { - x.createParameter(SourceOrigin.UNKNOWN, "_newParam_enter", JPrimitiveType.INT); + // Don't add args at the end of varargs methods + if (isNotVarargs(x)) { + x.createParameter(SourceOrigin.UNKNOWN, "_newParam_enter", JPrimitiveType.INT); + } return true; } @Override public void exit(JMethod x, Context ctx) { - x.createParameter(SourceOrigin.UNKNOWN, "_newParam_exit", JPrimitiveType.INT); + // Don't add args at the end of varargs methods + if (isNotVarargs(x)) { + x.createParameter(SourceOrigin.UNKNOWN, "_newParam_exit", JPrimitiveType.INT); + } } } @@ -87,7 +106,10 @@ public AddParamWhenEnterNonConstructorMethodVisitor(OptimizerContext optimizerCt @Override public boolean enter(JMethod x, Context ctx) { - x.createParameter(SourceOrigin.UNKNOWN, "_newParam_enter", JPrimitiveType.INT); + // Don't add args at the end of varargs methods + if (isNotVarargs(x)) { + x.createParameter(SourceOrigin.UNKNOWN, "_newParam_enter", JPrimitiveType.INT); + } return true; } @@ -105,7 +127,10 @@ public AddParamWhenExitNonConstructorMethodVisitor(OptimizerContext optimizerCtx @Override public void exit(JMethod x, Context ctx) { - x.createParameter(SourceOrigin.UNKNOWN, "_newParam_exit", JPrimitiveType.INT); + // Don't add args at the end of varargs methods + if (isNotVarargs(x)) { + x.createParameter(SourceOrigin.UNKNOWN, "_newParam_exit", JPrimitiveType.INT); + } } @Override diff --git a/dev/core/test/com/google/gwt/dev/jjs/impl/Java17AstTest.java b/dev/core/test/com/google/gwt/dev/jjs/impl/Java17AstTest.java index fa18477e096..4e855c5cedb 100644 --- a/dev/core/test/com/google/gwt/dev/jjs/impl/Java17AstTest.java +++ b/dev/core/test/com/google/gwt/dev/jjs/impl/Java17AstTest.java @@ -134,18 +134,9 @@ public void testSealedClassesNotPermitted() { } } - public void testRecordsNotSupported() { - try { - addSnippetClassDecl("public record Point(int x, int y) {}"); - compileSnippet("void", "Point rectangle = new Point(0, 0);"); - fail("Compile should have failed but succeeded."); - } catch (Exception e) { - if (!(e.getCause() instanceof UnableToCompleteException) - && !(e instanceof UnableToCompleteException)) { - e.printStackTrace(); - fail(); - } - } + public void testRecordSyntax() throws UnableToCompleteException { + addSnippetClassDecl("public record Point(int x, int y) {}"); + compileSnippet("void", "Point rectangle = new Point(0, 0);"); } public void testSwitchExpressions() throws UnableToCompleteException { diff --git a/eclipse/dev/.classpath b/eclipse/dev/.classpath index ac771cbca58..a84d801f71e 100644 --- a/eclipse/dev/.classpath +++ b/eclipse/dev/.classpath @@ -21,7 +21,7 @@ - + diff --git a/eclipse/dev/codeserver/.classpath b/eclipse/dev/codeserver/.classpath index 88a6968b93b..bc64bf5c128 100644 --- a/eclipse/dev/codeserver/.classpath +++ b/eclipse/dev/codeserver/.classpath @@ -3,7 +3,7 @@ - + diff --git a/eclipse/user/.classpath b/eclipse/user/.classpath index 75f936204d0..1fa63d0ffcc 100644 --- a/eclipse/user/.classpath +++ b/eclipse/user/.classpath @@ -44,7 +44,7 @@ - + diff --git a/requestfactory/build.xml b/requestfactory/build.xml index c835c86da4e..ba82e804e35 100755 --- a/requestfactory/build.xml +++ b/requestfactory/build.xml @@ -41,6 +41,9 @@ + + + diff --git a/servlet/build.xml b/servlet/build.xml index 47d554892f1..9f23dfcdf5f 100644 --- a/servlet/build.xml +++ b/servlet/build.xml @@ -24,11 +24,11 @@ - - + + + - @@ -70,11 +70,11 @@ - - + + + - diff --git a/tools/api-checker/config/gwt211_212userapi.conf b/tools/api-checker/config/gwt211_212userapi.conf new file mode 100644 index 00000000000..ecd503d03df --- /dev/null +++ b/tools/api-checker/config/gwt211_212userapi.conf @@ -0,0 +1,156 @@ +#existing API + +# dirRoot_old is missing because refJars are being supplied +name_old gwt211userApi +#sourceFiles is specified as colon-separated list of files +sourceFiles_old com/google/gwt\ +:com/google/web\ +:javax/validation\ + +#excludedFiles is specified as colon-separated ant patterns +# The entries for javax and org exclude the validation stuff. +# Bug: http://code.google.com/p/google-web-toolkit/issues/detail?id=5566 +excludedFiles_old **/linker/**\ +:**/rebind/**\ +:**/server/**\ +:**/tools/**\ +:**/vm/**\ +:com/google/gwt/core/client/impl/JavaScriptExceptionBase.java\ +:com/google/gwt/core/client/impl/WeakMapping.java\ +:com/google/gwt/core/shared/impl/StringCase.java\ +:com/google/gwt/core/shared/impl/ThrowableTypeResolver.java\ +:com/google/gwt/core/ext/**\ +:com/google/gwt/dev/*.java\ +:com/google/gwt/dev/asm/**\ +:com/google/gwt/dev/cfg/**\ +:com/google/gwt/dev/codeserver/**\ +:com/google/gwt/dev/common/**\ +:com/google/gwt/dev/generator/**\ +:com/google/gwt/dev/javac/**\ +:com/google/gwt/dev/jdt/**\ +:com/google/gwt/dev/jjs/*.java\ +:com/google/gwt/dev/jjs/ast/**\ +:com/google/gwt/dev/jjs/impl/**\ +:com/google/gwt/dev/js/**\ +:com/google/gwt/dev/json/**\ +:com/google/gwt/dev/resource/**\ +:com/google/gwt/dev/shell/**\ +:com/google/gwt/dev/ui/**\ +:com/google/gwt/dev/url/**\ +:com/google/gwt/dev/util/**\ +:com/google/gwt/i18n/**/impl/cldr/**\ +:com/google/gwt/junit/*.java\ +:com/google/gwt/junit/client/GWTTestCase.java\ +:com/google/gwt/junit/client/impl/GWTRunner.java\ +:com/google/gwt/junit/client/impl/GWTTestAccessor.java\ +:com/google/gwt/junit/remote/**\ +:com/google/gwt/regexp/shared/**\ +:com/google/gwt/resources/css/**\ +:com/google/gwt/resources/gss/**\ +:com/google/gwt/resources/converter/**\ +:com/google/gwt/resources/ext/**\ +:com/google/gwt/resources/rg/**\ +:com/google/gwt/safecss/shared/SafeStylesHostedModeUtils.java\ +:com/google/gwt/safehtml/shared/SafeHtmlHostedModeUtils.java\ +:com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java\ +:com/google/gwt/soyc/**\ +:com/google/gwt/typedarrays/super/com/google/gwt/typedarrays/shared/TypedArraysFactory.java\ +:com/google/gwt/user/client/rpc/core/**\ +:com/google/gwt/user/client/rpc/impl/**\ +:com/google/gwt/uibinder/attributeparsers/**\ +:com/google/gwt/uibinder/client/impl/**\ +:com/google/gwt/uibinder/elementparsers/**\ +:com/google/gwt/uibinder/testing/**\ +:com/google/gwt/util/**\ +:com/google/gwt/validation/**\ +:com/google/web/bindery/autobean/shared/ValueCodexHelper.java\ +:com/google/web/bindery/autobean/**/impl/**\ +:com/google/web/bindery/requestfactory/apt/**\ +:com/google/web/bindery/requestfactory/gwt/client/RequestBatcher.java\ +:com/google/web/bindery/requestfactory/gwt/client/impl/**\ +:com/google/web/bindery/requestfactory/server/impl/**\ +:com/google/web/bindery/requestfactory/shared/impl/**\ +:com/google/web/bindery/requestfactory/vm/**\ +:javax/**\ +:org/**\ + +############################################## +#new Api + +dirRoot_new ./ +name_new gwt28userApi +#sourceFiles is specified as colon-separated list of files +sourceFiles_new dev/core/super\ +:user/src\ +:user/super\ + +#excludedFiles is specified as colon-separated ant patterns +# The entries for javax and org exclude the validation stuff. +# Bug: http://code.google.com/p/google-web-toolkit/issues/detail?id=5566 +excludedFiles_new **/linker/**\ +:**/rebind/**\ +:**/server/**\ +:**/tools/**\ +:**/vm/**\ +:user/src/com/google/gwt/core/client/impl/JavaScriptExceptionBase.java\ +:user/src/com/google/gwt/core/client/impl/WeakMapping.java\ +:user/src/com/google/gwt/core/shared/impl/ThrowableTypeResolver.java\ +:user/src/com/google/gwt/i18n/**/impl/cldr/**\ +:user/src/com/google/gwt/junit/*.java\ +:user/src/com/google/gwt/junit/client/GWTTestCase.java\ +:user/src/com/google/gwt/junit/client/impl/GWTRunner.java\ +:user/src/com/google/gwt/junit/client/impl/GWTTestAccessor.java\ +:user/src/com/google/gwt/regexp/shared/**\ +:user/src/com/google/gwt/resources/css/**\ +:user/src/com/google/gwt/resources/gss/**\ +:user/src/com/google/gwt/resources/converter/**\ +:user/src/com/google/gwt/resources/ext/**\ +:user/src/com/google/gwt/resources/rg/**\ +:user/src/com/google/gwt/safecss/shared/SafeStylesHostedModeUtils.java\ +:user/src/com/google/gwt/safehtml/shared/SafeHtmlHostedModeUtils.java\ +:user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java\ +:user/src/com/google/gwt/user/client/rpc/core/**\ +:user/src/com/google/gwt/user/client/rpc/impl/**\ +:user/src/com/google/gwt/uibinder/attributeparsers/**\ +:user/src/com/google/gwt/uibinder/client/impl/**\ +:user/src/com/google/gwt/uibinder/elementparsers/**\ +:user/src/com/google/gwt/uibinder/testing/**\ +:user/src/com/google/gwt/util/**\ +:user/src/com/google/gwt/validation/**\ +:user/src/com/google/web/bindery/autobean/shared/ValueCodexHelper.java\ +:user/src/com/google/web/bindery/autobean/**/impl/**\ +:user/src/com/google/web/bindery/requestfactory/apt/**\ +:user/src/com/google/web/bindery/requestfactory/gwt/client/RequestBatcher.java\ +:user/src/com/google/web/bindery/requestfactory/gwt/client/impl/**\ +:user/src/com/google/web/bindery/requestfactory/server/impl/**\ +:user/src/com/google/web/bindery/requestfactory/shared/impl/**\ +:user/src/com/google/web/bindery/requestfactory/vm/**\ +:user/src/javax/**\ +:user/src/org/**\ +:user/super/com/google/gwt/typedarrays/super/com/google/gwt/typedarrays/shared/TypedArraysFactory.java\ + +############################################## +#excluded packages colon separated list +excludedPackages com.google.gwt.core.client.impl\ +:com.google.gwt.core.shared.impl\ +:com.google.gwt.core.client.js.impl\ +:com.google.gwt.editor.client.impl\ +:com.google.gwt.i18n.client.impl\ +:com.google.gwt.junit.client.impl\ +:com.google.gwt.lang\ +:com.google.gwt.logging.impl\ +:com.google.gwt.resources.client.impl\ +:com.google.gwt.rpc.client.impl\ +:com.google.gwt.user.client.impl\ +:com.google.gwt.user.client.ui.impl\ +:com.google.gwt.xml.client.impl\ +:javaemul.internal\ + +############################################## +#Api whitelist +# when adding to the white-list, include comments as to why the addition is +# being made. + +# java.io.Reader missing IOException throws for read methods +java.io.Reader::read([C) OVERRIDABLE_METHOD_EXCEPTION_TYPE_CHANGE new method has more exceptions: [Ljava/io/IOException;] +java.io.Reader::read([CII) OVERRIDABLE_METHOD_EXCEPTION_TYPE_CHANGE new method has more exceptions: [Ljava/io/IOException;] diff --git a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml index df4aca4b873..fdeb60705fc 100644 --- a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml +++ b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml @@ -96,6 +96,12 @@ + + + + diff --git a/user/src/com/google/gwt/core/server/StackTraceDeobfuscator.java b/user/src/com/google/gwt/core/server/StackTraceDeobfuscator.java index 776e4cfd964..f6f37ef59f4 100644 --- a/user/src/com/google/gwt/core/server/StackTraceDeobfuscator.java +++ b/user/src/com/google/gwt/core/server/StackTraceDeobfuscator.java @@ -15,9 +15,9 @@ */ package com.google.gwt.core.server; +import com.google.gwt.thirdparty.debugging.sourcemap.OriginalMapping; import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapConsumerFactory; import com.google.gwt.thirdparty.debugging.sourcemap.SourceMapping; -import com.google.gwt.thirdparty.debugging.sourcemap.proto.Mapping; import java.io.BufferedReader; import java.io.File; @@ -299,13 +299,13 @@ public final StackTraceElement resymbolize(StackTraceElement ste, String strongN if (sourceMapCapable && fragmentId != -1 && column != -1) { SourceMapping sourceMapping = loadSourceMap(strongName, fragmentId); if (sourceMapping != null && ste.getLineNumber() > -1) { - Mapping.OriginalMapping mappingForLine = sourceMapping + OriginalMapping mappingForLine = sourceMapping .getMappingForLine(jsLineNumber, column); if (mappingForLine != null) { if (declaringClass == null || declaringClass.equals(ste.getClassName())) { declaringClass = mappingForLine.getOriginalFile(); - methodName = mappingForLine.getIdentifier(); + methodName = mappingForLine.getIdentifier().orElse(null); } fileName = mappingForLine.getOriginalFile(); lineNumber = mappingForLine.getLineNumber(); diff --git a/user/super/com/google/gwt/emul/java/io/Reader.java b/user/super/com/google/gwt/emul/java/io/Reader.java index 54137991e65..63216ad3a92 100644 --- a/user/super/com/google/gwt/emul/java/io/Reader.java +++ b/user/super/com/google/gwt/emul/java/io/Reader.java @@ -59,7 +59,7 @@ public int read() throws IOException { /** * Attempts to fill {@code buf} with characters up to the size of the array. */ - public int read(char[] buf) { + public int read(char[] buf) throws IOException { return read(buf, 0, buf.length); } @@ -67,7 +67,7 @@ public int read(char[] buf) { * Attempts to fill {@code buf} with up to {@code len} characters. Characters * will be stored in {@code buf} starting at index {@code off}. */ - public abstract int read(char[] buf, int off, int len); + public abstract int read(char[] buf, int off, int len) throws IOException; /** * Returns whether the stream is ready for reading characters. diff --git a/user/super/com/google/gwt/emul/java/lang/Character.java b/user/super/com/google/gwt/emul/java/lang/Character.java index 4b01e4d43d3..d675b1719e6 100644 --- a/user/super/com/google/gwt/emul/java/lang/Character.java +++ b/user/super/com/google/gwt/emul/java/lang/Character.java @@ -322,7 +322,7 @@ private static boolean isWhitespace(String ch) { // the Java definition includes separators. whitespaceRegex = new NativeRegExp( - "[\\u1680\\u180E\\u2000-\\u2006\\u2008-\\u200A\\u2028\\u2029\\u205F\\u3000\\uFEFF]" + "[\\u1680\\u2000-\\u2006\\u2008-\\u200A\\u2028\\u2029\\u205F\\u3000]" + "|[\\t-\\r ]" + "|[\\x1C-\\x1F]"); } diff --git a/user/super/com/google/gwt/emul/java/lang/Math.java b/user/super/com/google/gwt/emul/java/lang/Math.java index 5bf6829ebab..4d9d5da6742 100644 --- a/user/super/com/google/gwt/emul/java/lang/Math.java +++ b/user/super/com/google/gwt/emul/java/lang/Math.java @@ -32,14 +32,6 @@ public final class Math { // public static int getExponent (double d) // public static int getExponent (float f) // public static double IEEEremainder(double f1, double f2) - // public static double nextAfter(double start, double direction) - // public static float nextAfter(float start, float direction) - // public static double nextUp(double start) { - // return nextAfter(start, 1.0d); - // } - // public static float nextUp(float start) { - // return nextAfter(start,1.0f); - // } public static final double E = 2.7182818284590452354; public static final double PI = 3.14159265358979323846; @@ -344,6 +336,75 @@ public static double toRadians(double x) { return x * PI_OVER_180; } + public static double nextAfter(double start, double direction) { + // Simple case described by Javadoc: + if (start == direction) { + return direction; + } + + // NaN special case, if either is NaN, return NaN. + if (Double.isNaN(start) || Double.isNaN(direction)) { + return Double.NaN; + } + + // The javadoc 'special cases' for infinities and min_value are handled already by manipulating + // the bits of the start value below. However, that approach used below doesn't work around + // zeros - we have two zero values to deal with (positive and negative) with very different bit + // representations (zero and Long.MIN_VALUE respectively). + if (start == 0) { + return direction > start ? Double.MIN_VALUE : -Double.MIN_VALUE; + } + + // Convert to int bits and increment or decrement - the fact that two positive ieee754 float + // values can be compared as ints (or two negative values, with the comparison inverted) means + // that this trick works as naturally as A + 1 > A. NaNs and zeros were already handled above. + long bits = Double.doubleToLongBits(start); + bits += (direction > start) == (bits >= 0) ? 1 : -1; + return Double.longBitsToDouble(bits); + } + + public static float nextAfter(float start, double direction) { + // Simple case described by Javadoc: + if (start == direction) { + return (float) direction; + } + + // NaN special case, if either is NaN, return NaN. + if (Float.isNaN(start) || Double.isNaN(direction)) { + return Float.NaN; + } + // The javadoc 'special cases' for INFINITYs, MIN_VALUE, and MAX_VALUE are handled already by + // manipulating the bits of the start value below. However, that approach used below doesn't + // work around zeros - we have two zero values to deal with (positive and negative) with very + // different bit representations (zero and Integer.MIN_VALUE respectively). + if (start == 0) { + return direction > start ? Float.MIN_VALUE : -Float.MIN_VALUE; + } + + // Convert to int bits and increment or decrement - the fact that two positive ieee754 float + // values can be compared as ints (or two negative values, with the comparison inverted) means + // that this trick works as naturally as A + 1 > A. NaNs and zeros were already handled above. + int bits = Float.floatToIntBits(start); + bits += (direction > start) == (bits >= 0) ? 1 : -1; + return Float.intBitsToFloat(bits); + } + + public static double nextUp(double start) { + return nextAfter(start, Double.POSITIVE_INFINITY); + } + + public static float nextUp(float start) { + return nextAfter(start, Double.POSITIVE_INFINITY); + } + + public static double nextDown(double start) { + return nextAfter(start, Double.NEGATIVE_INFINITY); + } + + public static float nextDown(float start) { + return nextAfter(start, Double.NEGATIVE_INFINITY); + } + private static boolean isSafeIntegerRange(double value) { return Integer.MIN_VALUE <= value && value <= Integer.MAX_VALUE; } diff --git a/user/super/com/google/gwt/emul/java/lang/Record.java b/user/super/com/google/gwt/emul/java/lang/Record.java new file mode 100644 index 00000000000..bc9ee724f6a --- /dev/null +++ b/user/super/com/google/gwt/emul/java/lang/Record.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package java.lang; + +/** + * Superclass of all Java records. + */ +public abstract class Record { + + protected Record() { + } + + public abstract int hashCode(); + + public abstract boolean equals(Object other); + + public abstract String toString(); +} diff --git a/user/super/com/google/gwt/emul/java/lang/String.java b/user/super/com/google/gwt/emul/java/lang/String.java index 7b4872bb8ad..693f53bdc7f 100644 --- a/user/super/com/google/gwt/emul/java/lang/String.java +++ b/user/super/com/google/gwt/emul/java/lang/String.java @@ -16,6 +16,7 @@ package java.lang; +import static javaemul.internal.InternalPreconditions.checkArgument; import static javaemul.internal.InternalPreconditions.checkCriticalStringBounds; import static javaemul.internal.InternalPreconditions.checkNotNull; import static javaemul.internal.InternalPreconditions.checkStringBounds; @@ -27,7 +28,12 @@ import java.nio.charset.UnsupportedCharsetException; import java.util.Comparator; import java.util.Locale; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.StringJoiner; +import java.util.function.Consumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import javaemul.internal.ArrayHelper; import javaemul.internal.Coercions; import javaemul.internal.EmulatedCharset; @@ -755,6 +761,91 @@ public String trim() { return start > 0 || end < length ? substring(start, end) : this; } + public String strip() { + int length = length(); + int start = getLeadingWhitespaceLength(); + if (start == length) { + return ""; + } + return substring(start, length - getTrailingWhitespaceLength()); + } + + public String stripLeading() { + return substring(getLeadingWhitespaceLength()); + } + + public String stripTrailing() { + return substring(0, length() - getTrailingWhitespaceLength()); + } + + public boolean isBlank() { + return length() == getLeadingWhitespaceLength(); + } + + public Stream lines() { + return StreamSupport.stream(new LinesSpliterator(), false); + } + + public String repeat(int count) { + checkArgument(count >= 0, "count is negative: " + count); + return asNativeString().repeat(count); + } + + private int getLeadingWhitespaceLength() { + int length = length(); + for (int i = 0; i < length; i++) { + if (!Character.isWhitespace(charAt(i))) { + return i; + } + } + return length; + } + + private int getTrailingWhitespaceLength() { + int length = length(); + for (int i = length - 1; i >= 0; i--) { + if (!Character.isWhitespace(charAt(i))) { + return length - 1 - i; + } + } + return length; + } + + private class LinesSpliterator extends Spliterators.AbstractSpliterator { + private int nextIndex = 0; + private int rPosition = -1; + private int nPosition = -1; + + private LinesSpliterator() { + super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.ORDERED); + } + + @Override + public boolean tryAdvance(Consumer action) { + if (isEmpty()) { + return false; + } + if (rPosition < nextIndex) { + rPosition = cappedIndexOf('\r'); + } + if (nPosition < nextIndex) { + nPosition = cappedIndexOf('\n'); + } + int lineEnd = Math.min(nPosition, rPosition); + action.accept(substring(nextIndex, lineEnd)); + nextIndex = lineEnd + 1; + if (nPosition == rPosition + 1) { + nextIndex++; + } + return nextIndex < length(); + } + + private int cappedIndexOf(char c) { + int index = indexOf(c, nextIndex); + return index == -1 ? length() : index; + } + } + @JsType(isNative = true, name = "String", namespace = "") private static class NativeString { public static native String fromCharCode(char x); @@ -771,6 +862,7 @@ private static class NativeString { public native String toLocaleUpperCase(); public native String toLowerCase(); public native String toUpperCase(); + public native String repeat(int count); } // CHECKSTYLE_OFF: Utility Methods for unboxed String. diff --git a/user/super/com/google/gwt/emul/java/lang/runtime/ObjectMethods.java b/user/super/com/google/gwt/emul/java/lang/runtime/ObjectMethods.java new file mode 100644 index 00000000000..5a42f844476 --- /dev/null +++ b/user/super/com/google/gwt/emul/java/lang/runtime/ObjectMethods.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package java.lang.runtime; + +/** + * This class must exist for Record to compile, but has no emulated methods. + */ +public class ObjectMethods { + +} diff --git a/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java17Test.java b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java17Test.java index f0cda6fe90b..0aea58348e7 100644 --- a/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java17Test.java +++ b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/Java17Test.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2024 GWT Project Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -21,6 +21,8 @@ import java.util.Arrays; import java.util.List; +import jsinterop.annotations.*; + /** * Tests Java 17 features. It is super sourced so that gwt can be compiles under Java 11. * @@ -114,6 +116,171 @@ private void checkIfCompiled(Shape square, Shape circle) { assertTrue(circle instanceof Circle); } + /** + * Sample nested record. Has no components. Implements a single interface + */ + record InnerRecord() implements Comparable { + /** + * Simple accessor-looking method to ensure it doesn't participate in hashcode/tostring. + */ + public String value() { + return "Hello"; + } + + @Override + public int compareTo(InnerRecord other) { + return 0; + } + } + + /** + * Record type that takes a record as a component + */ + record RecordWithReferenceType(TopLevelRecord refType){} + + public void testRecordClasses() { + /** + * Sample local record. + */ + record LocalRecord() { + @Override + public String toString() { + return "Example"; + } + } + + assertTrue(new InnerRecord() instanceof Record); + assertTrue(new InnerRecord() instanceof Comparable); + + assertFalse(new InnerRecord().toString().contains("Hello")); + assertTrue(new InnerRecord().toString().startsWith("InnerRecord")); + assertEquals(0, new InnerRecord().hashCode()); + assertEquals(new InnerRecord(), new InnerRecord()); + + assertEquals("Example", new LocalRecord().toString()); + + TopLevelRecord withValues = new TopLevelRecord("Banana", 7); + assertTrue(withValues.toString().contains("7")); + assertTrue(withValues.toString().contains("Banana")); + assertEquals("Banana", withValues.name()); + assertEquals(7, withValues.count()); + // Under the current implementation this next line would fail - this is not inconsistent with the spec, + // but it is different than what the JVM does. +// assertEquals(0, new TopLevelRecord("", 0).hashCode()); + assertFalse(0 == new TopLevelRecord("", 7).hashCode()); + assertFalse(0 == new TopLevelRecord("Pear", 0).hashCode()); + + assertFalse(new InnerRecord().equals(new LocalRecord())); + + RecordWithReferenceType sameA = new RecordWithReferenceType(new TopLevelRecord("a", 1)); + RecordWithReferenceType sameB = new RecordWithReferenceType(new TopLevelRecord("a", 1)); + RecordWithReferenceType different = new RecordWithReferenceType(new TopLevelRecord("a", 2)); + // check that an instance is equal to itself + assertEquals(sameA, sameA); + assertEquals(sameA.hashCode(), sameA.hashCode()); + //check that an instance is equal to a different record instance with same values + assertEquals(sameA, sameB); + assertEquals(sameA.hashCode(), sameB.hashCode()); + + assertFalse(sameA.equals(different)); + assertFalse(sameA.hashCode() == different.hashCode()); + } + + /** + * Simple record with one property accessor, one default method accessor + */ + @JsType(namespace = "java17") + public record JsRecord1(@JsProperty String name, int value) { } + + /** + * Simple native type to verify JsRecord1. + */ + @JsType(name = "JsRecord1", namespace = "java17", isNative = true) + public static class JsObject1 { + public String name; + public native int value(); + public JsObject1(String name, int value) { } + } + + /** + * Record with explicit method accessor + */ + @JsType(namespace = "java17") + public record JsRecord2(@JsMethod String name, int value) { } + + /** + * Simple native type to verify JsRecord2. + */ + @JsType(name = "JsRecord2", namespace = "java17", isNative = true) + public static class JsObject2 { + public JsObject2(String name, int value) { } + + public native String name(); + public native int value(); + } + + /** + * Record with exported properties and methods. + */ + public record JsRecord3(String red, JsRecord1 green, JsRecord2 blue) { + @JsProperty + public String getFlavor() { + return "grape"; + } + @JsMethod + public int countBeans() { + return 7; + } + } + + /** + * Represented as an interface since there is no constructor to call or use to type check. + */ + @JsType(isNative = true) + public interface JsObject3 { + @JsProperty + String getFlavor(); + int countBeans(); + } + + public void testJsTypeRecords() { + // Test with default accessor (method) and a property accessor + JsRecord1 r1 = new JsRecord1("foo", 7); + assertEquals("foo", r1.name()); + assertEquals(7, r1.value()); + assertEquals(new JsRecord1("foo", 7), r1); + + // Create an instance from JS, verify it is the same + JsObject1 o1 = new JsObject1("foo", 7); + assertEquals("foo", o1.name); + assertEquals(7, o1.value()); + assertEquals(o1.toString(), r1.toString()); + assertEquals(o1, r1); + + // Repeat the test with methods explicitly configured for accessors + JsRecord2 r2 = new JsRecord2("foo", 7); + assertEquals("foo", r2.name()); + assertEquals(7, r2.value()); + assertEquals(new JsRecord2("foo", 7), r2); + + // Create an instance from JS, verify it is the same + JsObject2 o2 = new JsObject2("foo", 7); + assertEquals("foo", o2.name()); + assertEquals(7, o2.value()); + assertEquals(o2.toString(), r2.toString()); + assertEquals(o2, r2); + + // Test an object with exposed properties and methods + JsRecord3 r3 = new JsRecord3("fork", r1, r2); + assertEquals("grape", r3.getFlavor()); + assertEquals(7, r3.countBeans()); + + // Cast the instance to JS, verify it is the same + JsObject3 o3 = (JsObject3) (Object) r3; + assertEquals("grape", r3.getFlavor()); + assertEquals(7, r3.countBeans()); + } + public void testInstanceOfPatternMatching() { Shape shape1 = new Circle(); if (shape1 instanceof Circle circle) { @@ -213,6 +380,14 @@ public void testInstanceOfPatternMatchingAsReturn() { assertTrue(bar.isSquare()); } + public void testNegativeInstanceOfPatternOutsideIfScope() { + Object bar = new Bar(); + if (!(bar instanceof Bar b)) { + throw new RuntimeException(); + } + assertTrue(b.isSquare()); + } + public void testSwitchExpressionOnConstant() { int value = switch(0) { default -> 17; diff --git a/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/TopLevelRecord.java b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/TopLevelRecord.java new file mode 100644 index 00000000000..51f830f4d8d --- /dev/null +++ b/user/test-super/com/google/gwt/dev/jjs/super/com/google/gwt/dev/jjs/test/TopLevelRecord.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.dev.jjs.test; + +/** + * Sample top-level record in its own compilation unit. + */ +public record TopLevelRecord(String name, int count) {} diff --git a/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java b/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java index 36d5172fd45..c94021619bc 100644 --- a/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java +++ b/user/test/com/google/gwt/core/interop/JsTypeVarargsTest.java @@ -20,10 +20,12 @@ import com.google.gwt.core.client.ScriptInjector; import com.google.gwt.junit.client.GWTTestCase; +import java.util.Objects; import javaemul.internal.annotations.DoNotInline; import jsinterop.annotations.JsFunction; import jsinterop.annotations.JsMethod; import jsinterop.annotations.JsPackage; +import jsinterop.annotations.JsProperty; import jsinterop.annotations.JsType; /** @@ -321,4 +323,111 @@ public static int varargJsMethodUninstantiatedVararg( public native void testVarargsCall_uninstantiatedVararg() /*-{ @GWTTestCase::assertEquals(II)(0, $global.varargJsMethodUninstantiatedVararg()); }-*/; + + // https://github.com/gwtproject/gwt/issues/9932 + public void testVarargsFromJavaToJsinterop() { + assertEquals(3, nonNativeMethod("A", "B", "C")); + } + + // Java declaration of globally available instance method that takes varargs + @JsType(namespace = JsPackage.GLOBAL) + public static class VarArgsQualifiedInstanceMethod { + @JsProperty(namespace = JsPackage.GLOBAL) + public static VarArgsQualifiedInstanceMethod INSTANCE = new VarArgsQualifiedInstanceMethod(); + public int getLength(Object... values) { + return values.length; + } + } + + // Declaring this type lets us use jsinterop to call the above method. + @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "VarArgsQualifiedInstanceMethod") + public static class VarArgsFromJava { + @JsProperty(namespace = JsPackage.GLOBAL) + public static VarArgsFromJava INSTANCE; + public native int getLength(Object... values); + } + + // This plain Java method accepts varargs, and tries to pass them into jsinterop. + private static int nonNativeMethod(Object... values) { + return VarArgsFromJava.INSTANCE.getLength(values); + } + + public void testVarargsObjects() { + assertEquals(new VarargsSummary<>(1, null, null), + varargInstance().acceptsObjects((Object) null)); + assertEquals(new VarargsSummary<>(0, null, null), + varargInstance().acceptsObjects()); + assertEquals(new VarargsSummary<>(1, String.class, null), + varargInstance().acceptsObjects("hello")); + assertEquals(new VarargsSummary<>(2, String.class, null), + varargInstance().acceptsObjects("hello", "world")); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(2, String.class, null), + varargInstance().acceptsObjects(new String[]{"hello", "world"})); + assertEquals(new VarargsSummary<>(2, String.class, null), + varargInstance().acceptsObjects(new Object[]{"hello", "world"})); + } + + private static VarargMethodHolderFromJava varargInstance() { + return new VarargMethodHolderFromJava(); + } + + // Java impl of the jsinterop type we'll call below. + @JsType(namespace = JsPackage.GLOBAL) + public static class VarargMethodHolder { + public VarargsSummary acceptsObjects(Object... values) { + // Note that unlike the VarargsTest version, values can never be null, and the values + // array is always Object[] since it is spliced from js's "arguments". + assertNotNull(values); + return new VarargsSummary<>( + values.length, + (values.length == 0 || values[0] == null) ? null : values[0].getClass(), + null); + } + } + + // Declaring this type lets us use jsinterop to call the above method. + @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "VarargMethodHolder") + public static class VarargMethodHolderFromJava { + public native VarargsSummary acceptsObjects(Object... values); + } + + @JsType + public static final class VarargsSummary { + private final int count; + private final Class firstItemType; + private final T value; + + public VarargsSummary(int count, Class firstItemType, T value) { + this.count = count; + this.firstItemType = firstItemType; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VarargsSummary that = (VarargsSummary) o; + return count == that.count + && Objects.equals(firstItemType, that.firstItemType) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(count, firstItemType, value); + } + + @Override + public String toString() { + return "count=" + count + + ", firstItemType=" + firstItemType + + ", value=" + value; + } + } } diff --git a/user/test/com/google/gwt/dev/jjs/test/Java17Test.java b/user/test/com/google/gwt/dev/jjs/test/Java17Test.java index 59a8c3cbc24..89784bbf03b 100644 --- a/user/test/com/google/gwt/dev/jjs/test/Java17Test.java +++ b/user/test/com/google/gwt/dev/jjs/test/Java17Test.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Google Inc. + * Copyright 2024 GWT Project Authors * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -50,10 +50,22 @@ public void testSealedClassesPermitted() { assertFalse(isGwtSourceLevel17()); } + public void testRecordClasses() { + assertFalse(isGwtSourceLevel17()); + } + + public void testJsTypeRecords() { + assertFalse(isGwtSourceLevel17()); + } + public void testInstanceOfPatternMatching() { assertFalse(isGwtSourceLevel17()); } + public void testInstanceOfPatternMatchingWithSideEffectExpression() { + assertFalse(isGwtSourceLevel17()); + } + public void testInstanceOfPatternMatchingWithAnd() { assertFalse(isGwtSourceLevel17()); } @@ -70,6 +82,14 @@ public void testMultipleInstanceOfPatternMatchingWithSameVariableName() { assertFalse(isGwtSourceLevel17()); } + public void testMultipleInstanceOfPatternMatchingWithSameVariableNameWithDifferentTypes() { + assertFalse(isGwtSourceLevel17()); + } + + public void testInstanceOfPatternMatchingIsFalse() { + assertFalse(isGwtSourceLevel17()); + } + public void testInstanceOfPatternMatchingInLambda() { assertFalse(isGwtSourceLevel17()); } @@ -78,6 +98,10 @@ public void testInstanceOfPatternMatchingAsReturn() { assertFalse(isGwtSourceLevel17()); } + public void testNegativeInstanceOfPatternOutsideIfScope() { + assertFalse(isGwtSourceLevel17()); + } + public void testSwitchExpressionOnConstant() { assertFalse(isGwtSourceLevel17()); } diff --git a/user/test/com/google/gwt/dev/jjs/test/VarargsTest.java b/user/test/com/google/gwt/dev/jjs/test/VarargsTest.java index 23e0a034380..7d7e2b066a0 100644 --- a/user/test/com/google/gwt/dev/jjs/test/VarargsTest.java +++ b/user/test/com/google/gwt/dev/jjs/test/VarargsTest.java @@ -18,6 +18,7 @@ import com.google.gwt.junit.client.GWTTestCase; import java.util.Arrays; +import java.util.Objects; /** * Tests the new JDK 1.5 varargs functionality. @@ -87,4 +88,153 @@ private Foo[] fooIdent(Foo[] args) { private int[] varargUnboxed(int... args) { return args; } + + public void testVarargsObjects() { + assertEquals(new VarargsSummary<>(1, Object[].class, null), + acceptsObjects((Object) null)); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(-1, null, null), + acceptsObjects(null)); + assertEquals(new VarargsSummary<>(-1, null, null), + acceptsObjects((Object[]) null)); + assertEquals(new VarargsSummary<>(0, Object[].class, null), + acceptsObjects()); + assertEquals(new VarargsSummary<>(1, Object[].class, null), + acceptsObjects("hello")); + assertEquals(new VarargsSummary<>(2, Object[].class, null), + acceptsObjects("hello", "world")); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(2, String[].class, null), + acceptsObjects(new String[]{"hello", "world"})); + assertEquals(new VarargsSummary<>(2, Object[].class, null), + acceptsObjects(new Object[]{"hello", "world"})); + } + + private VarargsSummary acceptsObjects(Object... values) { + return new VarargsSummary<>( + values == null ? -1 : values.length, + values == null ? null : values.getClass(), + null); + } + + public void testVarargsObjectsWithOtherParam() { + assertEquals(new VarargsSummary<>(1, Object[].class, 1), + acceptsObjectsAndOtherParam(1, (Object) null)); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(-1, null, 2), + acceptsObjectsAndOtherParam(2, null)); + assertEquals(new VarargsSummary<>(-1, null, 3), + acceptsObjectsAndOtherParam(3, (Object[]) null)); + assertEquals(new VarargsSummary<>(0, Object[].class, 4), + acceptsObjectsAndOtherParam(4)); + assertEquals(new VarargsSummary<>(1, Object[].class, 5), + acceptsObjectsAndOtherParam(5, "hello")); + assertEquals(new VarargsSummary<>(2, Object[].class, 6), + acceptsObjectsAndOtherParam(6, "hello", "world")); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(2, String[].class, 7), + acceptsObjectsAndOtherParam(7, new String[]{"hello", "world"})); + assertEquals(new VarargsSummary<>(2, Object[].class, 8), + acceptsObjectsAndOtherParam(8, new Object[]{"hello", "world"})); + } + + private VarargsSummary acceptsObjectsAndOtherParam(int number, Object... values) { + return new VarargsSummary<>( + values == null ? -1 : values.length, + values == null ? null : values.getClass(), + number); + } + + public void testObjectsWithOtherVarargsParam() { + assertEquals(new VarargsSummary<>(1, Object[].class, 1), + acceptsObjectsAndGenericParam(1, (Object) null)); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(-1, null, "2"), + acceptsObjectsAndGenericParam("2", null)); + assertEquals(new VarargsSummary<>(-1, null, null), + acceptsObjectsAndGenericParam(null, (Object[]) null)); + assertEquals(new VarargsSummary<>(0, Object[].class, 4), + acceptsObjectsAndGenericParam(4)); + assertEquals(new VarargsSummary<>(1, Object[].class, 5), + acceptsObjectsAndGenericParam(5, "hello")); + assertEquals(new VarargsSummary<>(2, Object[].class, 6), + acceptsObjectsAndGenericParam(6, "hello", "world")); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(2, String[].class, 7), + acceptsObjectsAndGenericParam(7, new String[]{"hello", "world"})); + assertEquals(new VarargsSummary<>(2, Object[].class, 8), + acceptsObjectsAndGenericParam(8, new Object[]{"hello", "world"})); + } + + private VarargsSummary acceptsObjectsAndGenericParam(T generic, Object... values) { + return new VarargsSummary<>( + values == null ? -1 : values.length, + values == null ? null : values.getClass(), + generic); + } + + public void testGenericVarargsWithOtherParam() { + assertEquals(new VarargsSummary<>(1, Object[].class, 1), + acceptsGenericVarargsAndOtherParam(1, (Object) null)); + // noinspection ConfusingArgumentToVarargsMethod + assertEquals(new VarargsSummary<>(-1, null, 2), + acceptsGenericVarargsAndOtherParam(2, null)); + assertEquals(new VarargsSummary<>(-1, null, 3), + acceptsGenericVarargsAndOtherParam(3, (Object[]) null)); + assertEquals(new VarargsSummary<>(0, Object[].class, 4), + acceptsGenericVarargsAndOtherParam(4)); + assertEquals(new VarargsSummary<>(1, String[].class, 5), + acceptsGenericVarargsAndOtherParam(5, "hello")); + assertEquals(new VarargsSummary<>(2, String[].class, 6), + acceptsGenericVarargsAndOtherParam(6, "hello", "world")); + assertEquals(new VarargsSummary<>(2, String[].class, 7), + acceptsGenericVarargsAndOtherParam(7, new String[]{"hello", "world"})); + assertEquals(new VarargsSummary<>(2, Object[].class, 8), + acceptsGenericVarargsAndOtherParam(8, new Object[]{"hello", "world"})); + } + + private VarargsSummary acceptsGenericVarargsAndOtherParam(int number, T... values) { + return new VarargsSummary<>( + values == null ? -1 : values.length, + values == null ? null : values.getClass(), + number); + } + + public static final class VarargsSummary { + private final int count; + private final Class paramType; + private final T value; + + public VarargsSummary(int count, Class paramType, T value) { + this.count = count; + this.paramType = paramType; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VarargsSummary that = (VarargsSummary) o; + return count == that.count + && Objects.equals(paramType, that.paramType) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(count, paramType, value); + } + + @Override + public String toString() { + return "count=" + count + + ", paramType=" + paramType + + ", value=" + value; + } + } } diff --git a/user/test/com/google/gwt/emultest/EmulJava11Suite.java b/user/test/com/google/gwt/emultest/EmulJava11Suite.java index 01b44205b85..9c757edfc3a 100644 --- a/user/test/com/google/gwt/emultest/EmulJava11Suite.java +++ b/user/test/com/google/gwt/emultest/EmulJava11Suite.java @@ -15,6 +15,7 @@ */ package com.google.gwt.emultest; +import com.google.gwt.emultest.java11.lang.StringTest; import com.google.gwt.emultest.java11.util.OptionalDoubleTest; import com.google.gwt.emultest.java11.util.OptionalIntTest; import com.google.gwt.emultest.java11.util.OptionalLongTest; @@ -32,6 +33,7 @@ OptionalLongTest.class, OptionalTest.class, PredicateTest.class, + StringTest.class, }) public class EmulJava11Suite { } diff --git a/user/test/com/google/gwt/emultest/java/lang/CharacterTest.java b/user/test/com/google/gwt/emultest/java/lang/CharacterTest.java index 41c6f247ec4..a2ccc9e6ade 100644 --- a/user/test/com/google/gwt/emultest/java/lang/CharacterTest.java +++ b/user/test/com/google/gwt/emultest/java/lang/CharacterTest.java @@ -360,7 +360,8 @@ public void testIsWhitepace() { char[] nonBreakingSpaceSeparators = { '\u00A0', // NO-BREAK SPACE. '\u2007', // FIGURE SPACE. - '\u202F' // NARROW NO-BREAK SPACE. + '\u202F', // NARROW NO-BREAK SPACE. + '\uFFEF' // ZERO WIDTH NO-BREAK SPACE. }; char[] specialCases = { @@ -379,7 +380,8 @@ public void testIsWhitepace() { 'a', // LATIN SMALL LETTER A. 'B', // LATIN CAPITAL LETTER B. '_', // LOW LINE. - '\u2500' // BOX DRAWINGS LIGHT HORIZONTAL. + '\u2500', // BOX DRAWINGS LIGHT HORIZONTAL. + '\u180E', // MONGOLIAN VOWEL SEPARATOR, was considered whitespace in Java 8. }; int[] supplementaryCounterExamples = { diff --git a/user/test/com/google/gwt/emultest/java/lang/MathTest.java b/user/test/com/google/gwt/emultest/java/lang/MathTest.java index a163ce67353..f195c4e147f 100644 --- a/user/test/com/google/gwt/emultest/java/lang/MathTest.java +++ b/user/test/com/google/gwt/emultest/java/lang/MathTest.java @@ -25,12 +25,13 @@ public class MathTest extends GWTTestCase { private static void assertNegativeZero(double x) { - assertTrue(isNegativeZero(x)); + assertEquals(0.0, x); + assertTrue(String.valueOf(x), isNegativeZero(x)); } private static void assertPositiveZero(double x) { assertEquals(0.0, x); - assertFalse(isNegativeZero(x)); + assertFalse(String.valueOf(x), isNegativeZero(x)); } private static void assertNaN(double x) { @@ -633,4 +634,239 @@ public void testScalb() { assertEquals(4294967296.0f, Math.scalb(1f, 32)); assertEquals(2.3283064e-10f, Math.scalb(1f, -32), 1e-7f); } + + public void testNextAfterFloat() { + // Test the five "special cases" described by the Javadoc, with both Float and Double + // "direction" values. + assertNaN(Math.nextAfter(Float.NaN, Float.NaN)); + assertNaN(Math.nextAfter(Float.NaN, Double.NaN)); + assertNaN(Math.nextAfter(Float.NaN, 0)); + assertNaN(Math.nextAfter(0, Float.NaN)); + assertNaN(Math.nextAfter(0, Double.NaN)); + + assertNegativeZero(Math.nextAfter(0.0f, -0.0f)); + assertNegativeZero(Math.nextAfter(0.0f, -0.0d)); + assertNegativeZero(Math.nextAfter(-0.0f, -0.0f)); + assertNegativeZero(Math.nextAfter(-0.0f, -0.0d)); + assertPositiveZero(Math.nextAfter(0.0f, 0.0f)); + assertPositiveZero(Math.nextAfter(0.0f, 0.0d)); + assertPositiveZero(Math.nextAfter(-0.0f, 0.0f)); + assertPositiveZero(Math.nextAfter(-0.0f, 0.0d)); + + assertNegativeZero(Math.nextAfter(-Float.MIN_VALUE, 1)); + assertPositiveZero(Math.nextAfter(Float.MIN_VALUE, -1)); + + assertEquals(Float.MAX_VALUE, Math.nextAfter(Float.POSITIVE_INFINITY, -1)); + assertEquals(Float.MAX_VALUE, + Math.nextAfter(Float.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY)); + assertEquals(Float.MAX_VALUE, + Math.nextAfter(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY)); + assertEquals(-Float.MAX_VALUE, + Math.nextAfter(Float.NEGATIVE_INFINITY, 1)); + assertEquals(-Float.MAX_VALUE, + Math.nextAfter(Float.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY)); + assertEquals(-Float.MAX_VALUE, + Math.nextAfter(Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY)); + + assertEquals(Float.POSITIVE_INFINITY, + Math.nextAfter(Float.MAX_VALUE, Float.POSITIVE_INFINITY)); + assertEquals(Float.POSITIVE_INFINITY, + Math.nextAfter(Float.MAX_VALUE, Double.POSITIVE_INFINITY)); + assertEquals(Float.NEGATIVE_INFINITY, + Math.nextAfter(-Float.MAX_VALUE, Float.NEGATIVE_INFINITY)); + assertEquals(Float.NEGATIVE_INFINITY, + Math.nextAfter(-Float.MAX_VALUE, Double.NEGATIVE_INFINITY)); + + // General rules: if values compare as equal, return "direction" (exceptions covered above) + assertEquals(Float.POSITIVE_INFINITY, + Math.nextAfter(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)); + assertEquals(Float.POSITIVE_INFINITY, + Math.nextAfter(Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + assertEquals(Float.NEGATIVE_INFINITY, + Math.nextAfter(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)); + assertEquals(Float.NEGATIVE_INFINITY, + Math.nextAfter(Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)); + assertEquals(Float.MAX_VALUE, Math.nextAfter(Float.MAX_VALUE, Float.MAX_VALUE)); + + // Return number adjacent to "start" in the relative direction of "direction". Using hex to + // easily see bit patterns in the sample data. + assertEquals(0x1.fffffcp127f, Math.nextAfter(Float.MAX_VALUE, 0)); + assertEquals(0x1.fffffcp127f, + Math.nextAfter(Float.MAX_VALUE, Float.NEGATIVE_INFINITY)); + assertEquals(-0x1.fffffcp127f, Math.nextAfter(-Float.MAX_VALUE, 0)); + assertEquals(-0x1.fffffcp127f, + Math.nextAfter(-Float.MAX_VALUE, Float.POSITIVE_INFINITY)); + assertEquals(0x1.fffffep124f, Math.nextAfter(0x1.0p125f, 0)); + assertEquals(0x1.0p125f, + Math.nextAfter(0x1.fffffep124f, Float.POSITIVE_INFINITY)); + + // Test near zero (minvalue -> zero is tested above), mantissa sign flips positive/negative + assertEquals(Float.MIN_VALUE, Math.nextAfter(0.0f, 1)); + assertEquals(Float.MIN_VALUE, Math.nextAfter(-0.0f, 1)); + assertEquals(-Float.MIN_VALUE, Math.nextAfter(0.0f, -1)); + assertEquals(-Float.MIN_VALUE, Math.nextAfter(-0.0f, -1)); + + // Test near 1, where exponent sign flips positive/negative + assertEquals(0x1.000002p0f, Math.nextAfter(1.0f, 2)); + assertEquals(0x1.fffffep-1f, Math.nextAfter(1.0f, 0)); + assertEquals(1.0f, Math.nextAfter(0x1.fffffep-1f, 2)); + assertEquals(1.0f, Math.nextAfter(0x1.000002p0f, 0)); + + // Repeat near -1 + assertEquals(-0x1.000002p0f, Math.nextAfter(-1.0f, -2)); + assertEquals(-0x1.fffffep-1f, Math.nextAfter(-1.0f, 0)); + assertEquals(-1.0f, Math.nextAfter(-0x1.fffffep-1f, -2)); + assertEquals(-1.0f, Math.nextAfter(-0x1.000002p0f, 0)); + } + + public void testNextUpFloat() { + // Special cases from javadoc + assertNaN(Math.nextUp(Float.NaN)); + assertEquals(Float.POSITIVE_INFINITY, Math.nextUp(Float.POSITIVE_INFINITY)); + assertEquals(Float.MIN_VALUE, Math.nextUp(0.0f)); + assertEquals(Float.MIN_VALUE, Math.nextUp(-0.0f)); + + assertEquals(Float.POSITIVE_INFINITY, Math.nextUp(Float.MAX_VALUE)); + assertEquals(-Float.MAX_VALUE, Math.nextUp(Float.NEGATIVE_INFINITY)); + + assertNegativeZero(Math.nextUp(-Float.MIN_VALUE)); + + assertEquals(0x1.0p2f, Math.nextUp(0x1.fffffep1f)); + assertEquals(0x1.000002p2f, Math.nextUp(0x1.0p2f)); + } + + public void testNextDownFloat() { + // Special cases from javadoc + assertNaN(Math.nextDown(Float.NaN)); + assertEquals(Float.NEGATIVE_INFINITY, Math.nextDown(Float.NEGATIVE_INFINITY)); + assertEquals(-Float.MIN_VALUE, Math.nextDown(0.0f)); + assertEquals(-Float.MIN_VALUE, Math.nextDown(-0.0f)); + + assertEquals(Float.NEGATIVE_INFINITY, Math.nextDown(-Float.MAX_VALUE)); + assertEquals(Float.MAX_VALUE, Math.nextDown(Float.POSITIVE_INFINITY)); + + assertPositiveZero(Math.nextDown(Float.MIN_VALUE)); + + assertEquals(0x1.fffffep1f, Math.nextDown(0x1.0p2f)); + assertEquals(0x1.fffffcp1f, Math.nextDown(0x1.fffffep1f)); + } + + public void testNextAfterDouble() { + // Test the five "special cases" described by the Javadoc + assertNaN(Math.nextAfter(Double.NaN, Double.NaN)); + assertNaN(Math.nextAfter(Double.NaN, 0)); + assertNaN(Math.nextAfter(0d, Double.NaN)); + + assertNegativeZero(Math.nextAfter(0.0d, -0.0d)); + assertNegativeZero(Math.nextAfter(-0.0d, -0.0d)); + assertPositiveZero(Math.nextAfter(0.0d, 0.0d)); + assertPositiveZero(Math.nextAfter(-0.0d, 0.0d)); + + assertNegativeZero(Math.nextAfter(-Double.MIN_VALUE, 1)); + assertPositiveZero(Math.nextAfter(Double.MIN_VALUE, -1)); + + assertEquals(Double.MAX_VALUE, Math.nextAfter(Double.POSITIVE_INFINITY, -1)); + assertEquals(Double.MAX_VALUE, + Math.nextAfter(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY)); + assertEquals(-Double.MAX_VALUE, Math.nextAfter(Double.NEGATIVE_INFINITY, 1)); + assertEquals(-Double.MAX_VALUE, + Math.nextAfter(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY)); + + assertEquals(Double.POSITIVE_INFINITY, + Math.nextAfter(Double.MAX_VALUE, Double.POSITIVE_INFINITY)); + assertEquals(Double.NEGATIVE_INFINITY, + Math.nextAfter(-Double.MAX_VALUE, Double.NEGATIVE_INFINITY)); + + // General rules: if values compare as equal, return "direction" (exceptions covered above) + assertEquals(Double.POSITIVE_INFINITY, + Math.nextAfter(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY)); + assertEquals(Double.NEGATIVE_INFINITY, + Math.nextAfter(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY)); + assertEquals(Double.MAX_VALUE, Math.nextAfter(Double.MAX_VALUE, Double.MAX_VALUE)); + + // Return number adjacent to "start" in the relative direction of "direction". Using hex to + // easily see bit patterns in the sample data. + assertEquals(0x1.ffffffffffffep1023, Math.nextAfter(Double.MAX_VALUE, 0)); + assertEquals(0x1.ffffffffffffep1023, + Math.nextAfter(Double.MAX_VALUE, Double.NEGATIVE_INFINITY)); + assertEquals(-0x1.ffffffffffffep1023, Math.nextAfter(-Double.MAX_VALUE, 0)); + assertEquals(-0x1.ffffffffffffep1023, + Math.nextAfter(-Double.MAX_VALUE, Double.POSITIVE_INFINITY)); + assertEquals(0x1.fffffffffffffp124, Math.nextAfter(0x1.0p125, 0)); + assertEquals(0x1.0p125, Math.nextAfter(0x1.fffffffffffffp124, Double.POSITIVE_INFINITY)); + + // Test near zero (minvalue -> zero is tested above), mantissa sign flips positive/negative + assertEquals(Double.MIN_VALUE, Math.nextAfter(0.0d, 1)); + assertEquals(Double.MIN_VALUE, Math.nextAfter(-0.0d, 1)); + assertEquals(-Double.MIN_VALUE, Math.nextAfter(0.0d, -1)); + assertEquals(-Double.MIN_VALUE, Math.nextAfter(-0.0d, -1)); + + // Test near 1, where exponent sign flips positive/negative + assertEquals(0x1.0000000000001p0d, Math.nextAfter(1.0d, 2)); + assertEquals(0x1.fffffffffffffp-1d, Math.nextAfter(1.0d, 0)); + assertEquals(1.0d, Math.nextAfter(0x1.fffffffffffffp-1d, 2)); + assertEquals(1.0d, Math.nextAfter(0x1.0000000000001p0d, 0)); + + // Repeat near -1 + assertEquals(-0x1.0000000000001p0d, Math.nextAfter(-1.0d, -2)); + assertEquals(-0x1.fffffffffffffp-1d, Math.nextAfter(-1.0d, 0)); + assertEquals(-1.0d, Math.nextAfter(-0x1.fffffffffffffp-1d, -2)); + assertEquals(-1.0d, Math.nextAfter(-0x1.0000000000001p0d, 0)); + } + + public void testNextUpDouble() { + // Special cases from javadoc + assertNaN(Math.nextUp(Double.NaN)); + assertEquals(Double.POSITIVE_INFINITY, Math.nextUp(Double.POSITIVE_INFINITY)); + assertEquals(Double.MIN_VALUE, Math.nextUp(0.0)); + assertEquals(Double.MIN_VALUE, Math.nextUp(-0.0)); + + assertEquals(Double.POSITIVE_INFINITY, Math.nextUp(Double.MAX_VALUE)); + assertEquals(-Double.MAX_VALUE, Math.nextUp(Double.NEGATIVE_INFINITY)); + + assertNegativeZero(Math.nextUp(-Double.MIN_VALUE)); + + assertEquals(0x1.0p2d, Math.nextUp(0x1.fffffffffffffp1d)); + assertEquals(0x1.0000000000001p2d, Math.nextUp(0x1.0p2d)); + + // Test near zero (minvalue -> zero is tested above), mantissa sign flips positive/negative + assertEquals(Double.MIN_VALUE, Math.nextUp(0.0d)); + assertEquals(Double.MIN_VALUE, Math.nextUp(-0.0d)); + + // Test near 1, where exponent sign flips positive/negative + assertEquals(0x1.0000000000001p0d, Math.nextUp(1.0d)); + assertEquals(1.0d, Math.nextUp(0x1.fffffffffffffp-1d)); + + // Repeat near -1 + assertEquals(-0x1.fffffffffffffp-1d, Math.nextUp(-1.0d)); + assertEquals(-1.0d, Math.nextUp(-0x1.0000000000001p0d)); + } + + public void testNextDownDouble() { + // Special cases from javadoc + assertNaN(Math.nextDown(Double.NaN)); + assertEquals(Double.NEGATIVE_INFINITY, Math.nextDown(Double.NEGATIVE_INFINITY)); + assertEquals(-Double.MIN_VALUE, Math.nextDown(0.0d)); + assertEquals(-Double.MIN_VALUE, Math.nextDown(-0.0d)); + + assertEquals(Double.NEGATIVE_INFINITY, Math.nextDown(-Double.MAX_VALUE)); + assertEquals(Double.MAX_VALUE, Math.nextDown(Double.POSITIVE_INFINITY)); + + assertPositiveZero(Math.nextDown(Double.MIN_VALUE)); + + assertEquals(0x1.fffffffffffffp1d, Math.nextDown(0x1.0p2d)); + assertEquals(0x1.ffffffffffffep1d, Math.nextDown(0x1.fffffffffffffp1d)); + + // Test near zero (minvalue -> zero is tested above), mantissa sign flips positive/negative + assertEquals(-Double.MIN_VALUE, Math.nextDown(0.0d)); + assertEquals(-Double.MIN_VALUE, Math.nextDown(-0.0d)); + + // Test near 1, where exponent sign flips positive/negative + assertEquals(0x1.fffffffffffffp-1d, Math.nextDown(1.0d)); + assertEquals(1.0d, Math.nextDown(0x1.0000000000001p0d)); + + // Repeat near -1 + assertEquals(-0x1.0000000000001p0d, Math.nextDown(-1.0d)); + assertEquals(-1.0d, Math.nextDown(-0x1.fffffffffffffp-1d)); + } } diff --git a/user/test/com/google/gwt/emultest/java/lang/StringTest.java b/user/test/com/google/gwt/emultest/java/lang/StringTest.java index 4b08a45eeea..ea12fee74c1 100644 --- a/user/test/com/google/gwt/emultest/java/lang/StringTest.java +++ b/user/test/com/google/gwt/emultest/java/lang/StringTest.java @@ -24,13 +24,6 @@ import java.util.Arrays; import java.util.Locale; -/** - * TODO: COMPILER OPTIMIZATIONS HAVE MADE THIS TEST NOT ACTUALLY TEST ANYTHING! - * NEED A VERSION THAT DOESN'T USE STATICALLY DETERMINABLE STRINGS! - * - * See individual method TODOs for ones that still need work -- the ones without - * comments are already protected against optimization. - */ public class StringTest extends GWTTestCase { @Override @@ -862,9 +855,6 @@ public void testToString() { assertSame("s same as s.toString()", s, s.toString()); } - /* - * TODO: needs rewriting to avoid compiler optimizations. - */ public void testTrim() { trimRightAssertEquals("abc", " \t abc \n "); trimRightAssertEquals("abc", "abc"); @@ -895,6 +885,7 @@ public void testTrim() { // JavaScript would trim \u2029 and other unicode whitespace type characters; but Java wont trimRightAssertEquals("\u2029abc\u00a0","\u2029abc\u00a0"); + trimRightAssertEquals("\uffefx\u180e", "\uffefx\u180e "); } public void testUpperCase() { @@ -911,40 +902,33 @@ public void testUpperCase() { assertEquals("", hideFromCompiler("").toUpperCase(Locale.getDefault())); } - /* - * TODO: needs rewriting to avoid compiler optimizations. - */ public void testValueOf() { - assertTrue(String.valueOf(C.FLOAT_VALUE).startsWith(C.FLOAT_STRING)); - assertEquals(C.INT_STRING, String.valueOf(C.INT_VALUE)); - assertEquals(C.LONG_STRING, String.valueOf(C.LONG_VALUE)); - assertTrue(String.valueOf(C.DOUBLE_VALUE).startsWith(C.DOUBLE_STRING)); - assertEquals(C.CHAR_STRING, String.valueOf(C.CHAR_VALUE)); - assertEquals(C.CHAR_ARRAY_STRING, String.valueOf(C.CHAR_ARRAY_VALUE)); + assertTrue(String.valueOf(hideFromCompiler(C.FLOAT_VALUE)).startsWith(C.FLOAT_STRING)); + assertEquals(C.INT_STRING, String.valueOf(hideFromCompiler(C.INT_VALUE))); + assertEquals(C.LONG_STRING, String.valueOf(hideFromCompiler(C.LONG_VALUE))); + assertTrue(String.valueOf(C.DOUBLE_VALUE).startsWith(hideFromCompiler(C.DOUBLE_STRING))); + assertEquals(C.CHAR_STRING, String.valueOf(hideFromCompiler(C.CHAR_VALUE))); + assertEquals(C.CHAR_ARRAY_STRING, String.valueOf(hideFromCompiler(C.CHAR_ARRAY_VALUE))); assertEquals( - C.CHAR_ARRAY_STRING_SUB, String.valueOf(C.CHAR_ARRAY_VALUE, 1, + C.CHAR_ARRAY_STRING_SUB, String.valueOf(hideFromCompiler(C.CHAR_ARRAY_VALUE), 1, 4)); - assertEquals(C.FALSE_STRING, String.valueOf(C.FALSE_VALUE)); - assertEquals(C.TRUE_STRING, String.valueOf(C.TRUE_VALUE)); + assertEquals(C.FALSE_STRING, String.valueOf(hideFromCompiler(C.FALSE_VALUE))); + assertEquals(C.TRUE_STRING, String.valueOf(hideFromCompiler(C.TRUE_VALUE))); assertEquals(C.getLargeCharArrayString(), String.valueOf(C.getLargeCharArrayValue())); } /** * Helper method for testTrim to avoid compiler optimizations. - * - * TODO: insufficient, compiler now inlines. */ public void trimRightAssertEquals(String left, String right) { - assertEquals(left, right.trim()); + assertEquals(left, hideFromCompiler(right).trim()); } /** * Helper method for testTrim to avoid compiler optimizations. - * - * TODO: insufficient, compiler now inlines. */ public void trimRightAssertSame(String left, String right) { - assertSame(left, right.trim()); + assertSame(left, hideFromCompiler(right).trim()); } private void compareList(String category, String[] desired, String[] got) { diff --git a/user/test/com/google/gwt/emultest/java/math/BigDecimalConvertTest.java b/user/test/com/google/gwt/emultest/java/math/BigDecimalConvertTest.java index 5f5dd965102..9592da17bca 100644 --- a/user/test/com/google/gwt/emultest/java/math/BigDecimalConvertTest.java +++ b/user/test/com/google/gwt/emultest/java/math/BigDecimalConvertTest.java @@ -62,14 +62,13 @@ public void testByteValue() { /** * Double value of a small negative BigDecimal. */ - // TODO(jat): add back after Double.doubleToLongBits is implemented -// public void testDoubleValueMinusZero() { -// String a = "-123809648392384754573567356745735.63567890295784902768787678287E-400"; -// BigDecimal aNumber = new BigDecimal(a); -// long minusZero = -9223372036854775808L; -// double result = aNumber.doubleValue(); -// assertTrue("incorrect value", Double.doubleToLongBits(result) == minusZero); -// } + public void testDoubleValueMinusZero() { + String a = "-123809648392384754573567356745735.63567890295784902768787678287E-400"; + BigDecimal aNumber = new BigDecimal(a); + long minusZero = -9223372036854775808L; + double result = aNumber.doubleValue(); + assertTrue("incorrect value", Double.doubleToLongBits(result) == minusZero); + } /** * Double value of a negative BigDecimal. @@ -94,14 +93,13 @@ public void testDoubleValueNegInfinity() { /** * Double value of a small positive BigDecimal. */ - // TODO(jat): add back after Double.doubleToLongBits is implemented -// public void testDoubleValuePlusZero() { -// String a = "123809648392384754573567356745735.63567890295784902768787678287E-400"; -// BigDecimal aNumber = new BigDecimal(a); -// long zero = 0; -// double result = aNumber.doubleValue(); -// assertTrue("incorrect value", Double.doubleToLongBits(result) == zero); -// } + public void testDoubleValuePlusZero() { + String a = "123809648392384754573567356745735.63567890295784902768787678287E-400"; + BigDecimal aNumber = new BigDecimal(a); + long zero = 0; + double result = aNumber.doubleValue(); + assertTrue("incorrect value", Double.doubleToLongBits(result) == zero); + } /** * Double value of a positive BigDecimal. @@ -126,14 +124,13 @@ public void testDoubleValuePosInfinity() { /** * Float value of a small negative BigDecimal. */ - // TODO(jat): add back after Float.floatToIntBits is implemented -// public void testFloatValueMinusZero() { -// String a = "-123809648392384754573567356745735.63567890295784902768787678287E-400"; -// BigDecimal aNumber = new BigDecimal(a); -// int minusZero = -2147483648; -// float result = aNumber.floatValue(); -// assertTrue("incorrect value", Float.floatToIntBits(result) == minusZero); -// } + public void testFloatValueMinusZero() { + String a = "-123809648392384754573567356745735.63567890295784902768787678287E-400"; + BigDecimal aNumber = new BigDecimal(a); + int minusZero = -2147483648; + float result = aNumber.floatValue(); + assertTrue("incorrect value", Float.floatToIntBits(result) == minusZero); + } /** * Float value of a negative BigDecimal. @@ -159,14 +156,13 @@ public void testFloatValueNegInfinity() { /** * Float value of a small positive BigDecimal. */ - // TODO(jat): add back after Float.floatToIntBits is implemented -// public void testFloatValuePlusZero() { -// String a = "123809648392384754573567356745735.63567890295784902768787678287E-400"; -// BigDecimal aNumber = new BigDecimal(a); -// int zero = 0; -// float result = aNumber.floatValue(); -// assertTrue("incorrect value", Float.floatToIntBits(result) == zero); -// } + public void testFloatValuePlusZero() { + String a = "123809648392384754573567356745735.63567890295784902768787678287E-400"; + BigDecimal aNumber = new BigDecimal(a); + int zero = 0; + float result = aNumber.floatValue(); + assertTrue("incorrect value", Float.floatToIntBits(result) == zero); + } /** * Float value of a positive BigDecimal. diff --git a/user/test/com/google/gwt/emultest/java11/lang/StringTest.java b/user/test/com/google/gwt/emultest/java11/lang/StringTest.java new file mode 100644 index 00000000000..92650aca510 --- /dev/null +++ b/user/test/com/google/gwt/emultest/java11/lang/StringTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2024 GWT Project Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.emultest.java11.lang; + +import com.google.gwt.emultest.java.util.EmulTestBase; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * Tests for java.lang.String Java 11 API emulation. + */ +public class StringTest extends EmulTestBase { + public void testIsBlank() { + assertTrue(hideFromCompiler("").isBlank()); + assertTrue(hideFromCompiler(" ").isBlank()); + assertFalse(hideFromCompiler("x ").isBlank()); + assertTrue(hideFromCompiler("\u001c").isBlank()); + assertFalse(hideFromCompiler("\u00a0").isBlank()); + } + + public void testStrip() { + stripRightAsssertEquals("", ""); + stripRightAsssertEquals("", " "); + stripRightAsssertEquals("x", " x "); + stripRightAsssertEquals("x", "\u001cx\u001c"); + stripRightAsssertEquals("\u00a0x\u00a0", "\u00a0x\u00a0 "); + stripRightAsssertEquals("\uffefx\u180e", "\uffefx\u180e "); + } + + public void testStripLeading() { + stripRightLeadingAsssertEquals("", ""); + stripRightLeadingAsssertEquals("", " "); + stripRightLeadingAsssertEquals("x ", " x "); + stripRightLeadingAsssertEquals("x\u001c", "\u001cx\u001c"); + stripRightLeadingAsssertEquals("\u00a0x\u00a0", "\u00a0x\u00a0"); + } + + public void testStripTrailing() { + stripRightTrailingAsssertEquals("", ""); + stripRightTrailingAsssertEquals("", " "); + stripRightTrailingAsssertEquals(" x", " x "); + stripRightTrailingAsssertEquals("\u001cx", "\u001cx\u001c"); + stripRightTrailingAsssertEquals("\u00a0x\u00a0", "\u00a0x\u00a0 "); + } + + private void stripRightAsssertEquals(String expected, String arg) { + assertEquals(expected, hideFromCompiler(arg).strip()); + } + + private void stripRightLeadingAsssertEquals(String expected, String arg) { + assertEquals(expected, hideFromCompiler(arg).stripLeading()); + } + + private void stripRightTrailingAsssertEquals(String expected, String arg) { + assertEquals(expected, hideFromCompiler(arg).stripTrailing()); + } + + public void testRepeat() { + assertEquals("", hideFromCompiler("foo").repeat(0)); + assertEquals("foo", hideFromCompiler("foo").repeat(1)); + assertEquals("foofoofoo", hideFromCompiler("foo").repeat(3)); + try { + String noFoo = hideFromCompiler("foo").repeat(-1); + throw new Error("Should fail with negative arg"); + } catch (IllegalArgumentException ex) { + assertEquals("count is negative: -1", ex.getMessage()); + } + } + + public void testLines() { + assertEquals(Arrays.asList("a", "b", "c", "d"), + "a\rb\nc\r\nd".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList("a"), + "a\n".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList("a"), + "a\r\n".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList(), + "".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList(""), + "\n".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList(""), + "\r\n".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList("", ""), + "\n\r\n".lines().collect(Collectors.toList())); + assertEquals(Arrays.asList("", "", "c"), + "\n\r\nc".lines().collect(Collectors.toList())); + } + + private T hideFromCompiler(T value) { + if (Math.random() < -1) { + // Can never happen, but fools the compiler enough not to optimize this call. + fail(); + } + return value; + } +}