async execute()

in eng/tools/typespec-validation/src/rules/compile.ts [15:214]


  async execute(folder: string): Promise<RuleResult> {
    let success = true;
    let stdOutput = "";
    let errorOutput = "";

    if (await fileExists(path.join(folder, "main.tsp"))) {
      let [err, stdout, stderr] = await runNpm([
        "exec",
        "--no",
        "--",
        "tsp",
        "compile",
        "--list-files",
        "--warn-as-error",
        folder,
      ]);

      stdOutput += stdout;

      // Rule output is easier to read if "tsp compile" stderr is redirected to stdOutput
      stdOutput += stderr;

      if (
        stdout.toLowerCase().includes("no emitter was configured") ||
        stdout.toLowerCase().includes("no output was generated")
      ) {
        success = false;
        errorOutput += "No emitter was configured and/or no output was generated.";
      }

      if (success) {
        if (!err) {
          // Check for *extra* typespec-generated swagger files under the output folder, which
          // indicates a mismatch between TypeSpec and swaggers.

          // Example 'stdout':
          //
          // TypeSpec compiler v0.67.2
          //
          // ../resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json
          // ../resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Operations_List.json
          //
          // Compilation completed successfully.

          // Remove ANSI color codes, handle windows and linux line endings
          const lines = stripAnsi(stdout).split(/\r?\n/);

          // TODO: Use helpers in /.github once they support platform-specific paths
          // Header, footer, and empty lines should be excluded by JSON filter
          const outputSwaggers = lines
            // Remove leading and trailing whitespace
            .map((l) => l.trim())
            // Normalize to platform-specific path
            .map((l) => normalize(l))
            // Filter to JSON files
            .filter((p) => basename(p).toLowerCase().endsWith(".json"))
            // Exclude examples
            .filter((p) => !p.split(path.sep).includes("examples"));

          stdOutput += "\nGenerated Swaggers:\n";
          stdOutput += outputSwaggers.join("\n") + "\n";

          if (outputSwaggers.length === 0) {
            throw new Error("No generated swaggers found in output of 'tsp compile'");
          }

          // ../resource-manager/Microsoft.Contoso
          const outputFolder = dirname(dirname(dirname(outputSwaggers[0])));
          const outputFilename = basename(outputSwaggers[0]);

          stdOutput += "\nOutput folder:\n";
          stdOutput += outputFolder + "\n";

          // Filter to only specs matching the folder and filename extracted from the first output-file.
          // Necessary to handle multi-project specs like keyvault.
          //
          // Globby only accepts patterns like posix paths.
          const pattern = path.posix.join(
            ...outputFolder.split(path.win32.sep),
            "**",
            outputFilename,
          );
          const allSwaggers = (await globby(pattern, { ignore: ["**/examples/**"] })).map(
            // Globby always returns posix paths
            (p) => normalize(p),
          );

          // Filter to files generated by TypeSpec
          const tspGeneratedSwaggers = await filterAsync(
            allSwaggers,
            async (swaggerPath: string) => {
              const swaggerText = await readFile(swaggerPath, { encoding: "utf8" });
              const swaggerObj = JSON.parse(swaggerText);
              return (
                swaggerObj["info"]?.["x-typespec-generated"] ||
                swaggerObj["info"]?.["x-cadl-generated"]
              );
            },
          );

          stdOutput += `\nSwaggers matching output folder and filename:\n`;
          stdOutput += tspGeneratedSwaggers.join("\n") + "\n";

          const suppressedSwaggers = await filterAsync(
            tspGeneratedSwaggers,
            async (swaggerPath: string) => {
              const suppressions = await getSuppressions(swaggerPath);

              const extraSwaggerSuppressions = suppressions.filter(
                (s) => s.rules?.includes(this.name) && s.subRules?.includes("ExtraSwagger"),
              );

              // Each path must specify a single version (without wildcards) under "preview|stable"
              //
              // Allowed:    data-plane/Azure.Contoso.WidgetManager/preview/2022-11-01-preview/**/*.json
              // Disallowed: data-plane/Azure.Contoso.WidgetManager/preview/**/*.json
              // Disallowed: data-plane/**/*.json
              //
              // Include "." since a few specs use versions like "X.Y" instead of "YYYY-MM-DD"
              const singleVersionPattern = "/(preview|stable)/[A-Za-z0-9._-]+/";

              for (const suppression of extraSwaggerSuppressions) {
                for (const p of suppression.paths) {
                  if (!p.match(singleVersionPattern)) {
                    throw new Error(
                      `Invalid path '${p}'. Path must only include one version per suppression.`,
                    );
                  }
                }
              }

              return extraSwaggerSuppressions.length > 0;
            },
          );

          stdOutput += `\nSwaggers excluded via suppressions.yaml:\n`;
          stdOutput += suppressedSwaggers.join("\n") + "\n";

          const remainingSwaggers = tspGeneratedSwaggers.filter(
            (s) => !suppressedSwaggers.includes(s),
          );

          stdOutput += `\nRemaining swaggers:\n`;
          stdOutput += remainingSwaggers.join("\n") + "\n";

          const extraSwaggers = remainingSwaggers.filter((s) => !outputSwaggers.includes(s));

          if (extraSwaggers.length > 0) {
            success = false;
            errorOutput += pc.red(
              `\nOutput folder '${outputFolder}' appears to contain TypeSpec-generated ` +
                `swagger files, not generated from the current TypeSpec sources. ` +
                `Perhaps you deleted a version from your TypeSpec, but didn't delete ` +
                `the associated swaggers?\n\n`,
            );
            errorOutput += pc.red(extraSwaggers.join("\n") + "\n");
          }
        } else {
          success = false;
          errorOutput += err.message;
        }
      }
    }

    const clientTsp = path.join(folder, "client.tsp");
    if (await fileExists(clientTsp)) {
      let [err, stdout, stderr] = await runNpm([
        "exec",
        "--no",
        "--",
        "tsp",
        "compile",
        "--no-emit",
        "--warn-as-error",
        clientTsp,
      ]);
      if (err) {
        success = false;
        errorOutput += err.message;
      }
      stdOutput += stdout;
      errorOutput += stderr;
    }

    if (success) {
      const gitDiffResult = await gitDiffTopSpecFolder(folder);
      stdOutput += gitDiffResult.stdOutput;
      if (!gitDiffResult.success) {
        success = false;
        errorOutput += gitDiffResult.errorOutput;
        errorOutput += `\nFiles have been changed after \`tsp compile\`. Run \`tsp compile\` and ensure all files are included in your change.`;
      }
    }

    return {
      success: success,
      stdOutput: stdOutput,
      errorOutput: errorOutput,
    };
  }