diff --git a/packages/http-client-java/emitter/src/code-model-builder.ts b/packages/http-client-java/emitter/src/code-model-builder.ts index 5bc7e1a8ddc..e2568c10b49 100644 --- a/packages/http-client-java/emitter/src/code-model-builder.ts +++ b/packages/http-client-java/emitter/src/code-model-builder.ts @@ -2472,8 +2472,14 @@ export class CodeModelBuilder { private processObjectSchema(type: SdkModelType, name: string): ObjectSchema { const rawModelType = type.__raw; + if (!name && !type.name) { + reportDiagnostic(this.program, { + code: "empty-name", + target: rawModelType ?? NoTarget, + }); + } const namespace = getNamespace(rawModelType); - const objectSchema = new ObjectSchema(name, type.doc ?? "", { + const objectSchema = new ObjectSchema(type.name ?? name, type.doc ?? "", { summary: type.summary, language: { default: { diff --git a/packages/http-client-java/emitter/src/emitter.ts b/packages/http-client-java/emitter/src/emitter.ts index 0609ebda8f1..145860929b6 100644 --- a/packages/http-client-java/emitter/src/emitter.ts +++ b/packages/http-client-java/emitter/src/emitter.ts @@ -1,4 +1,10 @@ -import { EmitContext, getNormalizedAbsolutePath, NoTarget, resolvePath } from "@typespec/compiler"; +import { + EmitContext, + getNormalizedAbsolutePath, + NoTarget, + Program, + resolvePath, +} from "@typespec/compiler"; import { promises } from "fs"; import { dump } from "js-yaml"; import { dirname } from "path"; @@ -106,7 +112,8 @@ export async function $onEmit(context: EmitContext) { javaArgs.push(codeModelFileName); try { const result = await spawnAsync("java", javaArgs, { stdio: "pipe" }); - trace(program, `Code generation log: ${result.stdout}`); + reportJarOutput(program, result.stdout); + // trace(program, `Code generation log: ${result.stdout}`); } catch (error: any) { if (error && "code" in error && error["code"] === "ENOENT") { reportDiagnostic(program, { @@ -114,18 +121,19 @@ export async function $onEmit(context: EmitContext) { target: NoTarget, }); } else { + if (error instanceof SpawnError) { + reportJarOutput(program, error.stdout); + // trace(program, `Code generation log: ${error.stdout}`); + } + // error in Java codegen, report as unknown error reportDiagnostic(program, { code: "unknown-error", format: { - errorMessage: `The emitter was unable to generate client code from this TypeSpec, please open an issue on https://github.com/microsoft/typespec, include TypeSpec source and all the diagnostic information in your submission.\nOutput: ${error.stdout}\nError: ${error.stderr}`, + errorMessage: `The emitter was unable to generate client code from this TypeSpec, please open an issue on https://github.com/microsoft/typespec, include TypeSpec source and all the diagnostic information in your submission.`, }, target: NoTarget, }); - if (error instanceof SpawnError) { - trace(program, `Code generation log: ${error.stdout}`); - trace(program, `Code generation error: ${error.stderr}`); - } } } @@ -135,3 +143,54 @@ export async function $onEmit(context: EmitContext) { } } } + +function reportJarOutput(program: Program, jarOutput: string) { + const lines = jarOutput.split("\n"); + const logs: Array = []; + + // parse stdout to array of logs + let currentLog = undefined; + for (const line of lines) { + if ( + line.startsWith("TRACE ") || + line.startsWith("DEBUG ") || + line.startsWith("INFO ") || + line.startsWith("WARN ") || + line.startsWith("ERROR ") + ) { + if (currentLog) { + logs.push(currentLog); + } + currentLog = line; + } else if (currentLog) { + currentLog = currentLog + "\n" + line; + } + } + if (currentLog) { + logs.push(currentLog); + } + + // trace or report the logs, according to log level + for (const log of logs) { + if (log.startsWith("ERROR ")) { + reportDiagnostic(program, { + code: "generator-error", + format: { + errorMessage: log.substring(6), + }, + target: NoTarget, + }); + } else if (log.startsWith("WARN ")) { + reportDiagnostic(program, { + code: "generator-warning", + format: { + warningMessage: log.substring(5), + }, + target: NoTarget, + }); + } else { + const index = log.indexOf(" "); + trace(program, log.substring(index + 1)); + } + } +} diff --git a/packages/http-client-java/emitter/src/lib.ts b/packages/http-client-java/emitter/src/lib.ts index f4885e66dbc..ae9bac3bd27 100644 --- a/packages/http-client-java/emitter/src/lib.ts +++ b/packages/http-client-java/emitter/src/lib.ts @@ -100,6 +100,12 @@ export const $lib = createTypeSpecLibrary({ default: paramMessage`An unknown error occurred. ${"errorMessage"}`, }, }, + "generator-error": { + severity: "error", + messages: { + default: paramMessage`${"errorMessage"}`, + }, + }, "invalid-java-sdk-dependency": { severity: "error", messages: { @@ -130,8 +136,20 @@ export const $lib = createTypeSpecLibrary({ multipartFormData: paramMessage`Unrecognized type for multipart form data, kind '${"typeKind"}'.`, }, }, + "empty-name": { + severity: "error", + messages: { + default: "Name from TCGC is empty.", + }, + }, // warning + "generator-warning": { + severity: "warning", + messages: { + default: paramMessage`${"warningMessage"}`, + }, + }, "no-service": { severity: "warning", messages: { diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java index d44ce8ec440..4aad12e272a 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/Postprocessor.java @@ -119,7 +119,7 @@ public static void writeToFiles(Map javaFiles, NewPlugin plugin, } try { - CodeFormatterUtil.formatCode(javaFiles, plugin); + CodeFormatterUtil.formatCode(javaFiles, plugin, logger); } catch (Exception ex) { throw new RuntimeException(ex); } diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java index 2d230031e31..3e5ed317fdd 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/postprocessor/implementation/CodeFormatterUtil.java @@ -18,21 +18,62 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.slf4j.Logger; /** * Utility class that handles code formatting. */ public final class CodeFormatterUtil { + + private static final Pattern SPOTLESS_ERROR_PATTERN + = Pattern.compile("^(\\d+):\\d+: error: (.*)$", Pattern.MULTILINE); + private static final int SPOTLESS_FILE_CONTENT_RANGE = 3; + /** * Formats the given files by removing unused imports and applying Eclipse code formatting. * * @param files The files to format. * @param plugin The plugin to use to write the formatted files. */ - public static void formatCode(Map files, NewPlugin plugin) { - for (Map.Entry file : formatCodeInternal(files.entrySet())) { - plugin.writeFile(file.getKey(), file.getValue(), null); + public static void formatCode(Map files, NewPlugin plugin, Logger logger) { + try { + for (Map.Entry file : formatCodeInternal(files.entrySet())) { + plugin.writeFile(file.getKey(), file.getValue(), null); + } + } catch (SpotlessException ex) { + // format one file at a time, to give better error diagnostics + for (Map.Entry file : files.entrySet()) { + try { + formatCodeInternal(List.of(file)); + } catch (RuntimeException e) { + // by default, log the whole file + String content = file.getValue(); + + // if we can find the line number from the error message, refine the "content" to the part of file + // around the line + Matcher matcher = SPOTLESS_ERROR_PATTERN.matcher(e.getMessage()); + if (matcher.find()) { + int lineNumber = Integer.parseInt(matcher.group(1)); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(matcher.group(0)).append("\n"); + + // line number from log starts from 1 + String[] lines = content.split("\n"); + int lineIndexBegin = Math.max(0, lineNumber - 1 - SPOTLESS_FILE_CONTENT_RANGE); + int lineIndexEnd = Math.min(lines.length - 1, lineNumber - 1 + SPOTLESS_FILE_CONTENT_RANGE); + for (int lineIndex = lineIndexBegin; lineIndex <= lineIndexEnd; ++lineIndex) { + stringBuilder.append(lineIndex + 1).append(" ").append(lines[lineIndex]).append("\n"); + } + content = stringBuilder.toString(); + } + logger.error("Failed to format file '{}'\n{}", file.getKey(), content); + } + } + throw ex; } } @@ -41,10 +82,10 @@ public static void formatCode(Map files, NewPlugin plugin) { * * @param files The files to format. The entry is filename and content. * @return the files after format. - * @throws Exception If code formatting fails. + * @throws RuntimeException If code formatting fails. */ - public static List formatCode(Collection> files) throws Exception { - return formatCodeInternal(files).stream().map(Map.Entry::getValue).collect(Collectors.toList()); + public static List formatCode(Map files) { + return formatCodeInternal(files.entrySet()).stream().map(Map.Entry::getValue).collect(Collectors.toList()); } private static List> formatCodeInternal(Collection> files) { @@ -101,12 +142,18 @@ private static void attemptMavenSpotless(Path pomPath) { if (process.isAlive() || process.exitValue() != 0) { process.destroyForcibly(); - throw new RuntimeException( - "Spotless failed to complete within 300 seconds or failed with an error code. " + throw new SpotlessException( + "Spotless failed to complete within 300 seconds or failed with an error code. Output:\n" + Files.readString(outputFile.toPath())); } } catch (IOException | InterruptedException ex) { throw new RuntimeException("Failed to run Spotless on generated code.", ex); } } + + private static final class SpotlessException extends RuntimeException { + public SpotlessException(String message) { + super(message); + } + } } diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/resources/simplelogger.properties b/packages/http-client-java/generator/http-client-generator-core/src/main/resources/simplelogger.properties deleted file mode 100644 index 1a392fcac60..00000000000 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/resources/simplelogger.properties +++ /dev/null @@ -1,35 +0,0 @@ -# SLF4J's SimpleLogger configuration file -# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. -org.slf4j.simpleLogger.logFile=System.out - -# Default logging detail level for all instances of SimpleLogger. -# Must be one of ("trace", "debug", "info", "warn", or "error"). -# If not specified, defaults to "info". -org.slf4j.simpleLogger.defaultLogLevel=error - -# Logging detail level for a SimpleLogger instance named "xxxxx". -# Must be one of ("trace", "debug", "info", "warn", or "error"). -# If not specified, the default logging detail level is used. -#org.slf4j.simpleLogger.log.xxxxx= - -# Set to true if you want the current date and time to be included in output messages. -# Default is false, and will output the number of milliseconds elapsed since startup. -#org.slf4j.simpleLogger.showDateTime=false - -# The date and time format to be used in the output messages. -# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. -# If the format is not specified or is invalid, the default format is used. -# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. -#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z - -# Set to true if you want to output the current thread name. -# Defaults to true. -#org.slf4j.simpleLogger.showThreadName=true - -# Set to true if you want the Logger instance name to be included in output messages. -# Defaults to true. -#org.slf4j.simpleLogger.showLogName=true - -# Set to true if you want the last component of the name to be included in output messages. -# Defaults to false. -#org.slf4j.simpleLogger.showShortLogName=false diff --git a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java index 007c242e448..4f2cb5ae39e 100644 --- a/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-mgmt/src/main/java/com/microsoft/typespec/http/client/generator/mgmt/template/SampleTemplate.java @@ -24,9 +24,8 @@ public String write(List examples, List sampleJavaFiles assert examples.size() == sampleJavaFiles.size(); // clean up copyright etc. - List> javaFiles = sampleJavaFiles.stream() - .map(e -> Map.entry(e.getFilePath(), cleanJavaFile(e))) - .collect(Collectors.toList()); + Map javaFiles + = sampleJavaFiles.stream().collect(Collectors.toMap(JavaFile::getFilePath, SampleTemplate::cleanJavaFile)); // format code List javaFileContents; try { diff --git a/packages/http-client-java/generator/http-client-generator-test/tsp/response.tsp b/packages/http-client-java/generator/http-client-generator-test/tsp/response.tsp index 60fd1335807..4bc7fc0662f 100644 --- a/packages/http-client-java/generator/http-client-generator-test/tsp/response.tsp +++ b/packages/http-client-java/generator/http-client-generator-test/tsp/response.tsp @@ -22,8 +22,8 @@ enum ApiVersions { op RpcOperationWithAdditionalResponse< TParams, TResponse extends TypeSpec.Reflection.Model, - TAdditionalResponse extends object, - Traits extends object = {}, + TAdditionalResponse extends {}, + Traits extends {} = {}, TErrorResponse = Azure.Core.Foundations.ErrorResponse > is Foundations.Operation< TParams & Azure.Core.Traits.Private.TraitProperties, diff --git a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java index 5821ec445ed..8b3d71842e0 100644 --- a/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java +++ b/packages/http-client-java/generator/http-client-generator/src/main/java/com/microsoft/typespec/http/client/generator/Main.java @@ -50,45 +50,52 @@ public class Main { // java -jar target/azure-typespec-extension-jar-with-dependencies.jar public static void main(String[] args) throws IOException { - // parameters - String inputYamlFileName = DEFAULT_OUTPUT_DIR + "code-model.yaml"; - if (args.length >= 1) { - inputYamlFileName = args[0]; - } + try { + // parameters + String inputYamlFileName = DEFAULT_OUTPUT_DIR + "code-model.yaml"; + if (args.length >= 1) { + inputYamlFileName = args[0]; + } + + LOGGER.info("Code model file: {}", inputYamlFileName); - LOGGER.info("Code model file: {}", inputYamlFileName); + // load code-model.yaml + CodeModel codeModel = loadCodeModel(inputYamlFileName); - // load code-model.yaml - CodeModel codeModel = loadCodeModel(inputYamlFileName); + EmitterOptions emitterOptions = loadEmitterOptions(codeModel); - EmitterOptions emitterOptions = loadEmitterOptions(codeModel); + boolean sdkIntegration = true; + String outputDir = emitterOptions.getOutputDir(); + Path outputDirPath = Paths.get(outputDir); + if (Files.exists(outputDirPath)) { + if (emitterOptions.getArm()) { + // check ../../parents/azure-client-sdk-parent + sdkIntegration = Files.exists(Paths.get(outputDir, "../../parents/azure-client-sdk-parent")); + } else { + try (Stream filestream = Files.list(outputDirPath)) { + Set filenames = filestream.map(p -> p.getFileName().toString()) + .map(name -> name.toLowerCase(Locale.ROOT)) + .collect(Collectors.toSet()); + + // if there is already pom and source, do not overwrite them (includes README.md, CHANGELOG.md + // etc.) + sdkIntegration = !filenames.containsAll(Arrays.asList("pom.xml", "src")); + } + } + } - boolean sdkIntegration = true; - String outputDir = emitterOptions.getOutputDir(); - Path outputDirPath = Paths.get(outputDir); - if (Files.exists(outputDirPath)) { if (emitterOptions.getArm()) { - // check ../../parents/azure-client-sdk-parent - sdkIntegration = Files.exists(Paths.get(outputDir, "../../parents/azure-client-sdk-parent")); + handleFluent(codeModel, emitterOptions, sdkIntegration); } else { - try (Stream filestream = Files.list(outputDirPath)) { - Set filenames = filestream.map(p -> p.getFileName().toString()) - .map(name -> name.toLowerCase(Locale.ROOT)) - .collect(Collectors.toSet()); - - // if there is already pom and source, do not overwrite them (includes README.md, CHANGELOG.md etc.) - sdkIntegration = !filenames.containsAll(Arrays.asList("pom.xml", "src")); - } + handleDPG(codeModel, emitterOptions, sdkIntegration, outputDir); } - } - if (emitterOptions.getArm()) { - handleFluent(codeModel, emitterOptions, sdkIntegration); - } else { - handleDPG(codeModel, emitterOptions, sdkIntegration, outputDir); + // ensure the process exits as expected + System.exit(0); + } catch (Exception e) { + LOGGER.error("Unhandled error.", e); + System.exit(1); } - // ensure the process exits as expected - System.exit(0); } private static void handleFluent(CodeModel codeModel, EmitterOptions emitterOptions, boolean sdkIntegration) { diff --git a/packages/http-client-java/generator/http-client-generator/src/main/resources/simplelogger.properties b/packages/http-client-java/generator/http-client-generator/src/main/resources/simplelogger.properties index 5f370f28d7b..9ef1ac091d3 100644 --- a/packages/http-client-java/generator/http-client-generator/src/main/resources/simplelogger.properties +++ b/packages/http-client-java/generator/http-client-generator/src/main/resources/simplelogger.properties @@ -2,11 +2,10 @@ # Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. org.slf4j.simpleLogger.logFile=System.out - # Default logging detail level for all instances of SimpleLogger. # Must be one of ("trace", "debug", "info", "warn", or "error"). # If not specified, defaults to "info". -org.slf4j.simpleLogger.defaultLogLevel=error +org.slf4j.simpleLogger.defaultLogLevel=warn # Logging detail level for a SimpleLogger instance named "xxxxx". # Must be one of ("trace", "debug", "info", "warn", or "error"). @@ -25,7 +24,7 @@ org.slf4j.simpleLogger.defaultLogLevel=error # Set to true if you want to output the current thread name. # Defaults to true. -#org.slf4j.simpleLogger.showThreadName=true +org.slf4j.simpleLogger.showThreadName=false # Set to true if you want the Logger instance name to be included in output messages. # Defaults to true.