Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@typespec/compiler",
"comment": "Add relative file path utils, and allow emitter framework's ObjectBuilder to be initialized with a placeholder object.",
"type": "none"
}
],
"packageName": "@typespec/compiler"
}
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ words:
- xplat
- keyer
- interner
- unprefixed
ignorePaths:
- "**/node_modules/**"
- "**/dist/**"
Expand Down
157 changes: 157 additions & 0 deletions packages/compiler/core/path-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,164 @@ export function normalizeSlashes(path: string): string {
}

//#endregion
// #region relative paths
type GetCanonicalFileName = (fileName: string) => string;

/** @internal */
function equateValues<T>(a: T, b: T) {
return a === b;
}

/**
* Compare the equality of two strings using a case-sensitive ordinal comparison.
*
* Case-sensitive comparisons compare both strings one code-point at a time using the integer
* value of each code-point after applying `toUpperCase` to each string. We always map both
* strings to their upper-case form as some unicode characters do not properly round-trip to
* lowercase (such as `ẞ` (German sharp capital s)).
*
* @internal
*/
function equateStringsCaseInsensitive(a: string, b: string) {
return a === b || (a !== undefined && b !== undefined && a.toUpperCase() === b.toUpperCase());
}

/**
* Compare the equality of two strings using a case-sensitive ordinal comparison.
*
* Case-sensitive comparisons compare both strings one code-point at a time using the
* integer value of each code-point.
*
* @internal
*/
function equateStringsCaseSensitive(a: string, b: string) {
return equateValues(a, b);
}

/**
* Returns its argument.
*
* @internal
*/
function identity<T>(x: T) {
return x;
}

/**
* Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.).
*
* ```ts
* // POSIX
* pathIsAbsolute("/path/to/file.ext") === true
* // DOS
* pathIsAbsolute("c:/path/to/file.ext") === true
* // URL
* pathIsAbsolute("file:///path/to/file.ext") === true
* // Non-absolute
* pathIsAbsolute("path/to/file.ext") === false
* pathIsAbsolute("./path/to/file.ext") === false
* ```
*
* @internal
*/
function pathIsAbsolute(path: string): boolean {
return getEncodedRootLength(path) !== 0;
}

/**
* Determines whether a path starts with a relative path component (i.e. `.` or `..`).
*
* @internal
*/
function pathIsRelative(path: string): boolean {
return /^\.\.?($|[\\/])/.test(path);
}

/**
* Ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
* with `./` or `../`) so as not to be confused with an unprefixed module name.
*
* ```ts
* ensurePathIsNonModuleName("/path/to/file.ext") === "/path/to/file.ext"
* ensurePathIsNonModuleName("./path/to/file.ext") === "./path/to/file.ext"
* ensurePathIsNonModuleName("../path/to/file.ext") === "../path/to/file.ext"
* ensurePathIsNonModuleName("path/to/file.ext") === "./path/to/file.ext"
* ```
*
* @internal
*/
export function ensurePathIsNonModuleName(path: string): string {
return !pathIsAbsolute(path) && !pathIsRelative(path) ? "./" + path : path;
}

/** @internal */
function getPathComponentsRelativeTo(
from: string,
to: string,
stringEqualityComparer: (a: string, b: string) => boolean,
getCanonicalFileName: GetCanonicalFileName
) {
const fromComponents = reducePathComponents(getPathComponents(from));
const toComponents = reducePathComponents(getPathComponents(to));

let start: number;
for (start = 0; start < fromComponents.length && start < toComponents.length; start++) {
const fromComponent = getCanonicalFileName(fromComponents[start]);
const toComponent = getCanonicalFileName(toComponents[start]);
const comparer = start === 0 ? equateStringsCaseInsensitive : stringEqualityComparer;
if (!comparer(fromComponent, toComponent)) break;
}

if (start === 0) {
return toComponents;
}

const components = toComponents.slice(start);
const relative: string[] = [];
for (; start < fromComponents.length; start++) {
relative.push("..");
}
return ["", ...relative, ...components];
}

/**
* Gets a relative path that can be used to traverse between `from` and `to`.
*/
export function getRelativePathFromDirectory(from: string, to: string, ignoreCase: boolean): string;
/**
* Gets a relative path that can be used to traverse between `from` and `to`.
*/
export function getRelativePathFromDirectory(
fromDirectory: string,
to: string,
getCanonicalFileName: GetCanonicalFileName
): string; // eslint-disable-line @typescript-eslint/unified-signatures
export function getRelativePathFromDirectory(
fromDirectory: string,
to: string,
getCanonicalFileNameOrIgnoreCase: GetCanonicalFileName | boolean
) {
if (getRootLength(fromDirectory) > 0 !== getRootLength(to) > 0) {
throw new Error("Paths must either both be absolute or both be relative");
}
const getCanonicalFileName =
typeof getCanonicalFileNameOrIgnoreCase === "function"
? getCanonicalFileNameOrIgnoreCase
: identity;
const ignoreCase =
typeof getCanonicalFileNameOrIgnoreCase === "boolean"
? getCanonicalFileNameOrIgnoreCase
: false;
const pathComponents = getPathComponentsRelativeTo(
fromDirectory,
to,
ignoreCase ? equateStringsCaseInsensitive : equateStringsCaseSensitive,
getCanonicalFileName
);
return getPathFromPathComponents(pathComponents);
}

// #endregion
const enum CharacterCodes {
nullCharacter = 0,
maxAsciiCharacter = 0x7f,
Expand Down
15 changes: 12 additions & 3 deletions packages/compiler/emitter-framework/builders/object-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ import { EmitEntity, EmitterResult } from "../types.js";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface ObjectBuilder<T> extends Record<string, any> {}
export class ObjectBuilder<T> {
constructor(initializer: Record<string, unknown> = {}) {
for (const [key, value] of Object.entries(initializer)) {
this.set(key, value as any);
constructor(initializer: Record<string, unknown> | Placeholder<Record<string, unknown>> = {}) {
if (initializer instanceof Placeholder) {
initializer.onValue((v) => {
for (const [key, value] of Object.entries(v)) {
this.set(key, value as any);
}
});
} else {
for (const [key, value] of Object.entries(initializer)) {
this.set(key, value as any);
}
}
}

set(key: string, v: EmitEntity<T> | Placeholder<T> | T) {
let value = v;
if (v instanceof EmitterResult) {
Expand Down
1 change: 0 additions & 1 deletion packages/compiler/emitter-framework/type-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,6 @@ export class TypeEmitter<T, TOptions extends object = Record<string, never>> {
}

arrayDeclarationReferenceContext(array: Model): Context {
this.emitter.emitType(array.indexer!.value);
return {};
}

Expand Down
19 changes: 19 additions & 0 deletions packages/compiler/test/emitter-framework/emitter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ describe("emitter-framework: typescript emitter", () => {
assert.match(contents, /z: 12/);
});

it("emits unknown", async () => {
const contents = await emitTypeSpecToTs(`
model A {
x: unknown
}
`);

assert.match(contents, /x: unknown/);
});

it("emits array literals", async () => {
const contents = await emitTypeSpecToTs(`
model MyArray2 is Array<string>;
Expand All @@ -155,6 +165,15 @@ describe("emitter-framework: typescript emitter", () => {
assert.match(contents, /y: string\[\]/);
assert.match(contents, /z: \(string \| number\)\[\]/);
});

it("emits arrays of unknown", async () => {
const contents = await emitTypeSpecToTs(`
model MyArray2 is Array<unknown>;
`);

assert.match(contents, /MyArray2 extends Array<unknown>/);
});

// todo: what to do with optionals not at the end??
it("emits operations", async () => {
const contents = await emitTypeSpecToTs(`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function isArrayType(m: Model) {
}

export const intrinsicNameToTSType = new Map<string, string>([
["unknown", "unknown"],
["string", "string"],
["int32", "number"],
["int16", "number"],
Expand Down