@typespec/http-server-js
Version:
TypeSpec HTTP server code generator for JavaScript
212 lines (189 loc) • 6.21 kB
text/typescript
import { DiagnosticTarget, NoTarget } from "@typespec/compiler";
import { JsContext } from "../ctx.js";
import { reportDiagnostic } from "../lib.js";
import { UnreachableError } from "./error.js";
/**
* A conceptual lexical scope.
*/
export interface Scope {
/**
* Declare a name in the scope, applying the appropriate resolution strategy if necessary.
*
* @param primaryName - the primary name we want to declare in this scope
* @param options - options for the declaration
* @returns the name that was finally declared in the scope
*/
declare(primaryName: string, options?: DeclarationOptions): string;
/**
* Determines whether or not a given name is declared in the scope.
*
* @param name - the name to check for declaration
*/
isDeclared(name: string): boolean;
}
export interface DeclarationOptions {
/**
* The source of the declaration, to be used when raising diagnostics.
*
* Default: NoTarget
*/
source?: DiagnosticTarget | typeof NoTarget;
/**
* The resolution strategy to use if the declared name conflicts with an already declared name.
*
* Default: "shadow"
*/
resolutionStrategy?: ResolutionStrategy;
}
const DEFAULT_DECLARATION_OPTIONS: Required<DeclarationOptions> = {
source: NoTarget,
resolutionStrategy: "shadow",
};
/**
* A strategy to use when attempting to resolve naming conflicts. This can be one of the following types:
*
* - `none`: no attempt will be made to resolve the naming conflict.
* - `shadow`: if the scope does not directly declare the name, this declaration will shadow it.
* - `prefix`: if the name is already declared, a prefix will be added to the name to resolve the conflict.
* - `alt-name`: if the name is already declared, an alternative name will be used to resolve the conflict.
*/
export type ResolutionStrategy = PrefixResolution | AltNameResolution | "shadow" | "none";
/**
* A resolution strategy that prepends a prefix.
*/
export interface PrefixResolution {
kind: "prefix";
/**
* The prefix to append to the name.
*
* Default: "_".
*/
prefix?: string;
/**
* Whether or not to repeat the prefix until the conflict is resolved.
*/
repeated?: boolean;
/**
* Whether or not the name should shadow existing declarations.
*
* This setting applies to the primary name as well, so if the primary name is not own-declared in the scope, no
* prefix will be added.
*/
shadow?: boolean;
}
/**
* A resolution strategy that attempts to use an alternative name to resolve conflicts.
*/
export interface AltNameResolution {
kind: "alt-name";
/**
* The alternative name for this declaration.
*/
altName: string;
}
const NO_PARENT: Scope = {
declare() {
throw new UnreachableError("Cannot declare in the no-parent scope");
},
isDeclared() {
return false;
},
};
/**
* Create a new scope.
*
* @param ctx - the JS emitter context.
* @param parent - an optional parent scope for this scope. It will consider declarations in the parent scope for some conflicts.
*/
export function createScope(ctx: JsContext, parent: Scope = NO_PARENT): Scope {
const ownDeclarations: Set<string> = new Set();
const self: Scope = {
declare(primaryName, options = {}) {
const { source: target, resolutionStrategy } = { ...DEFAULT_DECLARATION_OPTIONS, ...options };
if (!self.isDeclared(primaryName)) {
ownDeclarations.add(primaryName);
return primaryName;
}
// Apply resolution strategy
const resolutionStrategyName =
typeof resolutionStrategy === "string" ? resolutionStrategy : resolutionStrategy.kind;
switch (resolutionStrategyName) {
case "none":
// Report diagnostic and return the name as is.
reportDiagnostic(ctx.program, {
code: "name-conflict",
format: {
name: primaryName,
},
target,
});
return primaryName;
case "shadow":
// Check to make sure this name isn't an own-declaration, and if not allow it, otherwise raise a diagnostic.
if (!ownDeclarations.has(primaryName)) {
ownDeclarations.add(primaryName);
return primaryName;
} else {
reportDiagnostic(ctx.program, {
code: "name-conflict",
format: {
name: primaryName,
},
target,
});
return primaryName;
}
case "prefix": {
const {
prefix = "_",
repeated = false,
shadow = true,
} = resolutionStrategy as PrefixResolution;
let name = primaryName;
const isDeclared = shadow ? (name: string) => ownDeclarations.has(name) : self.isDeclared;
while (isDeclared(name)) {
name = prefix + name;
if (!repeated) break;
}
if (isDeclared(name)) {
// We were not able to resolve the conflict with this strategy, so raise a diagnostic.
reportDiagnostic(ctx.program, {
code: "name-conflict",
format: {
name: name,
},
target,
});
return name;
}
ownDeclarations.add(name);
return name;
}
case "alt-name": {
const { altName } = resolutionStrategy as AltNameResolution;
if (!self.isDeclared(altName)) {
ownDeclarations.add(altName);
return altName;
}
// We were not able to resolve the conflict with this strategy, so raise a diagnostic.
reportDiagnostic(ctx.program, {
code: "name-conflict",
format: {
name: altName,
},
target,
});
return altName;
}
default:
throw new UnreachableError(`Unknown resolution strategy: ${resolutionStrategy}`, {
resolutionStrategyName,
});
}
},
isDeclared(name) {
return ownDeclarations.has(name) || parent.isDeclared(name);
},
};
return self;
}