Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/rlc-common/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
}
],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error"
"@typescript-eslint/no-misused-promises": "error",
"no-console": "error"
}
}
3 changes: 2 additions & 1 deletion packages/typespec-ts/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-deprecated": "warn"
"@typescript-eslint/no-deprecated": "warn",
"no-console": "error"
}
}
20 changes: 16 additions & 4 deletions packages/typespec-ts/src/framework/load-static-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { refkey } from "./refkey.js";
import { resolveProjectRoot } from "../utils/resolve-project-root.js";
import { isAzurePackage } from "@azure-tools/rlc-common";
import { ModularEmitterOptions } from "../modular/interfaces.js";
import { NoTarget, Program } from "@typespec/compiler";
import { reportDiagnostic } from "../lib.js";
export const SourceFileSymbol = Symbol("SourceFile");
export interface StaticHelperMetadata {
name: string;
Expand Down Expand Up @@ -41,6 +43,7 @@ export interface LoadStaticHelpersOptions
extends Partial<ModularEmitterOptions> {
helpersAssetDirectory?: string;
sourcesDir?: string;
program?: Program;
}

export async function loadStaticHelpers(
Expand All @@ -55,7 +58,8 @@ export async function loadStaticHelpers(
DEFAULT_STATIC_HELPERS_PATH
);
const files = await traverseDirectory(
options.helpersAssetDirectory ?? defaultStaticHelpersPath
options.helpersAssetDirectory ?? defaultStaticHelpersPath,
options.program
);

for (const file of files) {
Expand Down Expand Up @@ -161,12 +165,13 @@ function getDeclarationByMetadata(
const _targetStaticHelpersBaseDir = "static-helpers";
async function traverseDirectory(
directory: string,
program?: Program,
result: { source: string; target: string }[] = [],
relativePath: string = ""
): Promise<{ source: string; target: string }[]> {
try {
const files = await readdir(directory);
const files = await readdir(directory);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why move this line outside the try catch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! The readdir(directory) call should be inside the try-catch block so that if it fails, we can properly report the diagnostic error. I've moved it inside the try block. Commit: 5c8b9e2


try {
await Promise.all(
files.map(async (file) => {
const filePath = path.join(directory, file);
Expand All @@ -175,6 +180,7 @@ async function traverseDirectory(
if (fileStat.isDirectory()) {
await traverseDirectory(
filePath,
program,
result,
path.join(relativePath, file)
);
Expand All @@ -195,7 +201,13 @@ async function traverseDirectory(

return result;
} catch (error) {
console.error(`Error traversing directory ${directory}:`, error);
if (program) {
reportDiagnostic(program, {
code: "directory-traversal-error",
format: { directory, error: String(error) },
target: NoTarget
});
}
throw error;
}
}
6 changes: 0 additions & 6 deletions packages/typespec-ts/src/framework/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,6 @@ sourceFile2.addStatements(`let obj: ${modelReference} = { id: 1 };`);
// Apply imports to ensure correct references
binder.resolveAllReferences("/modularPackageFolder/src");

// Output the generated files
console.log("// test.ts");
console.log(sourceFile.getFullText());
console.log("// test2.ts");
console.log(sourceFile2.getFullText());

// Output
// test.ts
// interface MyInterface {
Expand Down
26 changes: 2 additions & 24 deletions packages/typespec-ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,14 @@ import { emitSamples } from "./modular/emitSamples.js";
export * from "./lib.js";

export async function $onEmit(context: EmitContext) {
console.time("onEmit");
if (context.program.compilerOptions.noEmit || context.program.hasError()) {
return;
}
/** Shared status */
const outputProject = new Project();
const program: Program = context.program;
const emitterOptions: EmitterOptions = context.options;
console.time("onEmit: create context");
const dpgContext = await createContextWithDefaultOptions(context);
console.timeEnd("onEmit: create context");
// Enrich the dpg context with path detail and common options
await enrichDpgContext();
const rlcOptions = dpgContext.rlcOptions ?? {};
Expand All @@ -124,7 +121,6 @@ export async function $onEmit(context: EmitContext) {
compilerContext: context,
tcgcContext: dpgContext
});
console.time("onEmit: load static helpers");
const staticHelpers = await loadStaticHelpers(
outputProject,
{
Expand All @@ -137,36 +133,32 @@ export async function $onEmit(context: EmitContext) {
},
{
sourcesDir: dpgContext.generationPathDetail?.modularSourcesDir,
options: rlcOptions
options: rlcOptions,
program
}
);
console.timeEnd("onEmit: load static helpers");
const extraDependencies = isAzurePackage({ options: rlcOptions })
? {
...AzurePollingDependencies,
...AzureCoreDependencies,
...AzureIdentityDependencies
}
: { ...DefaultCoreDependencies };
console.time("onEmit: provide binder");
const binder = provideBinder(outputProject, {
staticHelpers,
dependencies: {
...extraDependencies
}
});
provideSdkTypes(dpgContext);
console.timeEnd("onEmit: provide binder");

const rlcCodeModels: RLCModel[] = [];
let modularEmitterOptions: ModularEmitterOptions;
// 1. Clear sources folder
await clearSrcFolder();
// 2. Generate RLC code model
// TODO: skip this step in modular once modular generator is sufficiently decoupled
console.time("onEmit: build RLC code models");
await buildRLCCodeModels();
console.timeEnd("onEmit: build RLC code models");

// 4. Generate sources
if (emitterOptions["is-modular-library"]) {
Expand Down Expand Up @@ -267,7 +259,6 @@ export async function $onEmit(context: EmitContext) {
}

async function generateModularSources() {
console.time("onEmit: generate modular sources");
const modularSourcesRoot =
dpgContext.generationPathDetail?.modularSourcesDir ?? "src";
const project = useContext("outputProject");
Expand All @@ -290,13 +281,10 @@ export async function $onEmit(context: EmitContext) {
);

const isMultiClients = dpgContext.sdkPackage.clients.length > 1;
console.time("onEmit: emit models");
emitTypes(dpgContext, { sourceRoot: modularSourcesRoot });
console.timeEnd("onEmit: emit models");
buildSubpathIndexFile(modularEmitterOptions, "models", undefined, {
recursive: true
});
console.time("onEmit: emit source files");
const clientMap = getClientHierarchyMap(dpgContext);
if (clientMap.length === 0) {
// If no clients, we still need to build the root index file
Expand Down Expand Up @@ -336,27 +324,20 @@ export async function $onEmit(context: EmitContext) {
subClient
);
}
console.timeEnd("onEmit: emit source files");
// Enable modular sample generation when explicitly set to true or MPG
if (emitterOptions["generate-sample"] === true) {
console.time("onEmit: emit samples");
const samples = emitSamples(dpgContext);
console.timeEnd("onEmit: emit samples");
// Refine the rlc sample generation logic
// TODO: remember to remove this out when RLC is splitted from Modular
if (samples.length > 0) {
dpgContext.rlcOptions!.generateSample = true;
}
}

console.time("onEmit: resolve references");
binder.resolveAllReferences(modularSourcesRoot);
if (program.compilerOptions.noEmit || program.hasError()) {
return;
}
console.timeEnd("onEmit: resolve references");

console.time("onEmit: generate files");

for (const file of project.getSourceFiles()) {
await emitContentByBuilder(
Expand All @@ -365,8 +346,6 @@ export async function $onEmit(context: EmitContext) {
modularEmitterOptions as any
);
}
console.timeEnd("onEmit: generate files");
console.timeEnd("onEmit: generate modular sources");
}
interface Metadata {
apiVersion?: string;
Expand Down Expand Up @@ -541,7 +520,6 @@ export async function $onEmit(context: EmitContext) {
.map((subClient) => getClientContextPath(subClient, options))
.map((path) => path.substring(path.indexOf("src")));
}
console.timeEnd("onEmit");
}

export async function createContextWithDefaultOptions(
Expand Down
19 changes: 19 additions & 0 deletions packages/typespec-ts/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,25 @@ const libDef = {
messages: {
default: paramMessage`The parameter name ${"parameterName"} has conflicts with others and please use @clientName to rename it.`
}
},
"file-format-error": {
severity: "error",
messages: {
default: paramMessage`Failed to format file: ${"filePath"}`
}
},
"directory-traversal-error": {
severity: "error",
messages: {
default: paramMessage`Error traversing directory ${"directory"}: ${"error"}`
}
},
"client-api-version-not-supported": {
severity: "warning",
messages: {
default:
"This client does not support client api-version, please change it at the operation level"
}
}
},
emitter: {
Expand Down
9 changes: 6 additions & 3 deletions packages/typespec-ts/src/modular/buildClientContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,18 @@ export function buildClientContext(
});`;
}
} else if (isAzurePackage(emitterOptions)) {
// Report diagnostic during code generation
reportDiagnostic(dpgContext.program, {
code: "client-api-version-not-supported",
target: NoTarget
});
apiVersionPolicyStatement += `
if (options.apiVersion) {
logger.warning("This client does not support client api-version, please change it at the operation level");
}`;
} else {
apiVersionPolicyStatement += `
if (options.apiVersion) {
console.warn("This client does not support client api-version, please change it at the operation level");
}`;
// This client does not support client api-version, please change it at the operation level`;
}
factoryFunction.addStatements(apiVersionPolicyStatement);

Expand Down
14 changes: 11 additions & 3 deletions packages/typespec-ts/src/utils/emitUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import {
isAzurePackage,
RLCModel
} from "@azure-tools/rlc-common";
import { CompilerHost, Program } from "@typespec/compiler";
import { CompilerHost, NoTarget, Program } from "@typespec/compiler";
import { dirname, join } from "path";
import { format } from "prettier";
import { prettierJSONOptions, prettierTypeScriptOptions } from "../lib.js";
import {
prettierJSONOptions,
prettierTypeScriptOptions,
reportDiagnostic
} from "../lib.js";

export async function emitModels(rlcModels: RLCModel, program: Program) {
const schemaOutput = buildSchemaTypes(rlcModels);
Expand Down Expand Up @@ -78,7 +82,11 @@ async function emitFile(
isJson ? prettierJSONOptions : prettierTypeScriptOptions
);
} catch (e) {
console.error(`Failed to format file: ${filePath}`);
reportDiagnostic(program, {
code: "file-format-error",
format: { filePath },
target: NoTarget
});
throw e;
}
}
Expand Down
41 changes: 41 additions & 0 deletions packages/typespec-ts/test-next/unit/diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NoTarget, Program } from "@typespec/compiler";
import * as lib from "../../src/lib.js";

// Mock the reportDiagnostic function
const mockReportDiagnostic = vi.fn();

describe("Diagnostic reporting", () => {
beforeEach(() => {
mockReportDiagnostic.mockClear();
// Spy on reportDiagnostic
vi.spyOn(lib, "reportDiagnostic").mockImplementation(mockReportDiagnostic);
});

describe("file-format-error diagnostic", () => {
it("should report diagnostic when file formatting fails", async () => {
// Import the module that uses reportDiagnostic
const { default: emitUtil } = await import("../../src/utils/emitUtil.js");

// This test verifies the diagnostic code is defined
expect(lib.$lib.diagnostics["file-format-error"]).toBeDefined();
expect(lib.$lib.diagnostics["file-format-error"].severity).toBe("error");
});
});

describe("directory-traversal-error diagnostic", () => {
it("should report diagnostic when directory traversal fails", () => {
// This test verifies the diagnostic code is defined
expect(lib.$lib.diagnostics["directory-traversal-error"]).toBeDefined();
expect(lib.$lib.diagnostics["directory-traversal-error"].severity).toBe("error");
});
});

describe("client-api-version-not-supported diagnostic", () => {
it("should report diagnostic when client api-version is not supported", () => {
// This test verifies the diagnostic code is defined
expect(lib.$lib.diagnostics["client-api-version-not-supported"]).toBeDefined();
expect(lib.$lib.diagnostics["client-api-version-not-supported"].severity).toBe("warning");
});
});
});