From 9a92bd9099b0113502e1afb9671bfe98cc356942 Mon Sep 17 00:00:00 2001
From: Ron Buckton <ron.buckton@microsoft.com>
Date: Sun, 1 May 2022 00:44:20 +0000
Subject: [PATCH 1/2] Add additional utility methods to DeclarationReference

---
 ...beta-declref-utility_2022-05-16-07-02.json |   11 +
 tsdoc/src/beta/DeclarationReference.ts        | 2557 ++++++++++++++---
 .../__tests__/DeclarationReference.test.ts    | 1767 +++++++++++-
 tsdoc/src/parser/StringChecks.ts              |   38 +
 4 files changed, 3942 insertions(+), 431 deletions(-)
 create mode 100644 common/changes/@microsoft/tsdoc/beta-declref-utility_2022-05-16-07-02.json

diff --git a/common/changes/@microsoft/tsdoc/beta-declref-utility_2022-05-16-07-02.json b/common/changes/@microsoft/tsdoc/beta-declref-utility_2022-05-16-07-02.json
new file mode 100644
index 00000000..2f619762
--- /dev/null
+++ b/common/changes/@microsoft/tsdoc/beta-declref-utility_2022-05-16-07-02.json
@@ -0,0 +1,11 @@
+{
+  "changes": [
+    {
+      "packageName": "@microsoft/tsdoc",
+      "comment": "add utility methods to DeclarationReference components",
+      "type": "minor"
+    }
+  ],
+  "packageName": "@microsoft/tsdoc",
+  "email": "ron.buckton@microsoft.com"
+}
\ No newline at end of file
diff --git a/tsdoc/src/beta/DeclarationReference.ts b/tsdoc/src/beta/DeclarationReference.ts
index 750a8b43..a4fe0d97 100644
--- a/tsdoc/src/beta/DeclarationReference.ts
+++ b/tsdoc/src/beta/DeclarationReference.ts
@@ -5,67 +5,77 @@
 /* eslint-disable no-inner-declarations */
 /* eslint-disable @typescript-eslint/no-use-before-define */
 /* eslint-disable @typescript-eslint/naming-convention */
+/* eslint-disable @rushstack/no-new-null */
 
 // NOTE: See DeclarationReference.grammarkdown for information on the underlying grammar.
+// NOTE: @rushstack/no-new-null is disabled for places where `null` is used as a sentinel to
+//       indicate explicit non-presence of a value (such as when removing values using `.with()`).
 
 import { StringChecks } from '../parser/StringChecks';
 
+// #region DeclarationReference
+
 /**
  * Represents a reference to a declaration.
  * @beta
  */
 export class DeclarationReference {
-  private _source: ModuleSource | GlobalSource | undefined;
-  private _navigation: Navigation.Locals | Navigation.Exports | undefined;
+  private _source: Source | undefined;
+  private _navigation: SourceNavigation | undefined;
   private _symbol: SymbolReference | undefined;
 
-  public constructor(
-    source?: ModuleSource | GlobalSource,
-    navigation?: Navigation.Locals | Navigation.Exports,
-    symbol?: SymbolReference
-  ) {
+  public constructor(source?: Source, navigation?: SourceNavigation, symbol?: SymbolReference) {
     this._source = source;
     this._navigation = navigation;
     this._symbol = symbol;
   }
 
-  public get source(): ModuleSource | GlobalSource | undefined {
+  /**
+   * Gets the source for the declaration.
+   */
+  public get source(): Source | undefined {
     return this._source;
   }
 
-  public get navigation(): Navigation.Locals | Navigation.Exports | undefined {
-    if (!this._source || !this._symbol) {
-      return undefined;
-    }
-    if (this._source === GlobalSource.instance) {
-      return Navigation.Locals;
-    }
-    if (this._navigation === undefined) {
-      return Navigation.Exports;
-    }
-    return this._navigation;
+  /**
+   * Gets whether the symbol for the declaration is a local or exported symbol of the source.
+   */
+  public get navigation(): SourceNavigation | undefined {
+    return resolveNavigation(this._source, this._symbol, this._navigation);
   }
 
+  /**
+   * Gets the symbol reference for the declaration.
+   */
   public get symbol(): SymbolReference | undefined {
     return this._symbol;
   }
 
+  /**
+   * Gets a value indicating whether this {@link DeclarationReference} is empty.
+   */
   public get isEmpty(): boolean {
     return this.source === undefined && this.symbol === undefined;
   }
 
+  /**
+   * Parses a {@link DeclarationReference} from the provided text.
+   */
   public static parse(text: string): DeclarationReference {
     const parser: Parser = new Parser(text);
     const reference: DeclarationReference = parser.parseDeclarationReference();
     if (parser.errors.length) {
       throw new SyntaxError(`Invalid DeclarationReference '${text}':\n  ${parser.errors.join('\n  ')}`);
-    }
-    if (!parser.eof) {
+    } else if (!parser.eof) {
       throw new SyntaxError(`Invalid DeclarationReference '${text}'`);
+    } else {
+      return reference;
     }
-    return reference;
   }
 
+  /**
+   * Parses a {@link Component} from the provided text.
+   */
   public static parseComponent(text: string): Component {
     if (text[0] === '[') {
       return ComponentReference.parse(text);
@@ -93,12 +103,14 @@ export class DeclarationReference {
   public static escapeComponentString(text: string): string {
     if (text.length === 0) {
       return '""';
+    } else {
+      const ch: string = text.charAt(0);
+      if (ch === '[' || ch === '"' || !this.isWellFormedComponentString(text)) {
+        return JSON.stringify(text);
+      } else {
+        return text;
+      }
     }
-    const ch: string = text.charAt(0);
-    if (ch === '[' || ch === '"' || !this.isWellFormedComponentString(text)) {
-      return JSON.stringify(text);
-    }
-    return text;
   }
 
   /**
@@ -111,11 +123,11 @@ export class DeclarationReference {
       } catch {
         throw new SyntaxError(`Invalid Component '${text}'`);
       }
-    }
-    if (!this.isWellFormedComponentString(text)) {
+    } else if (!this.isWellFormedComponentString(text)) {
       throw new SyntaxError(`Invalid Component '${text}'`);
+    } else {
+      return text;
     }
-    return text;
   }
 
   /**
@@ -138,12 +150,14 @@ export class DeclarationReference {
   public static escapeModuleSourceString(text: string): string {
     if (text.length === 0) {
       return '""';
+    } else {
+      const ch: string = text.charAt(0);
+      if (ch === '"' || !this.isWellFormedModuleSourceString(text)) {
+        return JSON.stringify(text);
+      } else {
+        return text;
+      }
     }
-    const ch: string = text.charAt(0);
-    if (ch === '"' || !this.isWellFormedModuleSourceString(text)) {
-      return JSON.stringify(text);
-    }
-    return text;
   }
 
   /**
@@ -156,84 +170,183 @@ export class DeclarationReference {
       } catch {
         throw new SyntaxError(`Invalid Module source '${text}'`);
       }
-    }
-    if (!this.isWellFormedModuleSourceString(text)) {
+    } else if (!this.isWellFormedModuleSourceString(text)) {
       throw new SyntaxError(`Invalid Module source '${text}'`);
+    } else {
+      return text;
     }
-    return text;
   }
 
+  /**
+   * Returns an empty {@link DeclarationReference}.
+   */
   public static empty(): DeclarationReference {
     return new DeclarationReference();
   }
 
+  /**
+   * Creates a new {@link DeclarationReference} for the provided package.
+   */
   public static package(packageName: string, importPath?: string): DeclarationReference {
     return new DeclarationReference(ModuleSource.fromPackage(packageName, importPath));
   }
 
+  /**
+   * Creates a new {@link DeclarationReference} for the provided module path.
+   */
   public static module(path: string, userEscaped?: boolean): DeclarationReference {
     return new DeclarationReference(new ModuleSource(path, userEscaped));
   }
 
+  /**
+   * Creates a new {@link DeclarationReference} for the global scope.
+   */
   public static global(): DeclarationReference {
     return new DeclarationReference(GlobalSource.instance);
   }
 
-  public static from(base: DeclarationReference | undefined): DeclarationReference {
-    return base || this.empty();
+  /**
+   * Creates a new {@link DeclarationReference} from the provided parts.
+   */
+  public static from(parts: DeclarationReferenceLike</*With*/ false> | undefined): DeclarationReference {
+    const resolved: ResolvedDeclarationReferenceLike = resolveDeclarationReferenceLike(
+      parts,
+      /*fallbackReference*/ undefined
+    );
+    if (resolved instanceof DeclarationReference) {
+      return resolved;
+    } else {
+      const { source, navigation, symbol } = resolved;
+      return new DeclarationReference(
+        source === undefined ? undefined : Source.from(source),
+        navigation,
+        symbol === undefined ? undefined : SymbolReference.from(symbol)
+      );
+    }
   }
 
-  public withSource(source: ModuleSource | GlobalSource | undefined): DeclarationReference {
-    return this._source === source ? this : new DeclarationReference(source, this._navigation, this._symbol);
+  /**
+   * Returns a {@link DeclarationReference} updated with the provided parts.
+   * If a part is set to `undefined`, the current value is used.
+   * If a part is set to `null`, the part will be removed in the result.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parts.
+   */
+  public with(parts: DeclarationReferenceParts</*With*/ true>): DeclarationReference {
+    const { source, navigation, symbol } = resolveDeclarationReferenceParts(
+      parts,
+      this.source,
+      this.navigation,
+      this.symbol
+    );
+    const resolvedSource: Source | undefined = source === undefined ? undefined : Source.from(source);
+    const resolvedSymbol: SymbolReference | undefined =
+      symbol === undefined ? undefined : SymbolReference.from(symbol);
+    const resolvedNavigation: SourceNavigation | undefined = resolveNavigation(
+      resolvedSource,
+      resolvedSymbol,
+      navigation
+    );
+    if (
+      Source.equals(this.source, resolvedSource) &&
+      this.navigation === resolvedNavigation &&
+      SymbolReference.equals(this.symbol, resolvedSymbol)
+    ) {
+      return this;
+    } else {
+      return new DeclarationReference(resolvedSource, navigation, resolvedSymbol);
+    }
   }
 
-  public withNavigation(
-    navigation: Navigation.Locals | Navigation.Exports | undefined
-  ): DeclarationReference {
-    return this._navigation === navigation
-      ? this
-      : new DeclarationReference(this._source, navigation, this._symbol);
+  /**
+   * Returns an {@link DeclarationReference} updated with the provided source.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided source.
+   */
+  public withSource(source: Source | undefined): DeclarationReference {
+    return this.with({ source: source ?? null });
+  }
+
+  /**
+   * Returns an {@link DeclarationReference} updated with the provided navigation.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided navigation.
+   */
+  public withNavigation(navigation: SourceNavigation | undefined): DeclarationReference {
+    return this.with({ navigation: navigation ?? null });
   }
 
+  /**
+   * Returns an {@link DeclarationReference} updated with the provided symbol.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided symbol.
+   */
   public withSymbol(symbol: SymbolReference | undefined): DeclarationReference {
-    return this._symbol === symbol ? this : new DeclarationReference(this._source, this._navigation, symbol);
+    return this.with({ symbol: symbol ?? null });
   }
 
-  public withComponentPath(componentPath: ComponentPath): DeclarationReference {
-    return this.withSymbol(
-      this.symbol ? this.symbol.withComponentPath(componentPath) : new SymbolReference(componentPath)
-    );
+  /**
+   * Returns an {@link DeclarationReference} whose symbol has been updated with the provided component path.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided component path.
+   */
+  public withComponentPath(componentPath: ComponentPath | undefined): DeclarationReference {
+    return this.with({ componentPath: componentPath ?? null });
   }
 
+  /**
+   * Returns an {@link DeclarationReference} whose symbol has been updated with the provided meaning.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided meaning.
+   */
   public withMeaning(meaning: Meaning | undefined): DeclarationReference {
-    if (!this.symbol) {
-      if (meaning === undefined) {
-        return this;
-      }
-      return this.withSymbol(SymbolReference.empty().withMeaning(meaning));
-    }
-    return this.withSymbol(this.symbol.withMeaning(meaning));
+    return this.with({ meaning: meaning ?? null });
   }
 
+  /**
+   * Returns an {@link DeclarationReference} whose symbol has been updated with the provided overload index.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided overload index.
+   */
   public withOverloadIndex(overloadIndex: number | undefined): DeclarationReference {
-    if (!this.symbol) {
-      if (overloadIndex === undefined) {
-        return this;
-      }
-      return this.withSymbol(SymbolReference.empty().withOverloadIndex(overloadIndex));
-    }
-    return this.withSymbol(this.symbol.withOverloadIndex(overloadIndex));
+    return this.with({ overloadIndex: overloadIndex ?? null });
   }
 
-  public addNavigationStep(navigation: Navigation, component: ComponentLike): DeclarationReference {
+  /**
+   * Returns a new {@link DeclarationReference} whose symbol has been updated to include the provided navigation step in its component path.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided navigation step.
+   */
+  public addNavigationStep(
+    navigation: Navigation,
+    component: ComponentLike</*With*/ false>
+  ): DeclarationReference {
     if (this.symbol) {
       return this.withSymbol(this.symbol.addNavigationStep(navigation, component));
+    } else {
+      if (navigation === Navigation.Members) {
+        navigation = Navigation.Exports;
+      }
+      return this.with({
+        navigation,
+        symbol: SymbolReference.from({ componentPath: ComponentRoot.from({ component }) })
+      });
     }
-    if (navigation === Navigation.Members) {
-      navigation = Navigation.Exports;
+  }
+
+  /**
+   * Tests whether two {@link DeclarationReference} objects are equivalent.
+   */
+  public static equals(
+    left: DeclarationReference | undefined,
+    right: DeclarationReference | undefined
+  ): boolean {
+    if (left === undefined) {
+      return right === undefined || right.isEmpty;
+    } else if (right === undefined) {
+      return left === undefined || left.isEmpty;
+    } else {
+      return left.toString() === right.toString();
     }
-    const symbol: SymbolReference = new SymbolReference(new ComponentRoot(Component.from(component)));
-    return new DeclarationReference(this.source, navigation, symbol);
+  }
+
+  /**
+   * Tests whether this object is equivalent to `other`.
+   */
+  public equals(other: DeclarationReference): boolean {
+    return DeclarationReference.equals(this, other);
   }
 
   public toString(): string {
@@ -245,99 +358,399 @@ export class DeclarationReference {
   }
 }
 
+// #region DeclarationReferenceParts
+
 /**
- * Indicates the symbol table from which to resolve the next symbol component.
  * @beta
  */
-export const enum Navigation {
-  Exports = '.',
-  Members = '#',
-  Locals = '~'
+export type DeclarationReferenceSourcePart<With extends boolean> = Parts<
+  With,
+  {
+    /** The module or global source for a symbol. */
+    source?: Part<With, SourceLike<With>>;
+  }
+>;
+
+/**
+ * @beta
+ */
+export type DeclarationReferenceSourceParts<With extends boolean> =
+  | DeclarationReferenceSourcePart<With>
+  | SourceParts<With>;
+
+/**
+ * @beta
+ */
+export type DeclarationReferenceNavigationParts<With extends boolean> = Parts<
+  With,
+  {
+    /** Indicates whether the symbol is exported or local to the source. */
+    navigation?: Part<With, SourceNavigation>;
+  }
+>;
+
+/**
+ * @beta
+ */
+export type DeclarationReferenceSymbolPart<With extends boolean> = Parts<
+  With,
+  {
+    /** The referenced symbol. */
+    symbol?: Part<With, SymbolReferenceLike<With>>;
+  }
+>;
+
+/**
+ * @beta
+ */
+export type DeclarationReferenceSymbolParts<With extends boolean> =
+  | DeclarationReferenceSymbolPart<With>
+  | SymbolReferenceParts<With>;
+
+/**
+ * @beta
+ */
+export type DeclarationReferenceParts<With extends boolean> = DeclarationReferenceSourceParts<With> &
+  DeclarationReferenceNavigationParts<With> &
+  DeclarationReferenceSymbolParts<With>;
+
+function resolveDeclarationReferenceSourcePart(
+  parts: DeclarationReferenceSourceParts</*With*/ true>,
+  fallbackSource: Source | undefined
+): SourceLike</*With*/ false> | undefined {
+  const { source, packageName, scopeName, unscopedPackageName, importPath } = parts as AllParts<typeof parts>;
+  if (source !== undefined) {
+    if (packageName !== undefined) {
+      throw new TypeError(`Cannot specify both 'source' and 'packageName'`);
+    } else if (scopeName !== undefined) {
+      throw new TypeError(`Cannot specify both 'source' and 'scopeName'`);
+    } else if (unscopedPackageName !== undefined) {
+      throw new TypeError(`Cannot specify both 'source' and 'unscopedPackageName'`);
+    } else if (importPath !== undefined) {
+      throw new TypeError(`Cannot specify both 'source' and 'importPath'`);
+    }
+    if (source === null) {
+      return undefined;
+    } else {
+      return resolveSourceLike(source, fallbackSource);
+    }
+  } else if (
+    packageName !== undefined ||
+    scopeName !== undefined ||
+    unscopedPackageName !== undefined ||
+    importPath !== undefined
+  ) {
+    return resolveModuleSourceParts(
+      parts as ModuleSourceParts</*With*/ true>,
+      tryCast(fallbackSource, ModuleSource)?.['_getOrParsePathComponents']()
+    );
+  } else {
+    return fallbackSource;
+  }
+}
+
+function resolveDeclarationReferenceNavigationPart(
+  parts: DeclarationReferenceNavigationParts</*With*/ true>,
+  fallbackNavigation: SourceNavigation | undefined
+): SourceNavigation | undefined {
+  const { navigation } = parts;
+  if (navigation !== undefined) {
+    if (navigation === null) {
+      return undefined;
+    } else {
+      return navigation;
+    }
+  } else {
+    return fallbackNavigation;
+  }
+}
+
+function resolveDeclarationReferenceSymbolPart(
+  parts: DeclarationReferenceSymbolParts</*With*/ true>,
+  fallbackSymbol: SymbolReference | undefined
+): SymbolReferenceLike</*With*/ false> | undefined {
+  const { symbol, componentPath, meaning, overloadIndex } = parts as AllParts<typeof parts>;
+  if (symbol !== undefined) {
+    if (componentPath !== undefined) {
+      throw new TypeError(`Cannot specify both 'symbol' and 'componentPath'`);
+    } else if (meaning !== undefined) {
+      throw new TypeError(`Cannot specify both 'symbol' and 'meaning'`);
+    } else if (overloadIndex !== undefined) {
+      throw new TypeError(`Cannot specify both 'symbol' and 'overloadIndex'`);
+    }
+    if (symbol === null) {
+      return undefined;
+    } else {
+      return resolveSymbolReferenceLike(symbol, fallbackSymbol);
+    }
+  } else if (componentPath !== undefined || meaning !== undefined || overloadIndex !== undefined) {
+    return resolveSymbolReferenceParts(
+      parts as SymbolReferenceParts</*With*/ true>,
+      fallbackSymbol?.componentPath,
+      fallbackSymbol?.meaning,
+      fallbackSymbol?.overloadIndex
+    );
+  } else {
+    return fallbackSymbol;
+  }
 }
 
+type ResolvedDeclarationReferenceParts = DeclarationReferenceSourcePart</*With*/ false> &
+  DeclarationReferenceNavigationParts</*With*/ false> &
+  DeclarationReferenceSymbolPart</*With*/ false>;
+
+function resolveDeclarationReferenceParts(
+  parts: DeclarationReferenceParts</*With*/ true>,
+  fallbackSource: Source | undefined,
+  fallbackNavigation: Navigation.Exports | Navigation.Locals | undefined,
+  fallbackSymbol: SymbolReference | undefined
+): ResolvedDeclarationReferenceParts {
+  return {
+    source: resolveDeclarationReferenceSourcePart(parts, fallbackSource),
+    navigation: resolveDeclarationReferenceNavigationPart(parts, fallbackNavigation),
+    symbol: resolveDeclarationReferenceSymbolPart(parts, fallbackSymbol)
+  };
+}
+
+// #endregion DeclarationReferenceParts
+
 /**
- * Represents a module.
  * @beta
  */
-export class ModuleSource {
-  public readonly escapedPath: string;
-  private _path: string | undefined;
+export type DeclarationReferenceLike<With extends boolean> =
+  | DeclarationReference
+  | DeclarationReferenceParts<With>
+  | string;
+
+type ResolvedDeclarationReferenceLike = DeclarationReference | ResolvedDeclarationReferenceParts;
+
+function resolveDeclarationReferenceLike(
+  reference: DeclarationReferenceLike</*With*/ true> | undefined,
+  fallbackReference: DeclarationReference | undefined
+): ResolvedDeclarationReferenceLike {
+  if (reference instanceof DeclarationReference) {
+    return reference;
+  } else if (reference === undefined) {
+    return DeclarationReference.empty();
+  } else if (typeof reference === 'string') {
+    return DeclarationReference.parse(reference);
+  } else {
+    return resolveDeclarationReferenceParts(
+      reference,
+      fallbackReference?.source,
+      fallbackReference?.navigation,
+      fallbackReference?.symbol
+    );
+  }
+}
+
+function resolveNavigation(
+  source: Source | undefined,
+  symbol: SymbolReference | undefined,
+  navigation: SourceNavigation | undefined
+): SourceNavigation | undefined {
+  if (!source || !symbol) {
+    return undefined;
+  } else if (source === GlobalSource.instance) {
+    return Navigation.Locals;
+  } else if (navigation === undefined) {
+    return Navigation.Exports;
+  } else {
+    return navigation;
+  }
+}
+
+// #endregion DeclarationReference
+
+// #region SourceBase
+
+/**
+ * Abstract base class for the source of a {@link DeclarationReference}.
+ * @beta
+ */
+export abstract class SourceBase {
+  public abstract readonly kind: string;
+
+  /**
+   * Combines this source with the provided parts to create a new {@link DeclarationReference}.
+   */
+  public toDeclarationReference(
+    this: Source,
+    parts?: DeclarationReferenceNavigationParts</*With*/ false> &
+      DeclarationReferenceSymbolParts</*With*/ false>
+  ): DeclarationReference {
+    return DeclarationReference.from({ ...parts, source: this });
+  }
+
+  public abstract toString(): string;
+}
+
+// #endregion SourceBase
+
+// #region GlobalSource
+
+/**
+ * Represents the global scope.
+ * @beta
+ */
+export class GlobalSource extends SourceBase {
+  /**
+   * A singleton instance of {@link GlobalSource}.
+   */
+  public static readonly instance: GlobalSource = new GlobalSource();
+
+  public readonly kind: 'global-source' = 'global-source';
+
+  private constructor() {
+    super();
+  }
+
+  public toString(): string {
+    return '!';
+  }
+}
 
+// #endregion GlobalSource
+
+// #region ModuleSource
+
+/**
+ * Represents a module source.
+ * @beta
+ */
+export class ModuleSource extends SourceBase {
+  public readonly kind: 'module-source' = 'module-source';
+
+  private _escapedPath: string;
+  private _path: string | undefined;
   private _pathComponents: IParsedPackage | undefined;
+  private _packageName: string | undefined;
 
   public constructor(path: string, userEscaped: boolean = true) {
-    this.escapedPath =
-      this instanceof ParsedModuleSource ? path : escapeModuleSourceIfNeeded(path, userEscaped);
+    super();
+    this._escapedPath = escapeModuleSourceIfNeeded(path, this instanceof ParsedModuleSource, userEscaped);
+  }
+
+  /** A canonically escaped module source string. */
+  public get escapedPath(): string {
+    return this._escapedPath;
   }
 
+  /**
+   * An unescaped module source string.
+   */
   public get path(): string {
-    return this._path || (this._path = DeclarationReference.unescapeModuleSourceString(this.escapedPath));
+    return this._path !== undefined
+      ? this._path
+      : (this._path = DeclarationReference.unescapeModuleSourceString(this.escapedPath));
   }
 
+  /**
+   * The full name of the module's package, such as `typescript` or `@microsoft/api-extractor`.
+   */
   public get packageName(): string {
-    return this._getOrParsePathComponents().packageName;
+    if (this._packageName === undefined) {
+      const parsed: IParsedPackage = this._getOrParsePathComponents();
+      this._packageName = formatPackageName(parsed.scopeName, parsed.unscopedPackageName);
+    }
+    return this._packageName;
   }
 
+  /**
+   * Returns the scope portion of a scoped package name (i.e., `@scope` in `@scope/package`).
+   */
   public get scopeName(): string {
-    const scopeName: string = this._getOrParsePathComponents().scopeName;
-    return scopeName ? '@' + scopeName : '';
+    return this._getOrParsePathComponents().scopeName ?? '';
   }
 
+  /**
+   * Returns the non-scope portion of a scoped package name (i.e., `package` in `@scope/package`)
+   */
   public get unscopedPackageName(): string {
     return this._getOrParsePathComponents().unscopedPackageName;
   }
 
+  /**
+   * Returns the package-relative import path of a module source (i.e., `path/to/file` in `packageName/path/to/file`).
+   */
   public get importPath(): string {
-    return this._getOrParsePathComponents().importPath || '';
+    return this._getOrParsePathComponents().importPath ?? '';
+  }
+
+  /**
+   * Creates a new {@link ModuleSource} from the supplied parts.
+   */
+  public static from(parts: ModuleSourceLike</*With*/ false> | string): ModuleSource {
+    const resolved: ResolvedModuleSourceLike = resolveModuleSourceLike(parts, /*fallbackSource*/ undefined);
+    if (resolved instanceof ModuleSource) {
+      return resolved;
+    } else {
+      const source: ModuleSource = new ModuleSource(
+        formatModuleSource(resolved.scopeName, resolved.unscopedPackageName, resolved.importPath)
+      );
+      source._pathComponents = resolved;
+      return source;
+    }
   }
 
+  /**
+   * Creates a new {@link ModuleSource} for a scoped package.
+   */
   public static fromScopedPackage(
     scopeName: string | undefined,
     unscopedPackageName: string,
     importPath?: string
   ): ModuleSource {
-    let packageName: string = unscopedPackageName;
-    if (scopeName) {
-      if (scopeName.charAt(0) === '@') {
-        scopeName = scopeName.slice(1);
-      }
-      packageName = `@${scopeName}/${unscopedPackageName}`;
-    }
-
-    const parsed: IParsedPackage = { packageName, scopeName: scopeName || '', unscopedPackageName };
-    return this._fromPackageName(parsed, packageName, importPath);
+    return ModuleSource.from({ scopeName, unscopedPackageName, importPath });
   }
 
+  /**
+   * Creates a new {@link ModuleSource} for package.
+   */
   public static fromPackage(packageName: string, importPath?: string): ModuleSource {
-    return this._fromPackageName(parsePackageName(packageName), packageName, importPath);
+    return ModuleSource.from({ packageName, importPath });
   }
 
-  private static _fromPackageName(
-    parsed: IParsedPackage | null,
-    packageName: string,
-    importPath?: string
-  ): ModuleSource {
-    if (!parsed) {
-      throw new Error('Parsed package must be provided.');
-    }
-
-    const packageNameError: string | undefined = StringChecks.explainIfInvalidPackageName(packageName);
-    if (packageNameError) {
-      throw new SyntaxError(`Invalid NPM package name: ${packageNameError}`);
+  /**
+   * Gets a {@link ModuleSource} updated with the provided parts.
+   * If a part is set to `undefined`, the current value is used.
+   * If a part is set to `null`, the part will be removed in the result.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parts.
+   */
+  public with(parts: ModuleSourceParts</*With*/ true>): ModuleSource {
+    const current: IParsedPackage = this._getOrParsePathComponents();
+    const parsed: IParsedPackage = resolveModuleSourceParts(parts, current);
+    if (
+      parsed.scopeName === current.scopeName &&
+      parsed.unscopedPackageName === current.unscopedPackageName &&
+      parsed.importPath === current.importPath
+    ) {
+      return this;
+    } else {
+      const source: ModuleSource = new ModuleSource(
+        formatModuleSource(parsed.scopeName, parsed.unscopedPackageName, parsed.importPath)
+      );
+      source._pathComponents = parsed;
+      return source;
     }
+  }
 
-    let path: string = packageName;
-    if (importPath) {
-      if (invalidImportPathRegExp.test(importPath)) {
-        throw new SyntaxError(`Invalid import path '${importPath}`);
-      }
-      path += '/' + importPath;
-      parsed.importPath = importPath;
+  /**
+   * Tests whether two {@link ModuleSource} values are equivalent.
+   */
+  public static equals(left: ModuleSource | undefined, right: ModuleSource | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else {
+      return left.packageName === right.packageName && left.importPath === right.importPath;
     }
+  }
 
-    const source: ModuleSource = new ModuleSource(path);
-    source._pathComponents = parsed;
-    return source;
+  /**
+   * Tests whether this object is equivalent to `other`.
+   */
+  public equals(other: ModuleSource): boolean {
+    return ModuleSource.equals(this, other);
   }
 
   public toString(): string {
@@ -347,13 +760,17 @@ export class ModuleSource {
   private _getOrParsePathComponents(): IParsedPackage {
     if (!this._pathComponents) {
       const path: string = this.path;
-      const parsed: IParsedPackage | null = parsePackageName(path);
-      if (parsed && !StringChecks.explainIfInvalidPackageName(parsed.packageName)) {
+      const parsed: IParsedPackage | null = tryParsePackageName(path);
+      if (
+        parsed &&
+        !StringChecks.explainIfInvalidPackageName(
+          formatPackageName(parsed.scopeName, parsed.unscopedPackageName)
+        )
+      ) {
         this._pathComponents = parsed;
       } else {
         this._pathComponents = {
-          packageName: '',
-          scopeName: '',
+          scopeName: undefined,
           unscopedPackageName: '',
           importPath: path
         };
@@ -363,23 +780,31 @@ export class ModuleSource {
   }
 }
 
-class ParsedModuleSource extends ModuleSource {}
+class ParsedModuleSource extends ModuleSource {
+  public constructor(text: string, userEscaped?: boolean) {
+    super(text, userEscaped);
+    try {
+      setPrototypeOf?.(this, ModuleSource.prototype);
+    } catch {
+      // ignored
+    }
+  }
+}
 
 // matches the following:
-//   'foo'            -> ["foo", "foo", undefined, "foo", undefined]
-//   'foo/bar'        -> ["foo/bar", "foo", undefined, "foo", "bar"]
-//   '@scope/foo'     -> ["@scope/foo", "@scope/foo", "scope", "foo", undefined]
-//   '@scope/foo/bar' -> ["@scope/foo/bar", "@scope/foo", "scope", "foo", "bar"]
+//   'foo'            -> ["foo", undefined, "foo", undefined]
+//   'foo/bar'        -> ["foo/bar", undefined, "foo", "bar"]
+//   '@scope/foo'     -> ["@scope/foo", "scope", "foo", undefined]
+//   '@scope/foo/bar' -> ["@scope/foo/bar", "scope", "foo", "bar"]
 // does not match:
 //   '/'
 //   '@/'
 //   '@scope/'
 // capture groups:
-//   1. The package name (including scope)
-//   2. The scope name (excluding the leading '@')
-//   3. The unscoped package name
-//   4. The package-relative import path
-const packageNameRegExp: RegExp = /^((?:@([^/]+?)\/)?([^/]+?))(?:\/(.+))?$/;
+//   1. The scope name (including the leading '@')
+//   2. The unscoped package name
+//   3. The package-relative import path
+const packageNameRegExp: RegExp = /^(?:(@[^/]+?)\/)?([^/]+?)(?:\/(.+))?$/;
 
 // no leading './' or '.\'
 // no leading '../' or '..\'
@@ -388,98 +813,1338 @@ const packageNameRegExp: RegExp = /^((?:@([^/]+?)\/)?([^/]+?))(?:\/(.+))?$/;
 const invalidImportPathRegExp: RegExp = /^(\.\.?([\\/]|$)|[\\/])/;
 
 interface IParsedPackage {
-  packageName: string;
-  scopeName: string;
+  scopeName: string | undefined;
   unscopedPackageName: string;
-  importPath?: string;
+  importPath: string | undefined;
 }
 
-// eslint-disable-next-line @rushstack/no-new-null
-function parsePackageName(text: string): IParsedPackage | null {
-  const match: RegExpExecArray | null = packageNameRegExp.exec(text);
-  if (!match) {
-    return match;
+function parsePackageName(text: string): IParsedPackage {
+  // eslint-disable-next-line @rushstack/no-new-null
+  const parsed: IParsedPackage | null = tryParsePackageName(text);
+  if (!parsed) {
+    throw new SyntaxError(`Invalid NPM package name: The package name ${JSON.stringify(text)} was invalid`);
   }
-  const [, packageName = '', scopeName = '', unscopedPackageName = '', importPath]: RegExpExecArray = match;
-  return { packageName, scopeName, unscopedPackageName, importPath };
-}
+
+  const packageNameError: string | undefined = StringChecks.explainIfInvalidPackageName(
+    formatPackageName(parsed.scopeName, parsed.unscopedPackageName)
+  );
+  if (packageNameError !== undefined) {
+    throw new SyntaxError(packageNameError);
+  }
+
+  if (parsed.importPath && invalidImportPathRegExp.test(parsed.importPath)) {
+    throw new SyntaxError(`Invalid import path ${JSON.stringify(parsed.importPath)}`);
+  }
+
+  return parsed;
+}
+
+// eslint-disable-next-line @rushstack/no-new-null
+function tryParsePackageName(text: string): IParsedPackage | null {
+  const match: RegExpExecArray | null = packageNameRegExp.exec(text);
+  if (!match) {
+    return match;
+  }
+  const [, scopeName, unscopedPackageName = '', importPath]: RegExpExecArray = match;
+  return { scopeName, unscopedPackageName, importPath };
+}
+
+function formatPackageName(scopeName: string | undefined, unscopedPackageName: string | undefined): string {
+  let packageName: string = '';
+  if (unscopedPackageName) {
+    packageName = unscopedPackageName;
+    if (scopeName) {
+      packageName = `${scopeName}/${packageName}`;
+    }
+  }
+  return packageName;
+}
+
+function parseModuleSource(text: string): IParsedPackage {
+  if (text.slice(-1) === '!') {
+    text = text.slice(0, -1);
+  }
+  return parsePackageName(text);
+}
+
+function formatModuleSource(
+  scopeName: string | undefined,
+  unscopedPackageName: string | undefined,
+  importPath: string | undefined
+): string {
+  let path: string = formatPackageName(scopeName, unscopedPackageName);
+  if (importPath) {
+    path += '/' + importPath;
+  }
+  return path;
+}
+
+/**
+ * Specifies the parts that can be used to construct or update a {@link ModuleSource}.
+ * @beta
+ */
+export type ModuleSourceParts<With extends boolean> = Parts<
+  With,
+  | {
+      /** The full name of the package. */
+      packageName: string;
+
+      /** A package relative import path. */
+      importPath?: Part<With, string>;
+    }
+  | {
+      /** The scope name for a scoped package. */
+      scopeName?: Part<With, string>;
+
+      /** The unscoped package name for a scoped package, or a package name that must not contain a scope. */
+      unscopedPackageName: string;
+
+      /** A package relative import path. */
+      importPath?: Part<With, string>;
+    }
+  | {
+      /** A package relative import path. */
+      importPath: string;
+    }
+>;
+
+function resolveModuleSourceParts(
+  parts: ModuleSourceParts</*With*/ true>,
+  fallback: IParsedPackage | undefined
+): IParsedPackage {
+  const { scopeName, unscopedPackageName, packageName, importPath } = parts as AllParts<typeof parts>;
+  if (scopeName !== undefined) {
+    // If we reach this branch, we're defining a scoped package
+
+    // verify parts aren't incompatible
+    if (packageName !== undefined) {
+      throw new TypeError("Cannot specify 'packageName' with 'scopeName', use 'unscopedPackageName' instead");
+    }
+
+    // validate `scopeName`
+    const newScopeName: string | undefined = scopeName ? ensureScopeName(scopeName) : undefined;
+    if (newScopeName !== undefined) {
+      const scopeNameError: string | undefined = StringChecks.explainIfInvalidPackageScope(newScopeName);
+      if (scopeNameError !== undefined) {
+        throw new SyntaxError(`Invalid NPM package name: ${scopeNameError}`);
+      }
+    }
+
+    const newUnscopedPackageName: string | undefined = unscopedPackageName ?? fallback?.unscopedPackageName;
+    if (newUnscopedPackageName === undefined) {
+      throw new TypeError(
+        "If either 'scopeName' or 'unscopedPackageName' are specified, both must be present"
+      );
+    }
+
+    const unscopedPackageNameError: string | undefined = StringChecks.explainIfInvalidUnscopedPackageName(
+      newUnscopedPackageName
+    );
+    if (unscopedPackageNameError !== undefined) {
+      throw new SyntaxError(`Invalid NPM package name: ${unscopedPackageNameError}`);
+    }
+
+    if (typeof importPath === 'string' && invalidImportPathRegExp.test(importPath)) {
+      throw new SyntaxError(`Invalid import path ${JSON.stringify(importPath)}`);
+    }
+
+    const newImportPath: string | undefined =
+      typeof importPath === 'string'
+        ? importPath
+        : importPath === undefined
+        ? fallback?.importPath
+        : undefined;
+
+    return {
+      scopeName: newScopeName,
+      unscopedPackageName: newUnscopedPackageName,
+      importPath: newImportPath
+    };
+  } else if (unscopedPackageName !== undefined) {
+    // If we reach this branch, we're either:
+    // - creating an unscoped package
+    // - updating the non-scoped part of a scoped package
+    // - updating the package name of a non-scoped package
+
+    // verify parts aren't incompatible
+    if (packageName !== undefined) {
+      throw new TypeError("Cannot specify both 'packageName' and 'unscopedPackageName'");
+    }
+
+    const unscopedPackageNameError: string | undefined = StringChecks.explainIfInvalidUnscopedPackageName(
+      unscopedPackageName
+    );
+    if (unscopedPackageNameError !== undefined) {
+      throw new SyntaxError(`Invalid NPM package name: ${unscopedPackageNameError}`);
+    }
+
+    if (typeof importPath === 'string' && invalidImportPathRegExp.test(importPath)) {
+      throw new SyntaxError(`Invalid import path ${JSON.stringify(importPath)}`);
+    }
+
+    const newScopeName: string | undefined = fallback?.scopeName;
+
+    const newImportPath: string | undefined =
+      typeof importPath === 'string'
+        ? importPath
+        : importPath === undefined
+        ? fallback?.importPath
+        : undefined;
+
+    return {
+      scopeName: newScopeName,
+      unscopedPackageName,
+      importPath: newImportPath
+    };
+  } else if (packageName !== undefined) {
+    // If we reach this branch, we're creating a possibly scoped or unscoped package
+
+    // parse and verify package
+    const parsed: IParsedPackage = parsePackageName(packageName);
+    if (importPath !== undefined) {
+      // verify parts aren't incompatible.
+      if (parsed.importPath !== undefined) {
+        throw new TypeError("Cannot specify 'importPath' if 'packageName' contains a path");
+      }
+      // validate `importPath`
+      if (typeof importPath === 'string' && invalidImportPathRegExp.test(importPath)) {
+        throw new SyntaxError(`Invalid import path ${JSON.stringify(importPath)}`);
+      }
+      parsed.importPath = importPath ?? undefined;
+    } else if (parsed.importPath === undefined) {
+      parsed.importPath = fallback?.importPath;
+    }
+    return parsed;
+  } else if (importPath !== undefined) {
+    // If we reach this branch, we're creating a path without a package scope
+
+    if (fallback?.unscopedPackageName) {
+      if (typeof importPath === 'string' && invalidImportPathRegExp.test(importPath)) {
+        throw new SyntaxError(`Invalid import path ${JSON.stringify(importPath)}`);
+      }
+    }
+
+    return {
+      scopeName: fallback?.scopeName,
+      unscopedPackageName: fallback?.unscopedPackageName ?? '',
+      importPath: importPath ?? undefined
+    };
+  } else if (fallback !== undefined) {
+    return fallback;
+  } else {
+    throw new TypeError(
+      "You must specify either 'packageName', 'importPath', or both 'scopeName' and 'unscopedPackageName'"
+    );
+  }
+}
+
+/**
+ * @beta
+ */
+export type ModuleSourceLike<With extends boolean> = ModuleSourceParts<With> | ModuleSource | string;
+
+type ResolvedModuleSourceLike = ModuleSource | IParsedPackage;
+
+function resolveModuleSourceLike(
+  source: ModuleSourceLike</*With*/ true>,
+  fallbackSource: ModuleSource | undefined
+): ResolvedModuleSourceLike {
+  if (source instanceof ModuleSource) {
+    return source;
+  } else if (typeof source === 'string') {
+    return parseModuleSource(source);
+  } else {
+    return resolveModuleSourceParts(source, fallbackSource);
+  }
+}
+
+// #endregion ModuleSource
+
+// #region Source
+
+/**
+ * A valid source in a {@link DeclarationReference}.
+ * @beta
+ */
+export type Source = GlobalSource | ModuleSource;
+
+/**
+ * @beta
+ */
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace Source {
+  /**
+   * Creates a {@link Source} from the provided parts.
+   */
+  export function from(parts: SourceLike</*With*/ false>): Source {
+    const resolved: ResolvedSourceLike = resolveSourceLike(parts, /*fallbackSource*/ undefined);
+    if (resolved instanceof GlobalSource || resolved instanceof ModuleSource) {
+      return resolved;
+    } else {
+      const source: ModuleSource = new ModuleSource(
+        formatModuleSource(resolved.scopeName, resolved.unscopedPackageName, resolved.importPath)
+      );
+      source['_pathComponents'] = resolved;
+      return source;
+    }
+  }
+
+  /**
+   * Tests whether two {@link Source} objects are equivalent.
+   */
+  export function equals(left: Source | undefined, right: Source | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else if (left instanceof GlobalSource) {
+      return right instanceof GlobalSource;
+    } else if (right instanceof GlobalSource) {
+      return left instanceof GlobalSource;
+    } else {
+      return ModuleSource.equals(left, right);
+    }
+  }
+}
+
+/**
+ * @beta
+ */
+export type SourceParts<With extends boolean> = ModuleSourceParts<With>;
+
+type ResolvedSourceParts = IParsedPackage;
+
+function resolveSourceParts(
+  parts: SourceParts</*With*/ true>,
+  fallbackSource: Source | undefined
+): ResolvedSourceParts {
+  return resolveModuleSourceParts(parts, tryCast(fallbackSource, ModuleSource));
+}
+
+/**
+ * @beta
+ */
+export type SourceLike<With extends boolean> = GlobalSource | ModuleSourceLike<With>;
+
+type ResolvedSourceLike = Source | ResolvedSourceParts;
+
+function resolveSourceLike(
+  source: SourceLike</*With*/ true>,
+  fallbackSource: Source | undefined
+): ResolvedSourceLike {
+  if (source instanceof ModuleSource || source instanceof GlobalSource) {
+    return source;
+  } else if (source === '!') {
+    return GlobalSource.instance;
+  } else if (typeof source === 'string') {
+    return parseModuleSource(source);
+  } else {
+    return resolveSourceParts(source, fallbackSource);
+  }
+}
+
+// #endregion Source
+
+// #region SymbolReference
+
+/**
+ * Represents a reference to a TypeScript symbol.
+ * @beta
+ */
+export class SymbolReference {
+  public readonly componentPath: ComponentPath | undefined;
+  public readonly meaning: Meaning | undefined;
+  public readonly overloadIndex: number | undefined;
+
+  public constructor(
+    component: ComponentPath | undefined,
+    { meaning, overloadIndex }: Pick<SymbolReferenceParts</*With*/ false>, 'meaning' | 'overloadIndex'> = {}
+  ) {
+    this.componentPath = component;
+    this.overloadIndex = overloadIndex;
+    this.meaning = meaning;
+  }
+
+  public get isEmpty(): boolean {
+    return this.componentPath === undefined && this.overloadIndex === undefined && this.meaning === undefined;
+  }
+
+  /**
+   * Creates an empty {@link SymbolReference}.
+   */
+  public static empty(): SymbolReference {
+    return new SymbolReference(/*component*/ undefined);
+  }
+
+  /**
+   * Parses a {@link SymbolReference} from the supplied text.
+   */
+  public static parse(text: string): SymbolReference {
+    const parser: Parser = new Parser(text);
+    const symbol: SymbolReference | undefined = parser.tryParseSymbolReference();
+    if (parser.errors.length) {
+      throw new SyntaxError(`Invalid SymbolReference '${text}':\n  ${parser.errors.join('\n  ')}`);
+    } else if (!parser.eof || symbol === undefined) {
+      throw new SyntaxError(`Invalid SymbolReference '${text}'`);
+    } else {
+      return symbol;
+    }
+  }
+
+  /**
+   * Creates a new {@link SymbolReference} from the provided parts.
+   */
+  public static from(parts: SymbolReferenceLike</*With*/ false> | undefined): SymbolReference {
+    const resolved: ResolvedSymbolReferenceLike = resolveSymbolReferenceLike(
+      parts,
+      /*fallbackSymbol*/ undefined
+    );
+    if (typeof resolved === 'string') {
+      return SymbolReference.parse(resolved);
+    } else if (resolved instanceof SymbolReference) {
+      return resolved;
+    } else {
+      const { componentPath, meaning, overloadIndex } = resolved;
+      return new SymbolReference(
+        componentPath === undefined ? undefined : ComponentPath.from(componentPath),
+        { meaning, overloadIndex }
+      );
+    }
+  }
+
+  /**
+   * Returns a {@link SymbolReference} updated with the provided parts.
+   * If a part is set to `undefined`, the current value is used.
+   * If a part is set to `null`, the part will be removed in the result.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parts.
+   */
+  public with(parts: SymbolReferenceParts</*With*/ true>): SymbolReference {
+    const { componentPath, meaning, overloadIndex } = resolveSymbolReferenceParts(
+      parts,
+      this.componentPath,
+      this.meaning,
+      this.overloadIndex
+    );
+    const resolvedComponentPath: ComponentPath | undefined =
+      componentPath === undefined ? undefined : ComponentPath.from(componentPath);
+    if (
+      ComponentPath.equals(this.componentPath, resolvedComponentPath) &&
+      this.meaning === meaning &&
+      this.overloadIndex === overloadIndex
+    ) {
+      return this;
+    } else {
+      return new SymbolReference(resolvedComponentPath, { meaning, overloadIndex });
+    }
+  }
+
+  /**
+   * Gets a {@link SymbolReference} updated with the provided component path.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided component path.
+   */
+  public withComponentPath(componentPath: ComponentPath | undefined): SymbolReference {
+    return this.with({ componentPath: componentPath ?? null });
+  }
+
+  /**
+   * Gets a {@link SymbolReference} updated with the provided meaning.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided meaning.
+   */
+  public withMeaning(meaning: Meaning | undefined): SymbolReference {
+    return this.with({ meaning: meaning ?? null });
+  }
+
+  /**
+   * Gets a {@link SymbolReference} updated with the provided overload index.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided overload index.
+   */
+  public withOverloadIndex(overloadIndex: number | undefined): SymbolReference {
+    return this.with({ overloadIndex: overloadIndex ?? null });
+  }
+
+  public withSource(source: Source | undefined): DeclarationReference {
+    return this.toDeclarationReference({ source });
+  }
+
+  /**
+   * Creates a new {@link SymbolReference} that navigates from this SymbolReference to the provided component.
+   */
+  public addNavigationStep(
+    navigation: Navigation,
+    component: ComponentLike</*With*/ false>
+  ): SymbolReference {
+    if (!this.componentPath) {
+      throw new Error('Cannot add a navigation step to an empty symbol reference.');
+    }
+    return new SymbolReference(this.componentPath.addNavigationStep(navigation, component));
+  }
+
+  /**
+   * Tests whether two {@link SymbolReference} values are equivalent.
+   */
+  public static equals(left: SymbolReference | undefined, right: SymbolReference | undefined): boolean {
+    if (left === undefined) {
+      return right === undefined || right.isEmpty;
+    } else if (right === undefined) {
+      return left === undefined || left.isEmpty;
+    } else {
+      return (
+        ComponentPath.equals(left.componentPath, right.componentPath) &&
+        left.meaning === right.meaning &&
+        left.overloadIndex === right.overloadIndex
+      );
+    }
+  }
+
+  /**
+   * Tests whether this object is equivalent to `other`.
+   */
+  public equals(other: SymbolReference): boolean {
+    return SymbolReference.equals(this, other);
+  }
+
+  public toDeclarationReference(
+    parts?: DeclarationReferenceSourceParts</*With*/ false> &
+      DeclarationReferenceNavigationParts</*With*/ false>
+  ): DeclarationReference {
+    return DeclarationReference.from({ ...parts, symbol: this });
+  }
+
+  public toString(): string {
+    let result: string = `${this.componentPath || ''}`;
+    if (this.meaning && this.overloadIndex !== undefined) {
+      result += `:${this.meaning}(${this.overloadIndex})`;
+    } else if (this.meaning) {
+      result += `:${this.meaning}`;
+    } else if (this.overloadIndex !== undefined) {
+      result += `:${this.overloadIndex}`;
+    }
+    return result;
+  }
+}
+
+/**
+ * @beta
+ */
+export type SymbolReferenceParts<With extends boolean> = Parts<
+  With,
+  {
+    /** The component path for the symbol */
+    componentPath?: Part<With, ComponentPathLike<With>>;
+
+    /** The meaning of the symbol */
+    meaning?: Part<With, Meaning>;
+
+    /** The overload index of the symbol */
+    overloadIndex?: Part<With, number>;
+  }
+>;
+
+function resolveSymbolReferenceParts(
+  parts: SymbolReferenceParts</*With*/ true>,
+  fallbackComponentPath: ComponentPath | undefined,
+  fallbackMeaning: Meaning | undefined,
+  fallbackOverloadIndex: number | undefined
+): SymbolReferenceParts</*With*/ false> {
+  const { componentPath, meaning = fallbackMeaning, overloadIndex = fallbackOverloadIndex } = parts;
+  return {
+    componentPath:
+      componentPath === null
+        ? undefined
+        : componentPath === undefined
+        ? fallbackComponentPath
+        : resolveComponentPathLike(componentPath, fallbackComponentPath),
+    meaning: meaning ?? undefined,
+    overloadIndex: overloadIndex ?? undefined
+  };
+}
+
+/**
+ * @beta
+ */
+export type SymbolReferenceLike<With extends boolean> = string | SymbolReference | SymbolReferenceParts<With>;
+
+type ResolvedSymbolReferenceLike = string | SymbolReference | SymbolReferenceParts</*With*/ false>;
+
+function resolveSymbolReferenceLike(
+  symbol: SymbolReferenceLike</*With*/ true> | undefined,
+  fallbackSymbol: SymbolReference | undefined
+): ResolvedSymbolReferenceLike {
+  if (symbol instanceof SymbolReference || typeof symbol === 'string') {
+    return symbol;
+  } else if (symbol === undefined) {
+    return SymbolReference.empty();
+  } else {
+    return resolveSymbolReferenceParts(
+      symbol,
+      fallbackSymbol?.componentPath,
+      fallbackSymbol?.meaning,
+      fallbackSymbol?.overloadIndex
+    );
+  }
+}
+
+// #endregion SymbolReference
+
+// #region ComponentPathBase
+
+/**
+ * Abstract base class for a part of {@link ComponentPath}.
+ * @beta
+ */
+export abstract class ComponentPathBase {
+  public abstract readonly kind: string;
+  public readonly component: Component;
+
+  private declare _: never; // NOTE: This makes a ComponentPath compare nominally rather than structurally
+  //       which removes its properties from completions in `ComponentPath.from({ ... })`
+
+  public constructor(component: Component) {
+    this.component = component;
+  }
+
+  /**
+   * Gets the {@link ComponentRoot} at the root of the component path.
+   */
+  public abstract get root(): ComponentRoot;
+
+  /**
+   * Creates a new {@link ComponentNavigation} step that navigates from this {@link ComponentPath} to the provided component.
+   */
+  public addNavigationStep(
+    this: ComponentPath,
+    navigation: Navigation,
+    component: ComponentLike</*With*/ false>
+  ): ComponentNavigation {
+    // tslint:disable-next-line:no-use-before-declare
+    return new ComponentNavigation(this, navigation, Component.from(component));
+  }
+
+  /**
+   * Combines this {@link ComponentPath} with a {@link Meaning} to create a new {@link SymbolReference}.
+   */
+  public withMeaning(this: ComponentPath, meaning: Meaning | undefined): SymbolReference {
+    return this.toSymbolReference({ meaning });
+  }
+
+  /**
+   * Combines this {@link ComponentPath} with an overload index to create a new {@link SymbolReference}.
+   */
+  public withOverloadIndex(this: ComponentPath, overloadIndex: number | undefined): SymbolReference {
+    return this.toSymbolReference({ overloadIndex });
+  }
+
+  /**
+   * Combines this {@link ComponentPath} with a {@link Source} to create a new {@link DeclarationReference}.
+   */
+  public withSource(this: ComponentPath, source: Source | undefined): DeclarationReference {
+    return this.toDeclarationReference({ source });
+  }
+
+  /**
+   * Combines this {@link ComponentPath} with the provided parts to create a new {@link SymbolReference}.
+   */
+  public toSymbolReference(
+    this: ComponentPath,
+    parts?: Omit<SymbolReferenceParts</*With*/ false>, 'componentPath' | 'component'>
+  ): SymbolReference {
+    return SymbolReference.from({ ...parts, componentPath: this });
+  }
+
+  /**
+   * Combines this {@link ComponentPath} with the provided parts to create a new {@link DeclarationReference}.
+   */
+  public toDeclarationReference(
+    this: ComponentPath,
+    parts?: DeclarationReferenceSourceParts</*With*/ false> &
+      DeclarationReferenceNavigationParts</*With*/ false> &
+      Omit<SymbolReferenceParts</*With*/ false>, 'componentPath' | 'component'>
+  ): DeclarationReference {
+    return DeclarationReference.from({ ...parts, componentPath: this });
+  }
+
+  /**
+   * Starting with this path segment, yields each parent path segment.
+   */
+  public *ancestors(this: ComponentPath, includeSelf?: boolean): IterableIterator<ComponentPath> {
+    let ancestor: ComponentPath | undefined = this;
+    while (ancestor) {
+      if (!includeSelf) {
+        includeSelf = true;
+      } else {
+        yield ancestor;
+      }
+      ancestor = ancestor instanceof ComponentNavigation ? ancestor.parent : undefined;
+    }
+  }
+
+  public abstract toString(): string;
+}
+
+// #endregion ComponentPathBase
+
+// #region ComponentRoot
+
+/**
+ * Represents the root of a {@link ComponentPath}.
+ * @beta
+ */
+export class ComponentRoot extends ComponentPathBase {
+  public readonly kind: 'component-root' = 'component-root';
+
+  /**
+   * Gets the {@link ComponentRoot} at the root of the component path.
+   */
+  public get root(): ComponentRoot {
+    return this;
+  }
+
+  /**
+   * Creates a new {@link ComponentRoot} from the provided parts.
+   */
+  public static from(parts: ComponentRootLike</*With*/ false>): ComponentRoot {
+    const resolved: ResolvedComponentRootLike = resolveComponentRootLike(
+      parts,
+      /*fallbackComponent*/ undefined
+    );
+    if (resolved instanceof ComponentRoot) {
+      return resolved;
+    } else {
+      const { component } = resolved;
+      return new ComponentRoot(Component.from(component));
+    }
+  }
+
+  /**
+   * Returns a {@link ComponentRoot} updated with the provided parts.
+   * If a part is set to `undefined`, the current value is used.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parts.
+   */
+  public with(parts: ComponentRootParts</*With*/ true>): ComponentRoot {
+    const { component } = resolveComponentRootParts(parts, this.component);
+    const resolvedComponent: Component = Component.from(component);
+    if (Component.equals(this.component, resolvedComponent)) {
+      return this;
+    } else {
+      return new ComponentRoot(resolvedComponent);
+    }
+  }
+
+  /**
+   * Tests whether two {@link ComponentRoot} values are equivalent.
+   */
+  public static equals(left: ComponentRoot | undefined, right: ComponentRoot | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else {
+      return Component.equals(left.component, right.component);
+    }
+  }
+
+  /**
+   * Tests whether this object is equivalent to `other`.
+   */
+  public equals(other: ComponentRoot): boolean {
+    return ComponentRoot.equals(this, other);
+  }
+
+  /**
+   * Returns a {@link ComponentRoot} updated with the provided component.
+   * If a part is set to `undefined`, the current value is used.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided component.
+   */
+  public withComponent(component: ComponentLike</*With*/ false>): ComponentRoot {
+    return this.with({ component });
+  }
+
+  public toString(): string {
+    return this.component.toString();
+  }
+}
+
+/**
+ * @beta
+ */
+export type ComponentRootParts<With extends boolean> = Parts<
+  With,
+  {
+    /** The component for the {@link ComponentRoot} */
+    component: ComponentLike<With>;
+  }
+>;
+
+function resolveComponentRootParts(
+  parts: ComponentRootParts</*With*/ true>,
+  fallbackComponent: Component | undefined
+): ComponentRootParts</*With*/ false> {
+  const { component = fallbackComponent } = parts;
+  if (component === undefined) {
+    throw new TypeError("The property 'component' is required.");
+  }
+  return {
+    component: resolveComponentLike(component, fallbackComponent)
+  };
+}
+
+/**
+ * @beta
+ */
+export type ComponentRootLike<With extends boolean> =
+  | ComponentRoot
+  | ComponentRootParts<With>
+  | ComponentLike<With>;
+
+type ResolvedComponentRootLike = ComponentRoot | ComponentRootParts</*With*/ false>;
+
+function resolveComponentRootLike(
+  componentRoot: ComponentRootLike</*With*/ true>,
+  fallbackComponent: Component | undefined
+): ResolvedComponentRootLike {
+  if (componentRoot instanceof ComponentRoot) {
+    return componentRoot;
+  } else if (
+    componentRoot instanceof ComponentString ||
+    componentRoot instanceof ComponentReference ||
+    componentRoot instanceof DeclarationReference ||
+    typeof componentRoot === 'string'
+  ) {
+    return resolveComponentRootParts({ component: componentRoot }, fallbackComponent);
+  }
+  const { component, text, reference } = componentRoot as AllParts<typeof componentRoot>;
+  if (component !== undefined) {
+    if (text !== undefined) {
+      throw new TypeError(`Cannot specify both 'component' and 'text'`);
+    } else if (reference !== undefined) {
+      throw new TypeError(`Cannot specify both 'component' and 'reference'`);
+    }
+    return resolveComponentRootParts({ component }, fallbackComponent);
+  } else if (text !== undefined || reference !== undefined) {
+    return resolveComponentRootParts({ component: { text, reference } }, fallbackComponent);
+  } else {
+    return resolveComponentRootParts({}, fallbackComponent);
+  }
+}
+
+// #endregion ComponentRoot
+
+// #region ComponentNavigation
+
+/**
+ * Represents a navigation step in a {@link ComponentPath}.
+ * @beta
+ */
+export class ComponentNavigation extends ComponentPathBase {
+  public readonly kind: 'component-navigation' = 'component-navigation';
+  public readonly parent: ComponentPath;
+  public readonly navigation: Navigation;
+
+  public constructor(parent: ComponentPath, navigation: Navigation, component: Component) {
+    super(component);
+    this.parent = parent;
+    this.navigation = navigation;
+  }
+
+  /**
+   * Gets the {@link ComponentRoot} at the root of the component path.
+   */
+  public get root(): ComponentRoot {
+    let parent: ComponentPath = this.parent;
+    while (!(parent instanceof ComponentRoot)) {
+      parent = parent.parent;
+    }
+    return parent;
+  }
+
+  /**
+   * Creates a new {@link ComponentNavigation} from the provided parts.
+   */
+  public static from(parts: ComponentNavigationLike</*With*/ false>): ComponentNavigation {
+    const resolved: ResolvedComponentNavigationLike = resolveComponentNavigationLike(
+      parts,
+      /*fallbackParent*/ undefined,
+      /*fallbackNavigation*/ undefined,
+      /*fallbackComponent*/ undefined
+    );
+    if (resolved instanceof ComponentNavigation) {
+      return resolved;
+    } else {
+      const { parent, navigation, component } = resolved;
+      return new ComponentNavigation(ComponentPath.from(parent), navigation, Component.from(component));
+    }
+  }
+
+  /**
+   * Returns a {@link ComponentNavigation} updated with the provided parts.
+   * If a part is set to `undefined`, the current value is used.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parts.
+   */
+  public with(parts: ComponentNavigationParts</*With*/ true>): ComponentNavigation {
+    const { parent, navigation, component } = resolveComponentNavigationParts(
+      parts,
+      this.parent,
+      this.navigation,
+      this.component
+    );
+    const resolvedParent: ComponentPath = ComponentPath.from(parent);
+    const resolvedComponent: Component = Component.from(component);
+    if (
+      ComponentPath.equals(this.parent, resolvedParent) &&
+      this.navigation === navigation &&
+      Component.equals(this.component, resolvedComponent)
+    ) {
+      return this;
+    } else {
+      return new ComponentNavigation(resolvedParent, navigation, resolvedComponent);
+    }
+  }
+
+  /**
+   * Returns a {@link ComponentNavigation} updated with the provided parent.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parent.
+   */
+  public withParent(parent: ComponentPath): ComponentNavigation {
+    return this.with({ parent });
+  }
+
+  /**
+   * Returns a {@link ComponentNavigation} updated with the provided navigation.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided navigation.
+   */
+  public withNavigation(navigation: Navigation): ComponentNavigation {
+    return this.with({ navigation });
+  }
+
+  /**
+   * Returns a {@link ComponentNavigation} updated with the provided component.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided component.
+   */
+  public withComponent(component: ComponentLike</*With*/ false>): ComponentNavigation {
+    return this.with({ component });
+  }
+
+  /**
+   * Tests whether two {@link ComponentNavigation} values are equivalent.
+   */
+  public static equals(
+    left: ComponentNavigation | undefined,
+    right: ComponentNavigation | undefined
+  ): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else {
+      return (
+        ComponentPath.equals(left.parent, right.parent) &&
+        left.navigation === right.navigation &&
+        Component.equals(left.component, right.component)
+      );
+    }
+  }
+
+  /**
+   * Tests whether this object is equivalent to `other`.
+   */
+  public equals(other: ComponentNavigation): boolean {
+    return ComponentNavigation.equals(this, other);
+  }
+
+  public toString(): string {
+    return `${this.parent}${formatNavigation(this.navigation)}${this.component}`;
+  }
+}
+
+/**
+ * @beta
+ */
+export type ComponentNavigationParts<With extends boolean> = Parts<
+  With,
+  {
+    /** The parent {@link ComponentPath} segment for this navigation step. */
+    parent: ComponentPathLike<With>;
+
+    /** The kind of navigation for this navigation step. */
+    navigation: Navigation;
+
+    /** The component for this navigation step. */
+    component: ComponentLike<With>;
+  }
+>;
+
+function resolveComponentNavigationParts(
+  parts: ComponentNavigationParts</*With*/ true>,
+  fallbackParent: ComponentPath | undefined,
+  fallbackNavigation: Navigation | undefined,
+  fallbackComponent: Component | undefined
+): ComponentNavigationParts</*With*/ false> {
+  const {
+    parent = fallbackParent,
+    navigation = fallbackNavigation,
+    component = fallbackComponent
+  } = parts as AllParts<typeof parts>;
+  if (parent === undefined) {
+    throw new TypeError("The 'parent' property is required");
+  }
+  if (navigation === undefined) {
+    throw new TypeError("The 'navigation' property is required");
+  }
+  if (component === undefined) {
+    throw new TypeError("The 'component' property is required");
+  }
+  return {
+    parent: resolveComponentPathLike(parent, fallbackParent),
+    navigation,
+    component: resolveComponentLike(component, fallbackComponent)
+  };
+}
 
 /**
- * Represents the global scope.
  * @beta
  */
-export class GlobalSource {
-  public static readonly instance: GlobalSource = new GlobalSource();
-
-  private constructor() {}
-
-  public toString(): string {
-    return '!';
+export type ComponentNavigationLike<With extends boolean> =
+  | ComponentNavigation
+  | ComponentNavigationParts<With>;
+
+type ResolvedComponentNavigationLike = ComponentNavigation | ComponentNavigationParts</*With*/ false>;
+
+function resolveComponentNavigationLike(
+  value: ComponentNavigationLike</*With*/ true>,
+  fallbackParent: ComponentPath | undefined,
+  fallbackNavigation: Navigation | undefined,
+  fallbackComponent: Component | undefined
+): ResolvedComponentNavigationLike {
+  if (value instanceof ComponentNavigation) {
+    return value;
+  } else {
+    return resolveComponentNavigationParts(value, fallbackParent, fallbackNavigation, fallbackComponent);
   }
 }
 
+// #endregion ComponentNavigation
+
+// #region ComponentPath
+
 /**
+ * The path used to traverse a root symbol to a specific declaration.
  * @beta
  */
-export type Component = ComponentString | ComponentReference;
+export type ComponentPath = ComponentRoot | ComponentNavigation;
 
 /**
  * @beta
  */
 // eslint-disable-next-line @typescript-eslint/no-namespace
-export namespace Component {
-  export function from(value: ComponentLike): Component {
-    if (typeof value === 'string') {
-      return new ComponentString(value);
+export namespace ComponentPath {
+  /**
+   * Parses a {@link SymbolReference} from the supplied text.
+   */
+  export function parse(text: string): ComponentPath {
+    const parser: Parser = new Parser(text);
+    const componentPath: ComponentPath = parser.parseComponentPath();
+    if (parser.errors.length) {
+      throw new SyntaxError(`Invalid ComponentPath '${text}':\n  ${parser.errors.join('\n  ')}`);
+    } else if (!parser.eof || componentPath === undefined) {
+      throw new SyntaxError(`Invalid ComponentPath '${text}'`);
+    } else {
+      return componentPath;
+    }
+  }
+
+  /**
+   * Creates a new {@link ComponentPath} from the provided parts.
+   */
+  export function from(parts: ComponentPathLike</*With*/ false>): ComponentPath {
+    const resolved: ResolvedComponentPathLike = resolveComponentPathLike(
+      parts,
+      /*fallbackComponentPath*/ undefined
+    );
+    if (resolved instanceof ComponentRoot || resolved instanceof ComponentNavigation) {
+      return resolved;
+    } else if (typeof resolved === 'string') {
+      return parse(resolved);
+    } else if ('navigation' in resolved) {
+      return ComponentNavigation.from(resolved);
+    } else {
+      return ComponentRoot.from(resolved);
     }
-    if (value instanceof DeclarationReference) {
-      return new ComponentReference(value);
+  }
+
+  /**
+   * Tests whether two {@link ComponentPath} values are equivalent.
+   */
+  export function equals(left: ComponentPath | undefined, right: ComponentPath | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else if (left instanceof ComponentRoot) {
+      return right instanceof ComponentRoot && ComponentRoot.equals(left, right);
+    } else {
+      return right instanceof ComponentNavigation && ComponentNavigation.equals(left, right);
     }
+  }
+}
+
+/**
+ * @beta
+ */
+export type ComponentPathParts<With extends boolean> =
+  | ComponentRootParts<With>
+  | ComponentNavigationParts<With>;
+
+function resolveComponentPathParts(
+  parts: ComponentPathParts</*With*/ true>,
+  fallbackComponentPath: ComponentPath | undefined
+): ComponentPathParts</*With*/ false> {
+  const { component, navigation, parent } = parts as AllParts<typeof parts>;
+  if (navigation !== undefined || parent !== undefined) {
+    const fallbackComponent: ComponentNavigation | undefined = tryCast(
+      fallbackComponentPath,
+      ComponentNavigation
+    );
+    return resolveComponentNavigationParts(
+      { component, navigation, parent },
+      fallbackComponent?.parent,
+      fallbackComponent?.navigation,
+      fallbackComponent?.component
+    );
+  } else {
+    const fallbackComponent: ComponentRoot | undefined = tryCast(fallbackComponentPath, ComponentRoot);
+    return resolveComponentRootParts({ component }, fallbackComponent?.component);
+  }
+}
+
+/**
+ * @beta
+ */
+export type ComponentPathLike<With extends boolean> =
+  | Exclude<ComponentRootLike<With>, string>
+  | ComponentNavigationLike<With>
+  | string;
+
+type ResolvedComponentPathLike =
+  | ComponentPath
+  | ComponentRootParts</*With*/ false>
+  | ComponentNavigationParts</*With*/ false>
+  | string;
+
+function resolveComponentPathLike(
+  value: ComponentPathLike</*With*/ true>,
+  fallbackComponentPath: ComponentPath | undefined
+): ResolvedComponentPathLike {
+  if (value instanceof ComponentRoot || value instanceof ComponentNavigation) {
+    return value;
+  } else if (value instanceof ComponentString || value instanceof ComponentReference) {
+    return resolveComponentPathParts({ component: value }, fallbackComponentPath);
+  } else if (value instanceof DeclarationReference) {
+    return resolveComponentPathParts({ component: { reference: value } }, fallbackComponentPath);
+  } else if (typeof value === 'string') {
     return value;
   }
+  const { component, navigation, parent, text, reference } = value as AllParts<typeof value>;
+  if (component !== undefined || navigation !== undefined || parent !== undefined) {
+    if (text !== undefined || reference !== undefined) {
+      const first: string =
+        component !== undefined ? 'component' : navigation !== undefined ? 'navigation' : 'parent';
+      if (text !== undefined) {
+        throw new TypeError(`Cannot specify both '${first}' and 'text'`);
+      } else {
+        throw new TypeError(`Cannot specify both '${first}' and 'reference'`);
+      }
+    }
+    return resolveComponentPathParts({ component, navigation, parent }, fallbackComponentPath);
+  } else if (text !== undefined || reference !== undefined) {
+    return resolveComponentPathParts({ component: { text, reference } }, fallbackComponentPath);
+  } else {
+    return resolveComponentPathParts({}, fallbackComponentPath);
+  }
 }
 
+// #endregion ComponentPath
+
+// #region ComponentBase
+
 /**
+ * Abstract base class for a {@link Component}.
  * @beta
  */
-export type ComponentLike = Component | DeclarationReference | string;
+export abstract class ComponentBase {
+  public abstract readonly kind: string;
+
+  private declare _: never; // NOTE: This makes a Component compare nominally rather than structurally
+  //       which removes its properties from completions in `Component.from({ ... })`
+
+  /**
+   * Combines this component with the provided parts to create a new {@link Component}.
+   * @param parts - The parts for the component path segment. If `undefined` or an empty object, then the
+   * result is a {@link ComponentRoot}. Otherwise, the result is a {@link ComponentNavigation}.
+   */
+  public toComponentPath(
+    this: Component,
+    parts?: Omit<ComponentNavigationParts</*With*/ false>, 'component'>
+  ): ComponentPath {
+    return ComponentPath.from({ ...parts, component: this });
+  }
+
+  public abstract toString(): string;
+}
+
+// #endregion ComponentBase
+
+// #region ComponentString
 
 /**
+ * A {@link Component} in a component path that refers to a property name.
  * @beta
  */
-export class ComponentString {
+export class ComponentString extends ComponentBase {
+  public readonly kind: 'component-string' = 'component-string';
   public readonly text: string;
 
   public constructor(text: string, userEscaped?: boolean) {
+    super();
     this.text = this instanceof ParsedComponentString ? text : escapeComponentIfNeeded(text, userEscaped);
   }
 
+  /**
+   * Creates a new {@link ComponentString} from the provided parts.
+   */
+  public static from(parts: ComponentStringLike): ComponentString {
+    if (parts instanceof ComponentString) {
+      return parts;
+    } else if (typeof parts === 'string') {
+      return new ComponentString(parts);
+    } else {
+      return new ComponentString(parts.text);
+    }
+  }
+
+  /**
+   * Tests whether two {@link ComponentString} values are equivalent.
+   */
+  public static equals(left: ComponentString | undefined, right: ComponentString | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else {
+      return left.text === right.text;
+    }
+  }
+
+  /**
+   * Tests whether this component is equivalent to `other`.
+   */
+  public equals(other: ComponentString): boolean {
+    return ComponentString.equals(this, other);
+  }
+
   public toString(): string {
     return this.text;
   }
 }
 
-class ParsedComponentString extends ComponentString {}
+class ParsedComponentString extends ComponentString {
+  public constructor(text: string, userEscaped?: boolean) {
+    super(text, userEscaped);
+    try {
+      setPrototypeOf?.(this, ComponentString.prototype);
+    } catch {
+      // ignored
+    }
+  }
+}
+
+/**
+ * @beta
+ */
+export type ComponentStringParts = Parts<
+  /*With*/ false,
+  {
+    /** The text for a {@link ComponentString}. */
+    text: string;
+  }
+>;
+
+/**
+ * @beta
+ */
+export type ComponentStringLike = ComponentStringParts | ComponentString | string;
+
+// #endregion ComponentString
+
+// #region ComponentReference
 
 /**
+ * A {@link Component} in a component path that refers to a unique symbol declared on another declaration, such as `Symbol.iterator`.
  * @beta
  */
-export class ComponentReference {
+export class ComponentReference extends ComponentBase {
+  public readonly kind: 'component-reference' = 'component-reference';
   public readonly reference: DeclarationReference;
 
   public constructor(reference: DeclarationReference) {
+    super();
     this.reference = reference;
   }
 
+  /**
+   * Parses a string into a standalone {@link ComponentReference}.
+   */
   public static parse(text: string): ComponentReference {
-    if (text.length > 2 && text.charAt(0) === '[' && text.charAt(text.length - 1) === ']') {
+    if (isBracketed(text)) {
       return new ComponentReference(DeclarationReference.parse(text.slice(1, -1)));
     }
     throw new SyntaxError(`Invalid component reference: '${text}'`);
   }
 
+  /**
+   * Creates a new {@link ComponentReference} from the provided parts.
+   */
+  public static from(parts: ComponentReferenceLike</*With*/ false>): ComponentReference {
+    if (parts instanceof ComponentReference) {
+      return parts;
+    } else if (typeof parts === 'string') {
+      return ComponentReference.parse(parts);
+    } else if (parts instanceof DeclarationReference) {
+      return new ComponentReference(parts);
+    } else {
+      const { reference } = resolveComponentReferenceParts(parts, /*fallbackReference*/ undefined);
+      return new ComponentReference(DeclarationReference.from(reference));
+    }
+  }
+
+  /**
+   * Returns a {@link ComponentReference} updated with the provided parts.
+   * If a part is set to `undefined`, the current value is used.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided parts.
+   */
+  public with(parts: ComponentReferenceParts</*With*/ true>): ComponentReference {
+    const { reference } = resolveComponentReferenceParts(parts, this.reference);
+    const resolvedReference: DeclarationReference = DeclarationReference.from(reference);
+    if (DeclarationReference.equals(this.reference, resolvedReference)) {
+      return this;
+    } else {
+      return new ComponentReference(resolvedReference);
+    }
+  }
+
+  /**
+   * Returns a {@link ComponentReference} updated with the provided reference.
+   * @returns This object if there were no changes; otherwise, a new object updated with the provided reference.
+   */
   public withReference(reference: DeclarationReference): ComponentReference {
-    return this.reference === reference ? this : new ComponentReference(reference);
+    return this.with({ reference });
+  }
+
+  /**
+   * Tests whether two {@link ComponentReference} values are equivalent.
+   */
+  public static equals(left: ComponentReference | undefined, right: ComponentReference | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else {
+      return DeclarationReference.equals(left.reference, right.reference);
+    }
+  }
+
+  /**
+   * Tests whether this component is equivalent to `other`.
+   */
+  public equals(other: ComponentReference): boolean {
+    return ComponentReference.equals(this, other);
   }
 
   public toString(): string {
@@ -490,77 +2155,167 @@ export class ComponentReference {
 /**
  * @beta
  */
-export type ComponentPath = ComponentRoot | ComponentNavigation;
+export type ComponentReferenceParts<With extends boolean> = Parts<
+  With,
+  {
+    /** The reference for a {@link ComponentReference}. */
+    reference: DeclarationReferenceLike<With>;
+  }
+>;
+
+function resolveComponentReferenceParts(
+  parts: ComponentReferenceParts</*With*/ true>,
+  fallbackReference: DeclarationReference | undefined
+): ComponentReferenceParts</*With*/ false> {
+  const { reference = fallbackReference } = parts;
+  if (reference === undefined) {
+    throw new TypeError("The property 'reference' is required");
+  }
+  return {
+    reference: resolveDeclarationReferenceLike(reference, fallbackReference)
+  };
+}
 
 /**
  * @beta
  */
-export abstract class ComponentPathBase {
-  public readonly component: Component;
+export type ComponentReferenceLike<With extends boolean> =
+  | ComponentReference
+  | ComponentReferenceParts<With>
+  | DeclarationReference
+  | string;
 
-  public constructor(component: Component) {
-    this.component = component;
-  }
+// #endregion ComponentReference
 
-  public addNavigationStep(
-    this: ComponentPath,
-    navigation: Navigation,
-    component: ComponentLike
-  ): ComponentPath {
-    // tslint:disable-next-line:no-use-before-declare
-    return new ComponentNavigation(this, navigation, Component.from(component));
-  }
+// #region Component
 
-  public abstract toString(): string;
-}
+/**
+ * A component in a {@link ComponentPath}.
+ * @beta
+ */
+export type Component = ComponentString | ComponentReference;
 
 /**
  * @beta
  */
-export class ComponentRoot extends ComponentPathBase {
-  public withComponent(component: ComponentLike): ComponentRoot {
-    return this.component === component ? this : new ComponentRoot(Component.from(component));
+// eslint-disable-next-line @typescript-eslint/no-namespace
+export namespace Component {
+  /**
+   * Creates a new {@link Component} from the provided parts.
+   */
+  export function from(parts: ComponentLike</*With*/ false>): Component {
+    const resolved: ResolvedComponentLike = resolveComponentLike(parts, /*fallbackComponent*/ undefined);
+    if (resolved instanceof ComponentString || resolved instanceof ComponentReference) {
+      return resolved;
+    } else if ('text' in resolved) {
+      return ComponentString.from(resolved);
+    } else {
+      return ComponentReference.from(resolved);
+    }
   }
 
-  public toString(): string {
-    return this.component.toString();
+  /**
+   * Tests whether two {@link Component} values are equivalent.
+   */
+  export function equals(left: Component | undefined, right: Component | undefined): boolean {
+    if (left === undefined || right === undefined) {
+      return left === right;
+    } else if (left instanceof ComponentString) {
+      return right instanceof ComponentString && ComponentString.equals(left, right);
+    } else {
+      return right instanceof ComponentReference && ComponentReference.equals(left, right);
+    }
   }
 }
 
 /**
  * @beta
  */
-export class ComponentNavigation extends ComponentPathBase {
-  public readonly parent: ComponentPath;
-  public readonly navigation: Navigation;
-
-  public constructor(parent: ComponentPath, navigation: Navigation, component: Component) {
-    super(component);
-    this.parent = parent;
-    this.navigation = navigation;
+export type ComponentParts<With extends boolean> = ComponentStringParts | ComponentReferenceParts<With>;
+
+function resolveComponentParts(
+  parts: ComponentParts</*With*/ true>,
+  fallbackComponent: Component | undefined
+): ComponentParts</*With*/ false> {
+  const { text, reference } = parts as AllParts<typeof parts>;
+  if (text !== undefined) {
+    if (reference !== undefined) {
+      throw new TypeError("Cannot specify both 'text' and 'reference'");
+    }
+    return { text };
+  } else if (reference !== undefined) {
+    return resolveComponentReferenceParts(
+      { reference },
+      tryCast(fallbackComponent, ComponentReference)?.reference
+    );
+  } else {
+    if (fallbackComponent === undefined) {
+      throw new TypeError("One of properties 'text' or 'reference' is required");
+    }
+    return fallbackComponent;
   }
+}
 
-  public withParent(parent: ComponentPath): ComponentNavigation {
-    return this.parent === parent ? this : new ComponentNavigation(parent, this.navigation, this.component);
-  }
+/**
+ * @beta
+ */
+export type ComponentLike<With extends boolean> =
+  | ComponentStringLike
+  | Exclude<ComponentReferenceLike<With>, string>;
 
-  public withNavigation(navigation: Navigation): ComponentNavigation {
-    return this.navigation === navigation
-      ? this
-      : new ComponentNavigation(this.parent, navigation, this.component);
-  }
+type ResolvedComponentLike = Component | ComponentStringParts | ComponentReferenceParts</*With*/ false>;
 
-  public withComponent(component: ComponentLike): ComponentNavigation {
-    return this.component === component
-      ? this
-      : new ComponentNavigation(this.parent, this.navigation, Component.from(component));
+function resolveComponentLike(
+  value: ComponentLike</*With*/ true>,
+  fallbackComponent: Component | undefined
+): ResolvedComponentLike {
+  if (value instanceof ComponentString || value instanceof ComponentReference) {
+    return value;
+  } else if (value instanceof DeclarationReference) {
+    return resolveComponentParts({ reference: value }, fallbackComponent);
+  } else if (typeof value === 'string') {
+    return resolveComponentParts({ text: value }, fallbackComponent);
+  } else {
+    return resolveComponentParts(value, fallbackComponent);
   }
+}
 
-  public toString(): string {
-    return `${this.parent}${formatNavigation(this.navigation)}${this.component}`;
+// #endregion Component
+
+// #region Navigation
+
+/**
+ * Indicates the symbol table from which to resolve the next symbol component.
+ * @beta
+ */
+export const enum Navigation {
+  Exports = '.',
+  Members = '#',
+  Locals = '~'
+}
+
+/**
+ * @beta
+ */
+export type SourceNavigation = Navigation.Exports | Navigation.Locals;
+
+function formatNavigation(navigation: Navigation | undefined): string {
+  switch (navigation) {
+    case Navigation.Exports:
+      return '.';
+    case Navigation.Members:
+      return '#';
+    case Navigation.Locals:
+      return '~';
+    default:
+      return '';
   }
 }
 
+// #endregion Navigation
+
+// #region Meaning
+
 /**
  * @beta
  */
@@ -581,82 +2336,9 @@ export const enum Meaning {
   ComplexType = 'complex' // Any complex type
 }
 
-/**
- * @beta
- */
-export interface ISymbolReferenceOptions {
-  meaning?: Meaning;
-  overloadIndex?: number;
-}
-
-/**
- * Represents a reference to a TypeScript symbol.
- * @beta
- */
-export class SymbolReference {
-  public readonly componentPath: ComponentPath | undefined;
-  public readonly meaning: Meaning | undefined;
-  public readonly overloadIndex: number | undefined;
-
-  public constructor(
-    component: ComponentPath | undefined,
-    { meaning, overloadIndex }: ISymbolReferenceOptions = {}
-  ) {
-    this.componentPath = component;
-    this.overloadIndex = overloadIndex;
-    this.meaning = meaning;
-  }
-
-  public static empty(): SymbolReference {
-    return new SymbolReference(/*component*/ undefined);
-  }
-
-  public withComponentPath(componentPath: ComponentPath | undefined): SymbolReference {
-    return this.componentPath === componentPath
-      ? this
-      : new SymbolReference(componentPath, {
-          meaning: this.meaning,
-          overloadIndex: this.overloadIndex
-        });
-  }
-
-  public withMeaning(meaning: Meaning | undefined): SymbolReference {
-    return this.meaning === meaning
-      ? this
-      : new SymbolReference(this.componentPath, {
-          meaning,
-          overloadIndex: this.overloadIndex
-        });
-  }
-
-  public withOverloadIndex(overloadIndex: number | undefined): SymbolReference {
-    return this.overloadIndex === overloadIndex
-      ? this
-      : new SymbolReference(this.componentPath, {
-          meaning: this.meaning,
-          overloadIndex
-        });
-  }
-
-  public addNavigationStep(navigation: Navigation, component: ComponentLike): SymbolReference {
-    if (!this.componentPath) {
-      throw new Error('Cannot add a navigation step to an empty symbol reference.');
-    }
-    return new SymbolReference(this.componentPath.addNavigationStep(navigation, component));
-  }
-
-  public toString(): string {
-    let result: string = `${this.componentPath || ''}`;
-    if (this.meaning && this.overloadIndex !== undefined) {
-      result += `:${this.meaning}(${this.overloadIndex})`;
-    } else if (this.meaning) {
-      result += `:${this.meaning}`;
-    } else if (this.overloadIndex !== undefined) {
-      result += `:${this.overloadIndex}`;
-    }
-    return result;
-  }
-}
+// #endregion Meaning
+
+// #region Token
 
 const enum Token {
   None,
@@ -767,6 +2449,10 @@ function tokenToString(token: Token): string {
   }
 }
 
+// #endregion Token
+
+// #region Scanner
+
 class Scanner {
   private _tokenPos: number;
   private _pos: number;
@@ -1065,6 +2751,115 @@ class Scanner {
   }
 }
 
+function isHexDigit(ch: string): boolean {
+  switch (ch) {
+    case 'a':
+    case 'b':
+    case 'c':
+    case 'd':
+    case 'e':
+    case 'f':
+    case 'A':
+    case 'B':
+    case 'C':
+    case 'D':
+    case 'E':
+    case 'F':
+      return true;
+    default:
+      return isDecimalDigit(ch);
+  }
+}
+
+function isDecimalDigit(ch: string): boolean {
+  switch (ch) {
+    case '0':
+    case '1':
+    case '2':
+    case '3':
+    case '4':
+    case '5':
+    case '6':
+    case '7':
+    case '8':
+    case '9':
+      return true;
+    default:
+      return false;
+  }
+}
+
+function isCharacterEscapeSequence(ch: string): boolean {
+  return isSingleEscapeCharacter(ch) || isNonEscapeCharacter(ch);
+}
+
+function isNonEscapeCharacter(ch: string): boolean {
+  return !isEscapeCharacter(ch) && !isLineTerminator(ch);
+}
+
+function isEscapeCharacter(ch: string): boolean {
+  switch (ch) {
+    case 'x':
+    case 'u':
+      return true;
+    default:
+      return isSingleEscapeCharacter(ch) || isDecimalDigit(ch);
+  }
+}
+
+function isSingleEscapeCharacter(ch: string): boolean {
+  switch (ch) {
+    case "'":
+    case '"':
+    case '\\':
+    case 'b':
+    case 'f':
+    case 'n':
+    case 'r':
+    case 't':
+    case 'v':
+      return true;
+    default:
+      return false;
+  }
+}
+
+function isLineTerminator(ch: string): boolean {
+  switch (ch) {
+    case '\r':
+    case '\n':
+      // TODO: <LS>, <PS>
+      return true;
+    default:
+      return false;
+  }
+}
+
+function isPunctuator(ch: string): boolean {
+  switch (ch) {
+    case '{':
+    case '}':
+    case '(':
+    case ')':
+    case '[':
+    case ']':
+    case '!':
+    case '.':
+    case '#':
+    case '~':
+    case ':':
+    case ',':
+    case '@':
+      return true;
+    default:
+      return false;
+  }
+}
+
+// #endregion Scanner
+
+// #region Parser
+
 class Parser {
   private _errors: string[];
   private _scanner: Scanner;
@@ -1084,9 +2879,8 @@ class Parser {
   }
 
   public parseDeclarationReference(): DeclarationReference {
-    let source: ModuleSource | GlobalSource | undefined;
+    let source: Source | undefined;
     let navigation: Navigation.Locals | undefined;
-    let symbol: SymbolReference | undefined;
     if (this.optionalToken(Token.ExclamationToken)) {
       // Reference to global symbol
       source = GlobalSource.instance;
@@ -1097,12 +2891,7 @@ class Parser {
         navigation = Navigation.Locals;
       }
     }
-    if (this.isStartOfComponent()) {
-      symbol = this.parseSymbol();
-    } else if (this.token() === Token.ColonToken) {
-      symbol = this.parseSymbolRest(new ComponentRoot(new ComponentString('', /*userEscaped*/ true)));
-    }
-    return new DeclarationReference(source, navigation, symbol);
+    return new DeclarationReference(source, navigation, this.tryParseSymbolReference());
   }
 
   public parseModuleSourceString(): string {
@@ -1110,6 +2899,18 @@ class Parser {
     return this.parseTokenString(Token.ModuleSource, 'Module source');
   }
 
+  public parseComponentPath(): ComponentPath {
+    return this.parseComponentRest(this.parseRootComponent());
+  }
+
+  public tryParseSymbolReference(): SymbolReference | undefined {
+    if (this.isStartOfComponent()) {
+      return this.parseSymbol();
+    } else if (this.token() === Token.ColonToken) {
+      return this.parseSymbolRest(new ComponentRoot(new ComponentString('', /*userEscaped*/ true)));
+    }
+  }
+
   public parseComponentString(): string {
     switch (this._scanner.token()) {
       case Token.String:
@@ -1130,7 +2931,7 @@ class Parser {
   }
 
   private parseSymbol(): SymbolReference {
-    const component: ComponentPath = this.parseComponentRest(this.parseRootComponent());
+    const component: ComponentPath = this.parseComponentPath();
     return this.parseSymbolRest(component);
   }
 
@@ -1327,123 +3128,7 @@ class Parser {
   }
 }
 
-function formatNavigation(navigation: Navigation | undefined): string {
-  switch (navigation) {
-    case Navigation.Exports:
-      return '.';
-    case Navigation.Members:
-      return '#';
-    case Navigation.Locals:
-      return '~';
-    default:
-      return '';
-  }
-}
-
-function isCharacterEscapeSequence(ch: string): boolean {
-  return isSingleEscapeCharacter(ch) || isNonEscapeCharacter(ch);
-}
-
-function isSingleEscapeCharacter(ch: string): boolean {
-  switch (ch) {
-    case "'":
-    case '"':
-    case '\\':
-    case 'b':
-    case 'f':
-    case 'n':
-    case 'r':
-    case 't':
-    case 'v':
-      return true;
-    default:
-      return false;
-  }
-}
-
-function isNonEscapeCharacter(ch: string): boolean {
-  return !isEscapeCharacter(ch) && !isLineTerminator(ch);
-}
-
-function isEscapeCharacter(ch: string): boolean {
-  switch (ch) {
-    case 'x':
-    case 'u':
-      return true;
-    default:
-      return isSingleEscapeCharacter(ch) || isDecimalDigit(ch);
-  }
-}
-
-function isLineTerminator(ch: string): boolean {
-  switch (ch) {
-    case '\r':
-    case '\n':
-      // TODO: <LS>, <PS>
-      return true;
-    default:
-      return false;
-  }
-}
-
-function isDecimalDigit(ch: string): boolean {
-  switch (ch) {
-    case '0':
-    case '1':
-    case '2':
-    case '3':
-    case '4':
-    case '5':
-    case '6':
-    case '7':
-    case '8':
-    case '9':
-      return true;
-    default:
-      return false;
-  }
-}
-
-function isHexDigit(ch: string): boolean {
-  switch (ch) {
-    case 'a':
-    case 'b':
-    case 'c':
-    case 'd':
-    case 'e':
-    case 'f':
-    case 'A':
-    case 'B':
-    case 'C':
-    case 'D':
-    case 'E':
-    case 'F':
-      return true;
-    default:
-      return isDecimalDigit(ch);
-  }
-}
-
-function isPunctuator(ch: string): boolean {
-  switch (ch) {
-    case '{':
-    case '}':
-    case '(':
-    case ')':
-    case '[':
-    case ']':
-    case '!':
-    case '.':
-    case '#':
-    case '~':
-    case ':':
-    case ',':
-    case '@':
-      return true;
-    default:
-      return false;
-  }
-}
+// #endregion Parser
 
 function escapeComponentIfNeeded(text: string, userEscaped?: boolean): string {
   if (userEscaped) {
@@ -1455,12 +3140,56 @@ function escapeComponentIfNeeded(text: string, userEscaped?: boolean): string {
   return DeclarationReference.escapeComponentString(text);
 }
 
-function escapeModuleSourceIfNeeded(text: string, userEscaped?: boolean): string {
+function escapeModuleSourceIfNeeded(text: string, parsed: boolean, userEscaped: boolean): string {
   if (userEscaped) {
-    if (!DeclarationReference.isWellFormedModuleSourceString(text)) {
+    if (!parsed && !DeclarationReference.isWellFormedModuleSourceString(text)) {
       throw new SyntaxError(`Invalid Module source '${text}'`);
     }
     return text;
   }
   return DeclarationReference.escapeModuleSourceString(text);
 }
+
+function isBracketed(value: string): boolean {
+  return value.length > 2 && value.charAt(0) === '[' && value.charAt(value.length - 1) === ']';
+}
+
+function ensureScopeName(scopeName: string): string {
+  return scopeName.length && scopeName.charAt(0) !== '@' ? `@${scopeName}` : scopeName;
+}
+
+interface ObjectConstructorWithSetPrototypeOf extends ObjectConstructor {
+  // eslint-disable-next-line @rushstack/no-new-null
+  setPrototypeOf?(obj: object, proto: object | null): object;
+}
+
+// eslint-disable-next-line @rushstack/no-new-null
+const setPrototypeOf:
+  | ((obj: object, proto: object | null) => object)
+  | undefined = (Object as ObjectConstructorWithSetPrototypeOf).setPrototypeOf;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function tryCast<T>(value: unknown, type: new (...args: any[]) => T): T | undefined {
+  return value instanceof type ? value : undefined;
+}
+
+/**
+ * Describes the parts that can be used in a `from()` or `with()` call.
+ * @beta
+ */
+export type Parts<With extends boolean, T> = [With] extends [false]
+  ? T
+  : T extends unknown
+  ? Partial<T>
+  : never;
+
+/**
+ * If a part can be removed via a `with()` call, marks that part with `| null`
+ * @beta
+ */
+export type Part<With extends boolean, T> = [With] extends [false] ? T : T | null;
+
+type AllKeysOf<T> = T extends unknown ? keyof T : never;
+type AllParts<T> = {
+  [P in AllKeysOf<T>]: T extends unknown ? (P extends keyof T ? T[P] : undefined) : never;
+};
diff --git a/tsdoc/src/beta/__tests__/DeclarationReference.test.ts b/tsdoc/src/beta/__tests__/DeclarationReference.test.ts
index dfbc57f4..5e10da16 100644
--- a/tsdoc/src/beta/__tests__/DeclarationReference.test.ts
+++ b/tsdoc/src/beta/__tests__/DeclarationReference.test.ts
@@ -1,3 +1,5 @@
+/* eslint-disable max-lines */
+
 import {
   ModuleSource,
   GlobalSource,
@@ -7,9 +9,43 @@ import {
   ComponentNavigation,
   DeclarationReference,
   SymbolReference,
-  ComponentReference
+  ComponentReference,
+  ComponentString,
+  Component,
+  ComponentLike,
+  ComponentPath,
+  ComponentNavigationParts,
+  Source
 } from '../DeclarationReference';
 
+// aliases to make some of the 'each' tests easier to read
+const { from: MOD } = ModuleSource;
+const { from: DREF } = DeclarationReference;
+const { from: SYM } = SymbolReference;
+const { from: CROOT } = ComponentRoot;
+const { from: CSTR } = ComponentString;
+const { from: CREF } = ComponentReference;
+
+function CNAV(parts: ComponentNavigationParts</*With*/ false>): ComponentNavigation;
+function CNAV(
+  parent: ComponentPath,
+  navigation: '.' | '#' | '~',
+  component: ComponentLike</*With*/ false>
+): ComponentNavigation;
+function CNAV(
+  ...args:
+    | [ComponentNavigationParts</*With*/ false>]
+    | [ComponentPath, '.' | '#' | '~', ComponentLike</*With*/ false>]
+): ComponentNavigation {
+  switch (args.length) {
+    case 3:
+      const [parent, navigation, component] = args;
+      return ComponentNavigation.from({ parent, navigation: navigation as Navigation, component });
+    case 1:
+      return ComponentNavigation.from(args[0]);
+  }
+}
+
 describe('parser', () => {
   it('parse component text', () => {
     const ref: DeclarationReference = DeclarationReference.parse('abc');
@@ -118,16 +154,7 @@ describe('parser', () => {
     }).toThrow();
   });
 });
-it('add navigation step', () => {
-  const ref: DeclarationReference = DeclarationReference.empty().addNavigationStep(
-    Navigation.Members,
-    ComponentReference.parse('[Symbol.iterator]')
-  );
-  const symbol: SymbolReference = ref.symbol!;
-  expect(symbol).toBeInstanceOf(SymbolReference);
-  expect(symbol.componentPath).toBeDefined();
-  expect(symbol.componentPath!.component.toString()).toBe('[Symbol.iterator]');
-});
+
 describe('DeclarationReference', () => {
   it.each`
     text           | expected
@@ -164,7 +191,7 @@ describe('DeclarationReference', () => {
     ${'"}"'}       | ${true}
     ${'"("'}       | ${true}
     ${'")"'}       | ${true}
-  `('isWellFormedComponentString($text)', ({ text, expected }) => {
+  `('static isWellFormedComponentString($text)', ({ text, expected }) => {
     expect(DeclarationReference.isWellFormedComponentString(text)).toBe(expected);
   });
   it.each`
@@ -189,7 +216,7 @@ describe('DeclarationReference', () => {
     ${'[a!b]'}   | ${'"[a!b]"'}
     ${'""'}      | ${'"\\"\\""'}
     ${'"a"'}     | ${'"\\"a\\""'}
-  `('escapeComponentString($text)', ({ text, expected }) => {
+  `('static escapeComponentString($text)', ({ text, expected }) => {
     expect(DeclarationReference.escapeComponentString(text)).toBe(expected);
   });
   it.each`
@@ -201,7 +228,7 @@ describe('DeclarationReference', () => {
     ${'"a.b"'}     | ${'a.b'}
     ${'"\\"\\""'}  | ${'""'}
     ${'"\\"a\\""'} | ${'"a"'}
-  `('unescapeComponentString($text)', ({ text, expected }) => {
+  `('static unescapeComponentString($text)', ({ text, expected }) => {
     if (expected === undefined) {
       expect(() => DeclarationReference.unescapeComponentString(text)).toThrow();
     } else {
@@ -244,7 +271,7 @@ describe('DeclarationReference', () => {
     ${'"("'}       | ${true}
     ${'")"'}       | ${true}
     ${'"[a!b]"'}   | ${true}
-  `('isWellFormedModuleSourceString($text)', ({ text, expected }) => {
+  `('static isWellFormedModuleSourceString($text)', ({ text, expected }) => {
     expect(DeclarationReference.isWellFormedModuleSourceString(text)).toBe(expected);
   });
   it.each`
@@ -269,7 +296,7 @@ describe('DeclarationReference', () => {
     ${'[a!b]'}   | ${'"[a!b]"'}
     ${'""'}      | ${'"\\"\\""'}
     ${'"a"'}     | ${'"\\"a\\""'}
-  `('escapeModuleSourceString($text)', ({ text, expected }) => {
+  `('static escapeModuleSourceString($text)', ({ text, expected }) => {
     expect(DeclarationReference.escapeModuleSourceString(text)).toBe(expected);
   });
   it.each`
@@ -282,14 +309,801 @@ describe('DeclarationReference', () => {
     ${'"a.b"'}     | ${'a.b'}
     ${'"\\"\\""'}  | ${'""'}
     ${'"\\"a\\""'} | ${'"a"'}
-  `('unescapeModuleSourceString($text)', ({ text, expected }) => {
+  `('static unescapeModuleSourceString($text)', ({ text, expected }) => {
     if (expected === undefined) {
       expect(() => DeclarationReference.unescapeModuleSourceString(text)).toThrow();
     } else {
       expect(DeclarationReference.unescapeModuleSourceString(text)).toBe(expected);
     }
   });
+  describe('static from()', () => {
+    it('static from(undefined)', () => {
+      const declref = DeclarationReference.from(undefined);
+      expect(declref.isEmpty).toBe(true);
+    });
+    it('static from(string)', () => {
+      const declref = DeclarationReference.from('a!b');
+      expect(declref.source?.toString()).toBe('a!');
+      expect(declref.symbol?.toString()).toBe('b');
+    });
+    it('static from(DeclarationReference)', () => {
+      const declref1 = DeclarationReference.from('a!b');
+      const declref2 = DeclarationReference.from(declref1);
+      expect(declref2).toBe(declref1);
+    });
+    it('static from({ })', () => {
+      const declref = DeclarationReference.from({});
+      expect(declref.isEmpty).toBe(true);
+    });
+    it('static from({ source })', () => {
+      const source = MOD('a');
+      const declref = DeclarationReference.from({ source });
+      expect(declref.source).toBe(source);
+    });
+    it('static from({ packageName })', () => {
+      const declref = DeclarationReference.from({ packageName: 'a' });
+      expect(declref.source).toBeInstanceOf(ModuleSource);
+      expect(declref.source?.toString()).toBe('a!');
+    });
+    it('static from({ symbol })', () => {
+      const symbol = SYM('a');
+      const declref = DeclarationReference.from({ symbol });
+      expect(declref.symbol).toBe(symbol);
+    });
+    it('static from({ componentPath })', () => {
+      const declref = DeclarationReference.from({ componentPath: 'a' });
+      expect(declref.symbol).toBeDefined();
+      expect(declref.symbol?.toString()).toBe('a');
+    });
+  });
+  describe('with()', () => {
+    describe('with({ })', () => {
+      it('produces same reference', () => {
+        const declref = DeclarationReference.from({});
+        expect(declref.with({})).toBe(declref);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        declref.with({});
+        expect(declref.source).toBe(source);
+        expect(declref.navigation).toBe(Navigation.Exports);
+        expect(declref.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ source: <same> })', () => {
+      it('produces same reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        const declref2 = declref1.with({ source });
+        expect(declref2).toBe(declref1);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        declref1.with({ source });
+        expect(declref1.source).toBe(source);
+        expect(declref1.navigation).toBe(Navigation.Exports);
+        expect(declref1.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ source: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        const declref2 = declref1.with({ source: MOD('a') });
+        expect(declref2).toBe(declref1);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        declref1.with({ source: MOD('a') });
+        expect(declref1.source).toBe(source);
+        expect(declref1.navigation).toBe(Navigation.Exports);
+        expect(declref1.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ source: <equivalent string> })', () => {
+      it('produces same reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        const declref2 = declref1.with({ source: 'a' });
+        expect(declref2).toBe(declref1);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        declref1.with({ source: 'a' });
+        expect(declref1.source).toBe(source);
+        expect(declref1.navigation).toBe(Navigation.Exports);
+        expect(declref1.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ source: <different> })', () => {
+      it('produces new reference', () => {
+        const source1 = MOD('a');
+        const symbol = SYM('a');
+        const source2 = MOD('b');
+        const declref1 = DeclarationReference.from({
+          source: source1,
+          navigation: Navigation.Exports,
+          symbol
+        });
+        const declref2 = declref1.with({ source: source2 });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source).toBe(source2);
+        expect(declref2.navigation).toBe(Navigation.Exports);
+        expect(declref2.symbol).toBe(symbol);
+      });
+      it('does not change existing reference', () => {
+        const source1 = MOD('a');
+        const source2 = MOD('b');
+        const symbol = SYM('a');
+        const declref1 = DeclarationReference.from({
+          source: source1,
+          navigation: Navigation.Exports,
+          symbol
+        });
+        declref1.with({ source: source2 });
+        expect(declref1.source).toBe(source1);
+        expect(declref1.navigation).toBe(Navigation.Exports);
+        expect(declref1.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ source: <new> })', () => {
+      it('produces new reference', () => {
+        const source = MOD('b');
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ source });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source).toBe(source);
+      });
+    });
+    describe('with({ source: null })', () => {
+      it('w/existing source: produces new reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: null });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source).toBeUndefined();
+      });
+      it('w/existing source: does not change existing reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        declref1.with({ source: null });
+        expect(declref1.source).toBe(source);
+      });
+      it('w/o existing source: produces same reference', () => {
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ navigation: Navigation.Exports, symbol });
+        const declref2 = declref1.with({ source: null });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ source: undefined })', () => {
+      it('w/existing source: produces same reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: undefined });
+        expect(declref2).toBe(declref1);
+      });
+      it('w/o existing source: produces same reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ source: undefined });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ source: { packageName: <equivalent> } })', () => {
+      it('produces same reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: { packageName: 'a' } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ source: { packageName: <different> } })', () => {
+      it('produces new reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: { packageName: 'b' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/o new importPath in package name: does not change importPath', () => {
+        const source = MOD('a/b');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: { packageName: 'c' } });
+        expect(declref2.source?.toString()).toBe('c/b!');
+      });
+      it('w/new importPath in package name: changes importPath', () => {
+        const source = MOD('a/b');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: { packageName: 'c/d' } });
+        expect(declref2.source?.toString()).toBe('c/d!');
+      });
+    });
+    describe('with({ source: { packageName: <new> } })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ source: { packageName: 'b' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+    });
+    describe('with({ source: { unscopedPackageName: <equivalent> } })', () => {
+      it('w/o scope: produces same reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: { unscopedPackageName: 'a' } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ source: { unscopedPackageName: <different> } })', () => {
+      it('w/existing source w/o scope: produces new reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ source: { unscopedPackageName: 'b' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/o existing source: produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ source: { unscopedPackageName: 'b' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/existing source w/scope: produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ source: { unscopedPackageName: 'c' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@a/c!');
+      });
+      it('does not change importPath', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b/c') });
+        const declref2 = declref1.with({ source: { unscopedPackageName: 'd' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@a/d/c!');
+      });
+    });
+    describe('with({ source: { scopeName: <equivalent> } })', () => {
+      it('produces same reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ source: { scopeName: '@a' } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ source: { scopeName: <different> } })', () => {
+      it('w/source w/scope: produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ source: { scopeName: '@c' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@c/b!');
+      });
+      it('w/source w/o scope: produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('a') });
+        const declref2 = declref1.with({ source: { scopeName: '@b' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@b/a!');
+      });
+      it('does not change importPath', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b/c') });
+        const declref2 = declref1.with({ source: { scopeName: '@d' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@d/b/c!');
+      });
+    });
+    describe('with({ source: { scopeName: <new> } })', () => {
+      it('w/existing scope: produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ source: { scopeName: null } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/o existing scope: produces same reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('a') });
+        const declref2 = declref1.with({ source: { scopeName: null } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ source: { scopeName: null } })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('a') });
+        const declref2 = declref1.with({ source: { scopeName: '@c' } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@c/a!');
+      });
+    });
+    describe('with({ packageName: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ packageName: 'a' });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ packageName: <different> })', () => {
+      it('produces new reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ packageName: 'b' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/o new importPath in package name: does not change importPath', () => {
+        const source = MOD('a/b');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ packageName: 'c' });
+        expect(declref2.source?.toString()).toBe('c/b!');
+      });
+      it('w/new importPath in package name: changes importPath', () => {
+        const source = MOD('a/b');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ packageName: 'c/d' });
+        expect(declref2.source?.toString()).toBe('c/d!');
+      });
+    });
+    describe('with({ packageName: <new> })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ packageName: 'b' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+    });
+    describe('with({ unscopedPackageName: <equivalent> })', () => {
+      it('w/o scope: produces same reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ unscopedPackageName: 'a' });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ unscopedPackageName: <different> })', () => {
+      it('w/o scope: produces new reference', () => {
+        const source = MOD('a');
+        const declref1 = DeclarationReference.from({ source });
+        const declref2 = declref1.with({ unscopedPackageName: 'b' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/scope: produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ unscopedPackageName: 'c' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@a/c!');
+      });
+      it('does not change importPath', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b/c') });
+        const declref2 = declref1.with({ unscopedPackageName: 'd' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@a/d/c!');
+      });
+    });
+    describe('with({ unscopedPackageName: <new> })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ unscopedPackageName: 'b' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+    });
+    describe('with({ scopeName: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ scopeName: '@a' });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ scopeName: <different> })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ scopeName: '@c' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@c/b!');
+      });
+      it('does not change importPath', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b/c') });
+        const declref2 = declref1.with({ scopeName: '@d' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@d/b/c!');
+      });
+    });
+    describe('with({ scopeName: <new> })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('a') });
+        const declref2 = declref1.with({ scopeName: '@b' });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('@b/a!');
+      });
+    });
+    describe('with({ scopeName: null })', () => {
+      it('w/existing scope: produces new reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('@a/b') });
+        const declref2 = declref1.with({ scopeName: null });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.source?.toString()).toBe('b!');
+      });
+      it('w/o existing scope: produces same reference', () => {
+        const declref1 = DeclarationReference.from({ source: MOD('a') });
+        const declref2 = declref1.with({ scopeName: null });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ symbol: <same> })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol });
+        expect(declref2).toBe(declref1);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        declref1.with({ symbol });
+        expect(declref1.source).toBe(source);
+        expect(declref1.navigation).toBe(Navigation.Exports);
+        expect(declref1.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ symbol: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: SYM('a:var') });
+        expect(declref2).toBe(declref1);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ source, navigation: Navigation.Exports, symbol });
+        declref1.with({ symbol: SYM('b') });
+        expect(declref1.source).toBe(source);
+        expect(declref1.navigation).toBe(Navigation.Exports);
+        expect(declref1.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ symbol: <different> })', () => {
+      it('produces new reference', () => {
+        const symbol1 = SYM('a:var');
+        const symbol2 = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol: symbol1 });
+        const declref2 = declref1.with({ symbol: symbol2 });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol).toBe(symbol2);
+      });
+      it('does not change existing reference', () => {
+        const source = MOD('a');
+        const symbol = SYM('a:var');
+        const declref = DeclarationReference.from({ source, symbol });
+        declref.with({ symbol: SYM('b:var') });
+        expect(declref.source).toBe(source);
+        expect(declref.symbol).toBe(symbol);
+      });
+    });
+    describe('with({ symbol: <new> })', () => {
+      it('produces new reference', () => {
+        const symbol2 = SYM('b:var');
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: symbol2 });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol).toBe(symbol2);
+      });
+    });
+    describe('with({ symbol: null })', () => {
+      it('w/existing symbol: produces new reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: null });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol).toBeUndefined();
+      });
+      it('w/existing symbol: does not change existing reference', () => {
+        const symbol = SYM('a:var');
+        const declref = DeclarationReference.from({ symbol });
+        declref.with({ symbol: null });
+        expect(declref.symbol).toBe(symbol);
+      });
+      it('w/o existing symbol: produces same reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: null });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ symbol: { componentPath: <equivalent> } })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { componentPath: CROOT(CSTR('a')) } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ symbol: { componentPath: <different> } })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { componentPath: CROOT(CSTR('b')) } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.componentPath?.toString()).toBe('b');
+      });
+    });
+    describe('with({ symbol: { componentPath: <new> } })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: { componentPath: CROOT(CSTR('a')) } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.componentPath?.toString()).toBe('a');
+      });
+    });
+    describe('with({ symbol: { componentPath: null } })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { componentPath: null } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.componentPath).toBeUndefined();
+      });
+    });
+    describe('with({ symbol: { meaning: <equivalent> } })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { meaning: Meaning.Variable } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ symbol: { meaning: <different> } })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { meaning: Meaning.Interface } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBe(Meaning.Interface);
+      });
+    });
+    describe('with({ symbol: { meaning: <new> } })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { meaning: Meaning.Variable } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBe(Meaning.Variable);
+      });
+      it('w/o symbol: produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: { meaning: Meaning.Variable } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBe(Meaning.Variable);
+      });
+    });
+    describe('with({ symbol: { meaning: null } })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { meaning: null } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBeUndefined();
+      });
+      it('w/o symbol: produces same reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: { meaning: null } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ symbol: { overloadIndex: <equivalent> } })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('b:0');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { overloadIndex: 0 } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ symbol: { overloadIndex: <different> } })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('b:0');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { overloadIndex: 1 } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBe(1);
+      });
+    });
+    describe('with({ symbol: { overloadIndex: <new> } })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { overloadIndex: 0 } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBe(0);
+      });
+      it('w/o symbol: produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: { overloadIndex: 0 } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBe(0);
+      });
+    });
+    describe('with({ symbol: { overloadIndex: null } })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b:0');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ symbol: { overloadIndex: null } });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBeUndefined();
+      });
+      it('w/o symbol: produces same reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ symbol: { overloadIndex: null } });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ componentPath: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ componentPath: CROOT(CSTR('a')) });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ componentPath: <different> })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ componentPath: CROOT(CSTR('b')) });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.componentPath?.toString()).toBe('b');
+      });
+    });
+    describe('with({ componentPath: <new> })', () => {
+      it('produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ componentPath: CROOT(CSTR('a')) });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.componentPath?.toString()).toBe('a');
+      });
+    });
+    describe('with({ componentPath: null })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('a:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ componentPath: null });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.componentPath).toBeUndefined();
+      });
+    });
+    describe('with({ meaning: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ meaning: Meaning.Variable });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ meaning: <different> })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ meaning: Meaning.Interface });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBe(Meaning.Interface);
+      });
+    });
+    describe('with({ meaning: <new> })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ meaning: Meaning.Variable });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBe(Meaning.Variable);
+      });
+      it('w/o symbol: produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ meaning: Meaning.Variable });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBe(Meaning.Variable);
+      });
+    });
+    describe('with({ meaning: null })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b:var');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ meaning: null });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.meaning).toBeUndefined();
+      });
+      it('w/o symbol: produces same reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ meaning: null });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ overloadIndex: <equivalent> })', () => {
+      it('produces same reference', () => {
+        const symbol = SYM('b:0');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ overloadIndex: 0 });
+        expect(declref2).toBe(declref1);
+      });
+    });
+    describe('with({ overloadIndex: <different> })', () => {
+      it('produces new reference', () => {
+        const symbol = SYM('b:0');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ overloadIndex: 1 });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBe(1);
+      });
+    });
+    describe('with({ overloadIndex: <new> })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ overloadIndex: 0 });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBe(0);
+      });
+      it('w/o symbol: produces new reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ overloadIndex: 0 });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBe(0);
+      });
+    });
+    describe('with({ overloadIndex: null })', () => {
+      it('w/symbol: produces new reference', () => {
+        const symbol = SYM('b:0');
+        const declref1 = DeclarationReference.from({ symbol });
+        const declref2 = declref1.with({ overloadIndex: null });
+        expect(declref2).not.toBe(declref1);
+        expect(declref2.symbol?.overloadIndex).toBeUndefined();
+      });
+      it('w/o symbol: produces same reference', () => {
+        const declref1 = DeclarationReference.from({});
+        const declref2 = declref1.with({ overloadIndex: null });
+        expect(declref2).toBe(declref1);
+      });
+    });
+  });
+  it('addNavigationStep()', () => {
+    const symbol1 = SYM('a');
+    const symbol2 = symbol1.addNavigationStep(Navigation.Exports, 'b');
+    expect(symbol2.componentPath).toBeInstanceOf(ComponentNavigation);
+    expect((symbol2.componentPath as ComponentNavigation).parent).toBe(symbol1.componentPath);
+    expect((symbol2.componentPath as ComponentNavigation).navigation).toBe(Navigation.Exports);
+    expect((symbol2.componentPath as ComponentNavigation).component.toString()).toBe('b');
+  });
+  it.each`
+    left               | right              | expected
+    ${undefined}       | ${undefined}       | ${true}
+    ${DREF('a!b:var')} | ${undefined}       | ${false}
+    ${DREF('a!b:var')} | ${DREF('a!b:var')} | ${true}
+    ${DREF('a!b:var')} | ${DREF('a!b')}     | ${false}
+  `('static equals($left, $right)', ({ left, right, expected }) => {
+    expect(DeclarationReference.equals(left, right)).toBe(expected);
+    expect(DeclarationReference.equals(right, left)).toBe(expected);
+  });
 });
+
+describe('SourceBase', () => {
+  it('toDeclarationReference()', () => {
+    const source = MOD('a');
+    const symbol = SYM({});
+    const declref = source.toDeclarationReference({
+      navigation: Navigation.Exports,
+      symbol
+    });
+    expect(declref.source).toBe(source);
+    expect(declref.navigation).toBe(Navigation.Exports);
+    expect(declref.symbol).toBe(symbol);
+  });
+});
+
 describe('ModuleSource', () => {
   it.each`
     text        | packageName | scopeName | unscopedPackageName | importPath
@@ -329,4 +1143,923 @@ describe('ModuleSource', () => {
       expect(source.path).toBe(text);
     }
   );
+  describe('static from()', () => {
+    it('static from(string) w/o scope', () => {
+      const source = ModuleSource.from('a/b');
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('static from(string) w/trailing "!"', () => {
+      const source = ModuleSource.from('a/b!');
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('static from(string) w/ scope', () => {
+      const source = ModuleSource.from('@a/b/c');
+      expect(source.packageName).toBe('@a/b');
+      expect(source.scopeName).toBe('@a');
+      expect(source.unscopedPackageName).toBe('b');
+      expect(source.importPath).toBe('c');
+    });
+    it('static from(ModuleSource)', () => {
+      const source1 = ModuleSource.from('a');
+      const source2 = ModuleSource.from(source1);
+      expect(source2).toBe(source1);
+    });
+    it('static from({ packageName: "a/b" })', () => {
+      const source = ModuleSource.from({ packageName: 'a/b' });
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('static from({ packageName: "@a/b/c" })', () => {
+      const source = ModuleSource.from({ packageName: '@a/b/c' });
+      expect(source.packageName).toBe('@a/b');
+      expect(source.scopeName).toBe('@a');
+      expect(source.unscopedPackageName).toBe('b');
+      expect(source.importPath).toBe('c');
+    });
+    it('static from({ packageName, importPath })', () => {
+      const source = ModuleSource.from({ packageName: 'a', importPath: 'b' });
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('static from({ unscopedPackageName })', () => {
+      const source = ModuleSource.from({ unscopedPackageName: 'a', importPath: 'b' });
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('static from({ scopeName, unscopedPackageName, importPath })', () => {
+      const source = ModuleSource.from({ scopeName: 'a', unscopedPackageName: 'b', importPath: 'c' });
+      expect(source.packageName).toBe('@a/b');
+      expect(source.scopeName).toBe('@a');
+      expect(source.unscopedPackageName).toBe('b');
+      expect(source.importPath).toBe('c');
+    });
+    it('static from({ importPath })', () => {
+      const source = ModuleSource.from({ importPath: '/c' });
+      expect(source.packageName).toBe('');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('');
+      expect(source.importPath).toBe('/c');
+    });
+  });
+  describe('with()', () => {
+    it('with({ })', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({});
+      expect(source2).toBe(source1);
+    });
+    it('with({ packageName: <same> })', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ packageName: 'a' });
+      expect(source2).toBe(source1);
+    });
+    it('with({ packageName: <different> }) w/ existing scopeName', () => {
+      const source1 = ModuleSource.from('@a/b/c');
+      const source2 = source1.with({ packageName: 'd' });
+      expect(source2.packageName).toBe('d');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('d');
+      expect(source2.importPath).toBe('c');
+    });
+    it('with({ packageName: <different> }) w/o existing scopeName', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ packageName: 'c' });
+      expect(source2.packageName).toBe('c');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('c');
+      expect(source2.importPath).toBe('b');
+    });
+    it('with({ packageName: <different + path> })', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ packageName: 'c/d' });
+      expect(source2.packageName).toBe('c');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('c');
+      expect(source2.importPath).toBe('d');
+    });
+    it('with({ scopeName: <same> })', () => {
+      const source1 = ModuleSource.from('@a/b/c');
+      const source2 = source1.with({ scopeName: '@a' });
+      expect(source2).toBe(source1);
+    });
+    it('with({ scopeName: <different> }) w/ existing scopeName', () => {
+      const source1 = ModuleSource.from('@a/b/c');
+      const source2 = source1.with({ scopeName: 'd' });
+      expect(source2.packageName).toBe('@d/b');
+      expect(source2.scopeName).toBe('@d');
+      expect(source2.unscopedPackageName).toBe('b');
+      expect(source2.importPath).toBe('c');
+    });
+    it('with({ scopeName: <different> }) w/o existing scopeName', () => {
+      const source1 = ModuleSource.from('b/c');
+      const source2 = source1.with({ scopeName: 'd' });
+      expect(source2.packageName).toBe('@d/b');
+      expect(source2.scopeName).toBe('@d');
+      expect(source2.unscopedPackageName).toBe('b');
+      expect(source2.importPath).toBe('c');
+    });
+    it('with({ scopeName: null }) w/ existing scopeName', () => {
+      const source1 = ModuleSource.from('@a/b/c');
+      const source2 = source1.with({ scopeName: null });
+      expect(source2.packageName).toBe('b');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('b');
+      expect(source2.importPath).toBe('c');
+    });
+    it('with({ unscopedPackageName: <same> })', () => {
+      const source1 = ModuleSource.from('@a/b/c');
+      const source2 = source1.with({ unscopedPackageName: 'b' });
+      expect(source2).toBe(source1);
+    });
+    it('with({ unscopedPackageName: <different> }) w/ scopeName', () => {
+      const source1 = ModuleSource.from('@a/b/c');
+      const source2 = source1.with({ unscopedPackageName: 'd' });
+      expect(source2.packageName).toBe('@a/d');
+      expect(source2.scopeName).toBe('@a');
+      expect(source2.unscopedPackageName).toBe('d');
+      expect(source2.importPath).toBe('c');
+    });
+    it('with({ unscopedPackageName: <different> }) w/o scopeName', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ unscopedPackageName: 'c' });
+      expect(source2.packageName).toBe('c');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('c');
+      expect(source2.importPath).toBe('b');
+    });
+    it('with({ importPath: <same> })', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ importPath: 'b' });
+      expect(source2).toBe(source1);
+    });
+    it('with({ importPath: <different> })', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ importPath: 'c' });
+      expect(source2.packageName).toBe('a');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('a');
+      expect(source2.importPath).toBe('c');
+    });
+    it('with({ importPath: null })', () => {
+      const source1 = ModuleSource.from('a/b');
+      const source2 = source1.with({ importPath: null });
+      expect(source2.packageName).toBe('a');
+      expect(source2.scopeName).toBe('');
+      expect(source2.unscopedPackageName).toBe('a');
+      expect(source2.importPath).toBe('');
+    });
+  });
+  it.each`
+    left         | right         | expected
+    ${undefined} | ${undefined}  | ${true}
+    ${MOD('a')}  | ${undefined}  | ${false}
+    ${MOD('a')}  | ${MOD('a')}   | ${true}
+    ${MOD('a')}  | ${MOD('a/b')} | ${false}
+  `('static equals($left, $right)', ({ left, right, expected }) => {
+    expect(ModuleSource.equals(left, right)).toBe(expected);
+    expect(ModuleSource.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('Source', () => {
+  describe('from()', () => {
+    it('from("!")', () => {
+      const source = Source.from('!');
+      expect(source).toBe(GlobalSource.instance);
+    });
+    it('from(string) w/o scope', () => {
+      const source = Source.from('a/b') as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('from(string) w/trailing "!"', () => {
+      const source = Source.from('a/b!') as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('from(string) w/ scope', () => {
+      const source = Source.from('@a/b/c') as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('@a/b');
+      expect(source.scopeName).toBe('@a');
+      expect(source.unscopedPackageName).toBe('b');
+      expect(source.importPath).toBe('c');
+    });
+    it('from(ModuleSource)', () => {
+      const source1 = ModuleSource.from('a');
+      const source2 = Source.from(source1);
+      expect(source2).toBe(source1);
+    });
+    it('from(GlobalSource)', () => {
+      const source1 = GlobalSource.instance;
+      const source2 = Source.from(source1);
+      expect(source2).toBe(source1);
+    });
+    it('from({ packageName: "a/b" })', () => {
+      const source = Source.from({ packageName: 'a/b' }) as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('from({ packageName: "@a/b/c" })', () => {
+      const source = Source.from({ packageName: '@a/b/c' }) as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('@a/b');
+      expect(source.scopeName).toBe('@a');
+      expect(source.unscopedPackageName).toBe('b');
+      expect(source.importPath).toBe('c');
+    });
+    it('from({ packageName, importPath })', () => {
+      const source = Source.from({ packageName: 'a', importPath: 'b' }) as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('from({ unscopedPackageName })', () => {
+      const source = Source.from({ unscopedPackageName: 'a', importPath: 'b' }) as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('a');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('a');
+      expect(source.importPath).toBe('b');
+    });
+    it('from({ scopeName, unscopedPackageName, importPath })', () => {
+      const source = Source.from({
+        scopeName: 'a',
+        unscopedPackageName: 'b',
+        importPath: 'c'
+      }) as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('@a/b');
+      expect(source.scopeName).toBe('@a');
+      expect(source.unscopedPackageName).toBe('b');
+      expect(source.importPath).toBe('c');
+    });
+    it('from({ importPath })', () => {
+      const source = Source.from({ importPath: '/c' }) as ModuleSource;
+      expect(source).toBeInstanceOf(ModuleSource);
+      expect(source.packageName).toBe('');
+      expect(source.scopeName).toBe('');
+      expect(source.unscopedPackageName).toBe('');
+      expect(source.importPath).toBe('/c');
+    });
+  });
+  it.each`
+    left                     | right                    | expected
+    ${undefined}             | ${undefined}             | ${true}
+    ${GlobalSource.instance} | ${undefined}             | ${false}
+    ${GlobalSource.instance} | ${GlobalSource.instance} | ${true}
+    ${MOD('a')}              | ${undefined}             | ${false}
+    ${MOD('a')}              | ${MOD('a')}              | ${true}
+    ${MOD('a')}              | ${GlobalSource.instance} | ${false}
+    ${MOD('a')}              | ${MOD('a/b')}            | ${false}
+  `('equals($left, $right)', ({ left, right, expected }) => {
+    expect(Source.equals(left, right)).toBe(expected);
+    expect(Source.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('SymbolReference', () => {
+  it('static empty()', () => {
+    const symbol = SymbolReference.empty();
+    expect(symbol.componentPath).toBeUndefined();
+    expect(symbol.meaning).toBeUndefined();
+    expect(symbol.overloadIndex).toBeUndefined();
+  });
+  describe('static from()', () => {
+    it('static from({ })', () => {
+      const symbol = SymbolReference.from({});
+      expect(symbol.componentPath).toBeUndefined();
+      expect(symbol.meaning).toBeUndefined();
+      expect(symbol.overloadIndex).toBeUndefined();
+    });
+    it('static from({ componentPath })', () => {
+      const componentPath = CROOT(CSTR('a'));
+      const symbol = SymbolReference.from({ componentPath });
+      expect(symbol.componentPath).toBe(componentPath);
+      expect(symbol.meaning).toBeUndefined();
+      expect(symbol.overloadIndex).toBeUndefined();
+    });
+    it('static from({ meaning })', () => {
+      const symbol = SymbolReference.from({ meaning: Meaning.Variable });
+      expect(symbol.componentPath).toBeUndefined();
+      expect(symbol.meaning).toBe(Meaning.Variable);
+      expect(symbol.overloadIndex).toBeUndefined();
+    });
+    it('static from(SymbolReference)', () => {
+      const symbol1 = SYM({});
+      const symbol2 = SymbolReference.from(symbol1);
+      expect(symbol2).toBe(symbol1);
+    });
+  });
+  describe('with()', () => {
+    it('with({ })', () => {
+      const symbol = SYM({});
+      const updated = symbol.with({});
+      expect(updated).toBe(symbol);
+    });
+    it('with({ componentPath: <same> })', () => {
+      const componentPath = CROOT(CSTR('a'));
+      const symbol = SYM({ componentPath });
+      const updated = symbol.with({ componentPath });
+      expect(updated).toBe(symbol);
+    });
+    it('with({ componentPath: <equivalent> })', () => {
+      const componentPath = CROOT(CSTR('a'));
+      const symbol = SYM({ componentPath });
+      const updated = symbol.with({ componentPath: CROOT(CSTR('a')) });
+      expect(updated).toBe(symbol);
+    });
+    it('with({ componentPath: null })', () => {
+      const componentPath = CROOT(CSTR('a'));
+      const symbol = SYM({ componentPath });
+      const updated = symbol.with({ componentPath: null });
+      expect(updated).not.toBe(symbol);
+      expect(updated.componentPath).toBeUndefined();
+    });
+    it('with({ meaning: <same> })', () => {
+      const symbol = SYM({ meaning: Meaning.Variable });
+      const updated = symbol.with({ meaning: Meaning.Variable });
+      expect(updated).toBe(symbol);
+    });
+    it('with({ overloadIndex: <same> })', () => {
+      const symbol = SYM({ overloadIndex: 0 });
+      const updated = symbol.with({ overloadIndex: 0 });
+      expect(updated).toBe(symbol);
+    });
+  });
+  it('withComponentPath()', () => {
+    const root = CROOT(CSTR('a'));
+    const symbol = SYM({});
+    const updated = symbol.withComponentPath(root);
+    expect(updated).not.toBe(symbol);
+    expect(updated.componentPath).toBe(root);
+  });
+  it('withMeaning()', () => {
+    const symbol = SYM({});
+    const updated = symbol.withMeaning(Meaning.Variable);
+    expect(updated).not.toBe(symbol);
+    expect(updated.meaning).toBe(Meaning.Variable);
+  });
+  it('withOverloadIndex()', () => {
+    const symbol = SYM({});
+    const updated = symbol.withOverloadIndex(0);
+    expect(updated).not.toBe(symbol);
+    expect(updated.overloadIndex).toBe(0);
+  });
+  it('withSource()', () => {
+    const symbol = SYM({});
+    const source = ModuleSource.fromPackage('a');
+    const declref = symbol.withSource(source);
+    expect(declref.source).toBe(source);
+    expect(declref.symbol).toBe(symbol);
+  });
+  it('addNavigationStep()', () => {
+    const root = CROOT(CSTR('a'));
+    const component = CSTR('b');
+    const symbol = SYM({ componentPath: root });
+    const step = symbol.addNavigationStep(Navigation.Exports, component);
+    expect(step.componentPath).toBeInstanceOf(ComponentNavigation);
+    expect((step.componentPath as ComponentNavigation).parent).toBe(root);
+    expect((step.componentPath as ComponentNavigation).navigation).toBe(Navigation.Exports);
+    expect((step.componentPath as ComponentNavigation).component).toBe(component);
+  });
+  it('toDeclarationReference()', () => {
+    const root = CROOT(CSTR('a'));
+    const symbol = SYM({ componentPath: root });
+    const source = ModuleSource.fromPackage('b');
+    const declref = symbol.toDeclarationReference({
+      source,
+      navigation: Navigation.Exports
+    });
+    expect(declref.source).toBe(source);
+    expect(declref.navigation).toBe(Navigation.Exports);
+    expect(declref.symbol).toBe(symbol);
+  });
+});
+
+describe('ComponentPathBase', () => {
+  it('addNavigationStep()', () => {
+    const root = CROOT(CSTR('a'));
+    const component = CSTR('b');
+    const step = root.addNavigationStep(Navigation.Exports, component);
+    expect(step.parent).toBe(root);
+    expect(step.navigation).toBe(Navigation.Exports);
+    expect(step.component).toBe(component);
+  });
+  it('withMeaning()', () => {
+    const component = CROOT(CSTR('a'));
+    const symbol = component.withMeaning(Meaning.Variable);
+    expect(symbol.componentPath).toBe(component);
+    expect(symbol.meaning).toBe(Meaning.Variable);
+  });
+  it('withOverloadIndex()', () => {
+    const component = CROOT(CSTR('a'));
+    const symbol = component.withOverloadIndex(0);
+    expect(symbol.componentPath).toBe(component);
+    expect(symbol.overloadIndex).toBe(0);
+  });
+  it('withSource()', () => {
+    const component = CROOT(CSTR('a'));
+    const source = ModuleSource.fromPackage('b');
+    const declref = component.withSource(source);
+    expect(declref.source).toBe(source);
+    expect(declref.navigation).toBe(Navigation.Exports);
+    expect(declref.symbol?.componentPath).toBe(component);
+  });
+  it('toSymbolReference()', () => {
+    const component = CROOT(CSTR('a'));
+    const symbol = component.toSymbolReference({
+      meaning: Meaning.Variable,
+      overloadIndex: 0
+    });
+    expect(symbol.componentPath).toBe(component);
+    expect(symbol.meaning).toBe(Meaning.Variable);
+    expect(symbol.overloadIndex).toBe(0);
+  });
+  it('toDeclarationReference()', () => {
+    const component = CROOT(CSTR('a'));
+    const source = ModuleSource.fromPackage('b');
+    const declref = component.toDeclarationReference({
+      source,
+      navigation: Navigation.Exports,
+      meaning: Meaning.Variable,
+      overloadIndex: 0
+    });
+    expect(declref.source).toBe(source);
+    expect(declref.navigation).toBe(Navigation.Exports);
+    expect(declref.symbol?.componentPath).toBe(component);
+    expect(declref.symbol?.meaning).toBe(Meaning.Variable);
+    expect(declref.symbol?.overloadIndex).toBe(0);
+  });
+});
+
+describe('ComponentRoot', () => {
+  it('root', () => {
+    const component = new ComponentString('a');
+    const root = new ComponentRoot(component);
+    expect(root.root).toBe(root);
+  });
+  describe('static from()', () => {
+    it('static from({ component })', () => {
+      const component = Component.from('a');
+      const componentPath = ComponentRoot.from({ component });
+      expect(componentPath).toBeInstanceOf(ComponentRoot);
+      expect(componentPath.component).toBe(component);
+    });
+    it('static from(ComponentRoot)', () => {
+      const component = Component.from('a');
+      const root = new ComponentRoot(component);
+      const componentPath = ComponentRoot.from(root);
+      expect(componentPath).toBeInstanceOf(ComponentRoot);
+      expect(componentPath).toBe(root);
+    });
+  });
+  describe('with()', () => {
+    it('with({ })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const updated = root.with({});
+      expect(updated).toBe(root);
+    });
+    it('with({ component: <same> })', () => {
+      const component = Component.from('a');
+      const root = ComponentRoot.from({ component });
+      const updated = root.with({ component });
+      expect(updated).toBe(root);
+    });
+    it('with({ component: Component })', () => {
+      const component = Component.from('a');
+      const root = ComponentRoot.from({ component });
+      const newComponent = Component.from('b');
+      const updated = root.with({ component: newComponent });
+      expect(updated).not.toBe(root);
+      expect(updated.component).toBe(newComponent);
+    });
+    it('with({ component: DeclarationReference })', () => {
+      const component = Component.from('a');
+      const root = ComponentRoot.from({ component });
+      const reference = DeclarationReference.parse('b');
+      const updated = root.with({ component: reference });
+      expect(updated).not.toBe(root);
+      expect(updated.component).toBeInstanceOf(ComponentReference);
+      expect((updated.component as ComponentReference).reference).toBe(reference);
+    });
+    it('with({ component: string })', () => {
+      const component = Component.from('a');
+      const root = ComponentRoot.from({ component });
+      const updated = root.with({ component: 'b' });
+      expect(updated).not.toBe(root);
+      expect(updated.component).toBeInstanceOf(ComponentString);
+      expect((updated.component as ComponentString).text).toBe('b');
+    });
+  });
+  it.each`
+    left                | right                 | expected
+    ${undefined}        | ${undefined}          | ${true}
+    ${CROOT(CSTR('a'))} | ${undefined}          | ${false}
+    ${CROOT(CSTR('a'))} | ${CROOT(CSTR('a'))}   | ${true}
+    ${CROOT(CSTR('a'))} | ${CROOT(CSTR('b'))}   | ${false}
+    ${CROOT(CSTR('a'))} | ${CROOT(CREF('[a]'))} | ${false}
+  `('static equals(left, right) $#', ({ left, right, expected }) => {
+    expect(ComponentRoot.equals(left, right)).toBe(expected);
+    expect(ComponentRoot.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('ComponentNavigation', () => {
+  it('root', () => {
+    const root = ComponentRoot.from({ component: 'a' });
+    const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+    expect(step.root).toBe(root);
+  });
+  describe('static from()', () => {
+    it('static from(parts)', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      expect(step.parent).toBe(root);
+      expect(step.navigation).toBe(Navigation.Exports);
+      expect(step.component).toBeInstanceOf(ComponentString);
+      expect((step.component as ComponentString).text).toBe('b');
+    });
+    it('static from(ComponentNavigation)', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const result = ComponentNavigation.from(step);
+      expect(result).toBe(step);
+    });
+  });
+  describe('with()', () => {
+    it('with({ })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const updated = step.with({});
+      expect(updated).toBe(step);
+    });
+    it('with({ parent: <same> })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const updated = step.with({ parent: root });
+      expect(updated).toBe(step);
+    });
+    it('with({ parent: <different> })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const newRoot = ComponentRoot.from({ component: 'c' });
+      const updated = step.with({ parent: newRoot });
+      expect(updated).not.toBe(step);
+      expect(updated.parent).toBe(newRoot);
+    });
+    it('with({ navigation: <same> })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const updated = step.with({ navigation: Navigation.Exports });
+      expect(updated).toBe(step);
+    });
+    it('with({ navigation: <different> })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const updated = step.with({ navigation: Navigation.Members });
+      expect(updated).not.toBe(step);
+      expect(updated.navigation).toBe(Navigation.Members);
+    });
+    it('with({ component: <same> })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const component = Component.from('b');
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component });
+      const updated = step.with({ component });
+      expect(updated).toBe(step);
+    });
+    it('with({ component: new Component })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const component = Component.from('b');
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component });
+      const newComponent = Component.from('c');
+      const updated = step.with({ component: newComponent });
+      expect(updated).not.toBe(step);
+      expect(updated.component).toBe(newComponent);
+    });
+    it('with({ component: string })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const updated = step.with({ component: 'c' });
+      expect(updated).not.toBe(step);
+      expect(updated.component).toBeInstanceOf(ComponentString);
+      expect((updated.component as ComponentString).text).toBe('c');
+    });
+    it('with({ component: DeclarationReference })', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentNavigation.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const reference = DeclarationReference.parse('c');
+      const updated = step.with({ component: reference });
+      expect(updated).not.toBe(step);
+      expect(updated.component).toBeInstanceOf(ComponentReference);
+      expect((updated.component as ComponentReference).reference).toBe(reference);
+    });
+  });
+  it.each`
+    left                                        | right                                              | expected
+    ${undefined}                                | ${undefined}                                       | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${undefined}                                       | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}          | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', CSTR('b'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))}        | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', { text: 'a' })}      | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', { text: 'b' })}      | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', { reference: 'a' })} | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', DREF('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', 'a')}                | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', 'b')}                | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '#', CSTR('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))}        | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', CREF('[b]'))}        | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', { reference: 'a' })} | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', { reference: 'b' })} | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', { text: 'a' })}      | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', DREF('a'))}          | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', DREF('b'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', 'a')}                | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '#', CREF('[a]'))}        | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CREF('[a]')), '.', CSTR('a'))}        | ${false}
+  `('static equals(left, right) $#', ({ left, right, expected }) => {
+    expect(ComponentNavigation.equals(left, right)).toBe(expected);
+    expect(ComponentNavigation.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('ComponentPath', () => {
+  describe('static from()', () => {
+    it('from({ component })', () => {
+      const component = Component.from('a');
+      const componentPath = ComponentPath.from({ component });
+      expect(componentPath).toBeInstanceOf(ComponentRoot);
+      expect(componentPath.component).toBe(component);
+    });
+    it('from(ComponentRoot)', () => {
+      const component = Component.from('a');
+      const root = new ComponentRoot(component);
+      const componentPath = ComponentPath.from(root);
+      expect(componentPath).toBe(root);
+    });
+    it('from(string)', () => {
+      const componentPath = ComponentPath.from('a.b.[c]');
+      const pathABC = componentPath as ComponentNavigation;
+      const pathAB = pathABC?.parent as ComponentNavigation;
+      const pathA = pathAB?.parent as ComponentRoot;
+      expect(pathABC).toBeInstanceOf(ComponentNavigation);
+      expect(pathABC.component).toBeInstanceOf(ComponentReference);
+      expect(pathABC.component.toString()).toBe('[c]');
+      expect(pathAB).toBeInstanceOf(ComponentNavigation);
+      expect(pathAB.component).toBeInstanceOf(ComponentString);
+      expect(pathAB.component.toString()).toBe('b');
+      expect(pathA).toBeInstanceOf(ComponentRoot);
+      expect(pathA.component).toBeInstanceOf(ComponentString);
+      expect(pathA.component.toString()).toBe('a');
+    });
+    it('from(parts)', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentPath.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      expect(step).toBeInstanceOf(ComponentNavigation);
+      expect((step as ComponentNavigation).parent).toBe(root);
+      expect((step as ComponentNavigation).navigation).toBe(Navigation.Exports);
+      expect(step.component).toBeInstanceOf(ComponentString);
+      expect((step.component as ComponentString).text).toBe('b');
+    });
+    it('from(ComponentNavigation)', () => {
+      const root = ComponentRoot.from({ component: 'a' });
+      const step = ComponentPath.from({ parent: root, navigation: Navigation.Exports, component: 'b' });
+      const result = ComponentPath.from(step);
+      expect(result).toBe(step);
+    });
+  });
+  it.each`
+    left                                        | right                                              | expected
+    ${undefined}                                | ${undefined}                                       | ${true}
+    ${CROOT(CSTR('a'))}                         | ${undefined}                                       | ${false}
+    ${CROOT(CSTR('a'))}                         | ${CROOT(CSTR('a'))}                                | ${true}
+    ${CROOT(CSTR('a'))}                         | ${CROOT(CSTR('b'))}                                | ${false}
+    ${CROOT(CSTR('a'))}                         | ${CROOT(CREF('[a]'))}                              | ${false}
+    ${CROOT(CSTR('a'))}                         | ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}          | ${false}
+    ${undefined}                                | ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${undefined}                                       | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}          | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', CSTR('b'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))}        | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', { text: 'a' })}      | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', { text: 'b' })}      | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', { reference: 'a' })} | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', DREF('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', 'a')}                | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '.', 'b')}                | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CSTR('a')), '#', CSTR('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))}        | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', CREF('[b]'))}        | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', { reference: 'a' })} | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', { reference: 'b' })} | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', { text: 'a' })}      | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', DREF('a'))}          | ${true}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', DREF('b'))}          | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '.', 'a')}                | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CREF('[a]'))} | ${CNAV(CROOT(CSTR('a')), '#', CREF('[a]'))}        | ${false}
+    ${CNAV(CROOT(CSTR('a')), '.', CSTR('a'))}   | ${CNAV(CROOT(CREF('[a]')), '.', CSTR('a'))}        | ${false}
+  `('equals(left, right) $#', ({ left, right, expected }) => {
+    expect(ComponentPath.equals(left, right)).toBe(expected);
+    expect(ComponentPath.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('ComponentBase', () => {
+  it('toComponentPath()', () => {
+    const component = new ComponentString('a');
+    const componentPath = component.toComponentPath();
+    expect(componentPath).toBeInstanceOf(ComponentRoot);
+    expect(componentPath.component).toBe(component);
+  });
+  it('toComponentPath(parts)', () => {
+    const parent = new ComponentRoot(new ComponentString('a'));
+    const component = new ComponentString('b');
+    const componentPath = component.toComponentPath({ parent, navigation: Navigation.Exports });
+    expect(componentPath).toBeInstanceOf(ComponentNavigation);
+    expect(componentPath.component).toBe(component);
+    expect((componentPath as ComponentNavigation).parent).toBe(parent);
+    expect((componentPath as ComponentNavigation).navigation).toBe(Navigation.Exports);
+  });
+});
+
+describe('ComponentString', () => {
+  describe('static from()', () => {
+    it.each`
+      parts            | expected
+      ${''}            | ${'""'}
+      ${{ text: '' }}  | ${'""'}
+      ${'a'}           | ${'a'}
+      ${{ text: 'a' }} | ${'a'}
+      ${'['}           | ${'"["'}
+      ${{ text: '[' }} | ${'"["'}
+    `('static from($parts)', ({ parts, expected }) => {
+      const component = ComponentString.from(parts);
+      const actual = component.toString();
+      expect(actual).toBe(expected);
+    });
+    it('static from(ComponentString)', () => {
+      const component = new ComponentString('a');
+      const actual = ComponentString.from(component);
+      expect(actual).toBe(component);
+    });
+  });
+  it.each`
+    left         | right        | expected
+    ${undefined} | ${undefined} | ${true}
+    ${CSTR('a')} | ${undefined} | ${false}
+    ${CSTR('a')} | ${CSTR('a')} | ${true}
+    ${CSTR('a')} | ${CSTR('b')} | ${false}
+  `('static equals(left, right) $#', ({ left, right, expected }) => {
+    expect(ComponentString.equals(left, right)).toBe(expected);
+    expect(ComponentString.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('ComponentReference', () => {
+  describe('static from()', () => {
+    it.each`
+      parts                                             | expected
+      ${'[a]'}                                          | ${'[a]'}
+      ${DeclarationReference.parse('a')}                | ${'[a]'}
+      ${{ reference: 'a' }}                             | ${'[a]'}
+      ${{ reference: DeclarationReference.parse('a') }} | ${'[a]'}
+    `('static from($parts)', ({ parts, expected }) => {
+      const component = ComponentReference.from(parts);
+      const actual = component.toString();
+      expect(actual).toBe(expected);
+    });
+  });
+  describe('with()', () => {
+    it('with({ })', () => {
+      const component = ComponentReference.parse('[a]');
+      const updated = component.with({});
+      expect(updated).toBe(component);
+    });
+    it('with({ reference: same DeclarationReference })', () => {
+      const component = ComponentReference.parse('[a]');
+      const updated = component.with({ reference: component.reference });
+      expect(updated).toBe(component);
+    });
+    it('with({ reference: equivalent DeclarationReference })', () => {
+      const component = ComponentReference.parse('[a]');
+      const updated = component.with({ reference: DeclarationReference.parse('a') });
+      expect(updated).toBe(component);
+    });
+    it('with({ reference: equivalent string })', () => {
+      const component = ComponentReference.parse('[a]');
+      const updated = component.with({ reference: 'a' });
+      expect(updated).toBe(component);
+    });
+    it('with({ reference: different DeclarationReference })', () => {
+      const reference = DeclarationReference.parse('a');
+      const component = new ComponentReference(reference);
+      const newReference = DeclarationReference.parse('b');
+      const updated = component.with({ reference: newReference });
+      expect(updated).not.toBe(component);
+      expect(updated.reference).toBe(newReference);
+    });
+    it('with({ reference: different string })', () => {
+      const reference = DeclarationReference.parse('a');
+      const component = new ComponentReference(reference);
+      const updated = component.with({ reference: 'b' });
+      expect(updated).not.toBe(component);
+      expect(updated.reference).not.toBe(reference);
+      expect(updated.reference.toString()).toBe('b');
+    });
+  });
+  it.each`
+    left           | right                             | expected
+    ${undefined}   | ${undefined}                      | ${true}
+    ${CREF('[a]')} | ${undefined}                      | ${false}
+    ${CREF('[a]')} | ${CREF('[a]')}                    | ${true}
+    ${CREF('[a]')} | ${CREF('[b]')}                    | ${false}
+    ${CREF('[a]')} | ${CREF({ reference: 'a' })}       | ${true}
+    ${CREF('[a]')} | ${CREF({ reference: 'b' })}       | ${false}
+    ${CREF('[a]')} | ${CREF({ reference: DREF('a') })} | ${true}
+    ${CREF('[a]')} | ${CREF({ reference: DREF('b') })} | ${false}
+  `('static equals(left, right) $#', ({ left, right, expected }) => {
+    expect(ComponentReference.equals(left, right)).toBe(expected);
+    expect(ComponentReference.equals(right, left)).toBe(expected);
+  });
+});
+
+describe('Component', () => {
+  describe('static from()', () => {
+    it('from({ text: string })', () => {
+      const component = Component.from({ text: 'a' });
+      expect(component).toBeInstanceOf(ComponentString);
+      expect(component.toString()).toBe('a');
+    });
+    it('from({ reference: string })', () => {
+      const component = Component.from({ reference: 'a' });
+      expect(component).toBeInstanceOf(ComponentReference);
+      expect(component.toString()).toBe('[a]');
+    });
+    it('from({ reference: DeclarationReference })', () => {
+      const reference = DeclarationReference.parse('a');
+      const component = Component.from({ reference });
+      expect(component).toBeInstanceOf(ComponentReference);
+      expect((component as ComponentReference).reference).toBe(reference);
+    });
+    it('from(string)', () => {
+      const component = Component.from('a');
+      expect(component).toBeInstanceOf(ComponentString);
+      expect(component.toString()).toBe('a');
+    });
+    it('from(DeclarationReference)', () => {
+      const reference = DeclarationReference.parse('a');
+      const component = Component.from(reference);
+      expect(component).toBeInstanceOf(ComponentReference);
+      expect((component as ComponentReference).reference).toBe(reference);
+    });
+    it('from(Component)', () => {
+      const component = new ComponentString('a');
+      const result = Component.from(component);
+      expect(result).toBe(component);
+    });
+  });
+  it.each`
+    left           | right                             | expected
+    ${undefined}   | ${undefined}                      | ${true}
+    ${CSTR('a')}   | ${undefined}                      | ${false}
+    ${CSTR('a')}   | ${CREF('[a]')}                    | ${false}
+    ${CSTR('a')}   | ${CSTR('a')}                      | ${true}
+    ${CSTR('a')}   | ${CSTR('b')}                      | ${false}
+    ${CREF('[a]')} | ${undefined}                      | ${false}
+    ${CREF('[a]')} | ${CREF('[a]')}                    | ${true}
+    ${CREF('[a]')} | ${CREF('[b]')}                    | ${false}
+    ${CREF('[a]')} | ${CREF({ reference: 'a' })}       | ${true}
+    ${CREF('[a]')} | ${CREF({ reference: 'b' })}       | ${false}
+    ${CREF('[a]')} | ${CREF({ reference: DREF('a') })} | ${true}
+    ${CREF('[a]')} | ${CREF({ reference: DREF('b') })} | ${false}
+  `('equals(left, right) $#', ({ left, right, expected }) => {
+    expect(Component.equals(left, right)).toBe(expected);
+    expect(Component.equals(right, left)).toBe(expected);
+  });
 });
diff --git a/tsdoc/src/parser/StringChecks.ts b/tsdoc/src/parser/StringChecks.ts
index 22e95603..3bbe73bc 100644
--- a/tsdoc/src/parser/StringChecks.ts
+++ b/tsdoc/src/parser/StringChecks.ts
@@ -132,6 +132,44 @@ export class StringChecks {
     return undefined;
   }
 
+  /**
+   * Tests whether the input string is a valid scope portion of a scoped NPM package name.
+   */
+  public static explainIfInvalidPackageScope(scopeName: string): string | undefined {
+    if (scopeName.length === 0) {
+      return 'An package scope cannot be an empty string';
+    }
+
+    if (scopeName.charAt(0) !== '@') {
+      return `An package scope must start with '@'`;
+    }
+
+    if (!StringChecks._validPackageNameRegExp.test(`${scopeName}/package`)) {
+      return `The name ${JSON.stringify(scopeName)} is not a valid package scope`;
+    }
+
+    return undefined;
+  }
+
+  /**
+   * Tests whether the input string is a valid non-scope portion of a scoped NPM package name.
+   */
+  public static explainIfInvalidUnscopedPackageName(unscopedPackageName: string): string | undefined {
+    if (unscopedPackageName.length === 0) {
+      return 'An unscoped package name cannot be an empty string';
+    }
+
+    if (unscopedPackageName.charAt(0) === '@') {
+      return `An unscoped package name cannot start with '@'`;
+    }
+
+    if (!StringChecks._validPackageNameRegExp.test(`@scope/${unscopedPackageName}`)) {
+      return `The name ${JSON.stringify(unscopedPackageName)} is not a valid unscoped package name`;
+    }
+
+    return undefined;
+  }
+
   /**
    * Tests whether the input string is a valid declaration reference import path.
    */

From cd81fc958b634e20aa68878a69fc5f65bb91c717 Mon Sep 17 00:00:00 2001
From: Ron Buckton <ron.buckton@microsoft.com>
Date: Sun, 15 May 2022 23:55:56 -0700
Subject: [PATCH 2/2] Allow parsing of 'beta' DeclarationReference when parsing
 a DocDeclarationReference

---
 .../parse-beta-declref_2022-05-16-07-04.json  |  11 +
 tsdoc/etc/tsdoc.api.md                        |  29 +-
 tsdoc/src/beta/DeclarationReference.ts        | 797 +++++++++++++++---
 .../__tests__/DeclarationReference.test.ts    |  17 +
 tsdoc/src/configuration/TSDocConfiguration.ts |   9 +
 tsdoc/src/emitters/TSDocEmitter.ts            |  20 +-
 tsdoc/src/nodes/DocDeclarationReference.ts    | 142 +++-
 tsdoc/src/nodes/DocExcerpt.ts                 |   1 +
 tsdoc/src/parser/NodeParser.ts                | 115 ++-
 tsdoc/src/parser/ParserMessageLog.ts          |  16 +
 tsdoc/src/parser/TokenReader.ts               |  25 +
 .../__tests__/NodeParserLinkTag.test.ts       |  16 +
 .../NodeParserLinkTag.test.ts.snap            | 198 +++++
 13 files changed, 1219 insertions(+), 177 deletions(-)
 create mode 100644 common/changes/@microsoft/tsdoc/parse-beta-declref_2022-05-16-07-04.json

diff --git a/common/changes/@microsoft/tsdoc/parse-beta-declref_2022-05-16-07-04.json b/common/changes/@microsoft/tsdoc/parse-beta-declref_2022-05-16-07-04.json
new file mode 100644
index 00000000..adf1b168
--- /dev/null
+++ b/common/changes/@microsoft/tsdoc/parse-beta-declref_2022-05-16-07-04.json
@@ -0,0 +1,11 @@
+{
+  "changes": [
+    {
+      "packageName": "@microsoft/tsdoc",
+      "comment": "parse beta DeclarationReference in TSDoc",
+      "type": "minor"
+    }
+  ],
+  "packageName": "@microsoft/tsdoc",
+  "email": "ron.buckton@microsoft.com"
+}
\ No newline at end of file
diff --git a/tsdoc/etc/tsdoc.api.md b/tsdoc/etc/tsdoc.api.md
index d21fb58d..96e1815d 100644
--- a/tsdoc/etc/tsdoc.api.md
+++ b/tsdoc/etc/tsdoc.api.md
@@ -69,7 +69,11 @@ export class DocComment extends DocNode {
 // @public
 export class DocDeclarationReference extends DocNode {
     // @internal
-    constructor(parameters: IDocDeclarationReferenceParameters | IDocDeclarationReferenceParsedParameters);
+    constructor(parameters: IDocDeclarationReferenceParameters | IDocDeclarationReferenceParsedParameters | IBetaDocDeclarationReferenceParameters | IBetaDocDeclarationReferenceParsedParameters);
+    // Warning: (ae-forgotten-export) The symbol "DeclarationReference" needs to be exported by the entry point index.d.ts
+    //
+    // @beta
+    get declarationReference(): DeclarationReference | undefined;
     emitAsTsdoc(): string;
     get importPath(): string | undefined;
     // @override (undocumented)
@@ -450,6 +454,8 @@ export enum ExcerptKind {
     // (undocumented)
     CodeSpan_OpeningDelimiter = "CodeSpan_OpeningDelimiter",
     // (undocumented)
+    DeclarationReference_DeclarationReference = "DeclarationReference_DeclarationReference",
+    // (undocumented)
     DeclarationReference_ImportHash = "DeclarationReference_ImportHash",
     // (undocumented)
     DeclarationReference_ImportPath = "DeclarationReference_ImportPath",
@@ -531,6 +537,20 @@ export enum ExcerptKind {
     Spacing = "Spacing"
 }
 
+// @beta
+export interface IBetaDocDeclarationReferenceParameters extends IDocNodeParameters {
+    // (undocumented)
+    declarationReference: DeclarationReference;
+}
+
+// @beta
+export interface IBetaDocDeclarationReferenceParsedParameters extends IDocNodeParsedParameters {
+    // (undocumented)
+    declarationReference?: DeclarationReference;
+    // (undocumented)
+    declarationReferenceExcerpt: TokenSequence;
+}
+
 // @public
 export interface IDocBlockParameters extends IDocNodeParameters {
     // (undocumented)
@@ -1085,8 +1105,10 @@ export class ParserMessageLog {
     addMessageForDocErrorText(docErrorText: DocErrorText): void;
     addMessageForTextRange(messageId: TSDocMessageId, messageText: string, textRange: TextRange): void;
     addMessageForTokenSequence(messageId: TSDocMessageId, messageText: string, tokenSequence: TokenSequence, docNode?: DocNode): void;
+    createMarker(): number;
     get messages(): ReadonlyArray<ParserMessage>;
-    }
+    rollbackToMarker(marker: number): void;
+}
 
 // @public
 export class PlainTextEmitter {
@@ -1248,6 +1270,9 @@ export class TSDocConfiguration {
     isHtmlElementSupported(htmlTag: string): boolean;
     isKnownMessageId(messageId: TSDocMessageId | string): boolean;
     isTagSupported(tagDefinition: TSDocTagDefinition): boolean;
+    // (undocumented)
+    get parseBetaDeclarationReferences(): boolean | 'prefer';
+    set parseBetaDeclarationReferences(value: boolean | 'prefer');
     setSupportedHtmlElements(htmlTags: string[]): void;
     setSupportForTag(tagDefinition: TSDocTagDefinition, supported: boolean): void;
     setSupportForTags(tagDefinitions: ReadonlyArray<TSDocTagDefinition>, supported: boolean): void;
diff --git a/tsdoc/src/beta/DeclarationReference.ts b/tsdoc/src/beta/DeclarationReference.ts
index a4fe0d97..b7b706db 100644
--- a/tsdoc/src/beta/DeclarationReference.ts
+++ b/tsdoc/src/beta/DeclarationReference.ts
@@ -11,7 +11,17 @@
 // NOTE: @rushstack/no-new-null is disabled for places where `null` is used as a sentinel to
 //       indicate explicit non-presence of a value (such as when removing values using `.with()`).
 
+import { TSDocConfiguration } from '../configuration/TSDocConfiguration';
+import {
+  DocDeclarationReference,
+  DocMemberIdentifier,
+  DocMemberReference,
+  DocMemberSelector,
+  DocMemberSymbol
+} from '../nodes';
 import { StringChecks } from '../parser/StringChecks';
+import { TokenKind, Token as DocToken } from '../parser/Token';
+import { TokenReader } from '../parser/TokenReader';
 
 // #region DeclarationReference
 
@@ -61,16 +71,39 @@ export class DeclarationReference {
   /**
    * Parses a {@link DeclarationReference} from the provided text.
    */
-  public static parse(text: string): DeclarationReference {
-    const parser: Parser = new Parser(text);
+  public static parse(source: string): DeclarationReference {
+    const parser: Parser = new Parser(new TextReader(source));
     const reference: DeclarationReference = parser.parseDeclarationReference();
     if (parser.errors.length) {
-      throw new SyntaxError(`Invalid DeclarationReference '${text}':\n  ${parser.errors.join('\n  ')}`);
+      throw new SyntaxError(`Invalid DeclarationReference '${source}':\n  ${parser.errors.join('\n  ')}`);
     } else if (!parser.eof) {
-      throw new SyntaxError(`Invalid DeclarationReference '${text}'`);
-    } else {
-      return reference;
+      throw new SyntaxError(`Invalid DeclarationReference '${source}'`);
     }
+    return reference;
+  }
+
+  /**
+   * Parses a {@link DeclarationReference} from the provided text.
+   */
+  public static tryParse(source: string): DeclarationReference | undefined;
+  /**
+   * Parses a {@link DeclarationReference} from the provided text.
+   * @internal
+   */
+  public static tryParse(source: TokenReader, fallback?: boolean): DeclarationReference | undefined;
+  public static tryParse(source: string | TokenReader, fallback?: boolean): DeclarationReference | undefined {
+    const marker: number | undefined = typeof source === 'string' ? undefined : source.createMarker();
+    const reader: ICharacterReader =
+      typeof source === 'string' ? new TextReader(source) : new TokenReaderNormalizer(source);
+    const parser: Parser = new Parser(reader, fallback);
+    const reference: DeclarationReference = parser.parseDeclarationReference();
+    if (parser.errors.length || (!parser.eof && typeof source === 'string')) {
+      if (marker !== undefined && typeof source !== 'string') {
+        source.backtrackToMarker(marker);
+      }
+      return undefined;
+    }
+    return reference;
   }
 
   /**
@@ -88,7 +121,7 @@ export class DeclarationReference {
    * Determines whether the provided string is a well-formed symbol navigation component string.
    */
   public static isWellFormedComponentString(text: string): boolean {
-    const scanner: Scanner = new Scanner(text);
+    const scanner: Scanner = new Scanner(new TextReader(text));
     return scanner.scan() === Token.String
       ? scanner.scan() === Token.EofToken
       : scanner.token() === Token.Text
@@ -135,7 +168,7 @@ export class DeclarationReference {
    * have a trailing `!` character.
    */
   public static isWellFormedModuleSourceString(text: string): boolean {
-    const scanner: Scanner = new Scanner(text + '!');
+    const scanner: Scanner = new Scanner(new TextReader(text + '!'));
     return (
       scanner.rescanModuleSource() === Token.ModuleSource &&
       !scanner.stringIsUnterminated &&
@@ -179,16 +212,20 @@ export class DeclarationReference {
 
   /**
    * Returns an empty {@link DeclarationReference}.
+   *
+   * An alias for `DeclarationReference.from({ })`.
    */
   public static empty(): DeclarationReference {
-    return new DeclarationReference();
+    return DeclarationReference.from({});
   }
 
   /**
    * Creates a new {@link DeclarationReference} for the provided package.
+   *
+   * An alias for `Declaration.from({ packageName, importPath })`.
    */
   public static package(packageName: string, importPath?: string): DeclarationReference {
-    return new DeclarationReference(ModuleSource.fromPackage(packageName, importPath));
+    return DeclarationReference.from({ packageName, importPath });
   }
 
   /**
@@ -209,11 +246,13 @@ export class DeclarationReference {
    * Creates a new {@link DeclarationReference} from the provided parts.
    */
   public static from(parts: DeclarationReferenceLike</*With*/ false> | undefined): DeclarationReference {
-    const resolved: ResolvedDeclarationReferenceLike = resolveDeclarationReferenceLike(
+    const resolved: ResolvedDeclarationReferenceLike | undefined = resolveDeclarationReferenceLike(
       parts,
       /*fallbackReference*/ undefined
     );
-    if (resolved instanceof DeclarationReference) {
+    if (resolved === undefined) {
+      return new DeclarationReference();
+    } else if (resolved instanceof DeclarationReference) {
       return resolved;
     } else {
       const { source, navigation, symbol } = resolved;
@@ -238,18 +277,22 @@ export class DeclarationReference {
       this.navigation,
       this.symbol
     );
+
     const resolvedSource: Source | undefined = source === undefined ? undefined : Source.from(source);
+
     const resolvedSymbol: SymbolReference | undefined =
       symbol === undefined ? undefined : SymbolReference.from(symbol);
+
     const resolvedNavigation: SourceNavigation | undefined = resolveNavigation(
       resolvedSource,
       resolvedSymbol,
       navigation
     );
+
     if (
       Source.equals(this.source, resolvedSource) &&
-      this.navigation === resolvedNavigation &&
-      SymbolReference.equals(this.symbol, resolvedSymbol)
+      SymbolReference.equals(this.symbol, resolvedSymbol) &&
+      this.navigation === resolvedNavigation
     ) {
       return this;
     } else {
@@ -259,6 +302,9 @@ export class DeclarationReference {
 
   /**
    * Returns an {@link DeclarationReference} updated with the provided source.
+   *
+   * An alias for `declref.with({ source: source ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided source.
    */
   public withSource(source: Source | undefined): DeclarationReference {
@@ -267,6 +313,9 @@ export class DeclarationReference {
 
   /**
    * Returns an {@link DeclarationReference} updated with the provided navigation.
+   *
+   * An alias for `declref.with({ navigation: navigation ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided navigation.
    */
   public withNavigation(navigation: SourceNavigation | undefined): DeclarationReference {
@@ -275,6 +324,9 @@ export class DeclarationReference {
 
   /**
    * Returns an {@link DeclarationReference} updated with the provided symbol.
+   *
+   * An alias for `declref.with({ symbol: symbol ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided symbol.
    */
   public withSymbol(symbol: SymbolReference | undefined): DeclarationReference {
@@ -283,6 +335,9 @@ export class DeclarationReference {
 
   /**
    * Returns an {@link DeclarationReference} whose symbol has been updated with the provided component path.
+   *
+   * An alias for `declref.with({ componentPath: componentPath ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided component path.
    */
   public withComponentPath(componentPath: ComponentPath | undefined): DeclarationReference {
@@ -291,6 +346,9 @@ export class DeclarationReference {
 
   /**
    * Returns an {@link DeclarationReference} whose symbol has been updated with the provided meaning.
+   *
+   * An alias for `declref.with({ meaning: meaning ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided meaning.
    */
   public withMeaning(meaning: Meaning | undefined): DeclarationReference {
@@ -299,6 +357,9 @@ export class DeclarationReference {
 
   /**
    * Returns an {@link DeclarationReference} whose symbol has been updated with the provided overload index.
+   *
+   * An alias for `declref.with({ overloadIndex: overloadIndex ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided overload index.
    */
   public withOverloadIndex(overloadIndex: number | undefined): DeclarationReference {
@@ -333,10 +394,8 @@ export class DeclarationReference {
     left: DeclarationReference | undefined,
     right: DeclarationReference | undefined
   ): boolean {
-    if (left === undefined) {
-      return right === undefined || right.isEmpty;
-    } else if (right === undefined) {
-      return left === undefined || left.isEmpty;
+    if (left === undefined || right === undefined) {
+      return left === right;
     } else {
       return left.toString() === right.toString();
     }
@@ -361,6 +420,11 @@ export class DeclarationReference {
 // #region DeclarationReferenceParts
 
 /**
+ * A part that can be used to compose or update a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if this part is used by `with()` (which allows `null` for some parts), `false` if this part is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceSourcePart<With extends boolean> = Parts<
@@ -372,6 +436,10 @@ export type DeclarationReferenceSourcePart<With extends boolean> = Parts<
 >;
 
 /**
+ * Parts that can be used to compose or update a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceSourceParts<With extends boolean> =
@@ -379,6 +447,10 @@ export type DeclarationReferenceSourceParts<With extends boolean> =
   | SourceParts<With>;
 
 /**
+ * Parts that can be used to compose or update a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceNavigationParts<With extends boolean> = Parts<
@@ -390,6 +462,11 @@ export type DeclarationReferenceNavigationParts<With extends boolean> = Parts<
 >;
 
 /**
+ * A part that can be used to compose or update a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if this part is used by `with()` (which allows `null` for some parts), `false` if this part is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceSymbolPart<With extends boolean> = Parts<
@@ -401,6 +478,10 @@ export type DeclarationReferenceSymbolPart<With extends boolean> = Parts<
 >;
 
 /**
+ * Parts that can be used to compose or update a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceSymbolParts<With extends boolean> =
@@ -408,16 +489,24 @@ export type DeclarationReferenceSymbolParts<With extends boolean> =
   | SymbolReferenceParts<With>;
 
 /**
+ * Parts that can be used to compose or update a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceParts<With extends boolean> = DeclarationReferenceSourceParts<With> &
   DeclarationReferenceNavigationParts<With> &
   DeclarationReferenceSymbolParts<With>;
 
-function resolveDeclarationReferenceSourcePart(
+function resolveDeclarationReferenceSource(
   parts: DeclarationReferenceSourceParts</*With*/ true>,
   fallbackSource: Source | undefined
 ): SourceLike</*With*/ false> | undefined {
+  // If `source` is neither `null` or `undefined`, returns the resolved source-like.
+  // If `source` is `null`, returns `undefined` (which removes `source` from the updated DeclarationReference).
+  // If `packageName`, `scopeName`, `unscopedPackageName`, or `importPath` are present, returns the resolved `ModuleSourceParts` for those properties.
+  // If `source` is `undefined`, assumes no change and returns `fallbackSource`.
   const { source, packageName, scopeName, unscopedPackageName, importPath } = parts as AllParts<typeof parts>;
   if (source !== undefined) {
     if (packageName !== undefined) {
@@ -449,10 +538,13 @@ function resolveDeclarationReferenceSourcePart(
   }
 }
 
-function resolveDeclarationReferenceNavigationPart(
+function resolveDeclarationReferenceNavigation(
   parts: DeclarationReferenceNavigationParts</*With*/ true>,
   fallbackNavigation: SourceNavigation | undefined
 ): SourceNavigation | undefined {
+  // If `navigation` is neither `null` nor `undefined`, returns `navigation`.
+  // If `navigation` is `null`, returns `undefined` (which removes `navigation` from the updated DeclarationReference).
+  // If `navigation` is `undeifned`, returns `fallbackNavigation`.
   const { navigation } = parts;
   if (navigation !== undefined) {
     if (navigation === null) {
@@ -465,10 +557,14 @@ function resolveDeclarationReferenceNavigationPart(
   }
 }
 
-function resolveDeclarationReferenceSymbolPart(
+function resolveDeclarationReferenceSymbol(
   parts: DeclarationReferenceSymbolParts</*With*/ true>,
   fallbackSymbol: SymbolReference | undefined
 ): SymbolReferenceLike</*With*/ false> | undefined {
+  // If `symbol` is neither `null` or `undefined`, returns the resolved symbol-reference-like.
+  // If `symbol` is `null`, returns `undefined` (which removes `symbol` from the updated DeclarationReference).
+  // If `componentPath`, `meaning`, or `overloadIndex` are present, returns the resolved `SymbolReferenceParts` for those properties.
+  // If `symbol` is `undefined`, assumes no change and returns `fallbackSymbol`.
   const { symbol, componentPath, meaning, overloadIndex } = parts as AllParts<typeof parts>;
   if (symbol !== undefined) {
     if (componentPath !== undefined) {
@@ -484,12 +580,7 @@ function resolveDeclarationReferenceSymbolPart(
       return resolveSymbolReferenceLike(symbol, fallbackSymbol);
     }
   } else if (componentPath !== undefined || meaning !== undefined || overloadIndex !== undefined) {
-    return resolveSymbolReferenceParts(
-      parts as SymbolReferenceParts</*With*/ true>,
-      fallbackSymbol?.componentPath,
-      fallbackSymbol?.meaning,
-      fallbackSymbol?.overloadIndex
-    );
+    return resolveSymbolReferenceLike(parts as SymbolReferenceParts</*With*/ true>, fallbackSymbol);
   } else {
     return fallbackSymbol;
   }
@@ -506,15 +597,20 @@ function resolveDeclarationReferenceParts(
   fallbackSymbol: SymbolReference | undefined
 ): ResolvedDeclarationReferenceParts {
   return {
-    source: resolveDeclarationReferenceSourcePart(parts, fallbackSource),
-    navigation: resolveDeclarationReferenceNavigationPart(parts, fallbackNavigation),
-    symbol: resolveDeclarationReferenceSymbolPart(parts, fallbackSymbol)
+    source: resolveDeclarationReferenceSource(parts, fallbackSource),
+    navigation: resolveDeclarationReferenceNavigation(parts, fallbackNavigation),
+    symbol: resolveDeclarationReferenceSymbol(parts, fallbackSymbol)
   };
 }
 
 // #endregion DeclarationReferenceParts
 
 /**
+ * A value that can be resolved to a {@link DeclarationReference}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type DeclarationReferenceLike<With extends boolean> =
@@ -527,11 +623,11 @@ type ResolvedDeclarationReferenceLike = DeclarationReference | ResolvedDeclarati
 function resolveDeclarationReferenceLike(
   reference: DeclarationReferenceLike</*With*/ true> | undefined,
   fallbackReference: DeclarationReference | undefined
-): ResolvedDeclarationReferenceLike {
-  if (reference instanceof DeclarationReference) {
+): ResolvedDeclarationReferenceLike | undefined {
+  if (reference === undefined) {
+    return undefined;
+  } else if (reference instanceof DeclarationReference) {
     return reference;
-  } else if (reference === undefined) {
-    return DeclarationReference.empty();
   } else if (typeof reference === 'string') {
     return DeclarationReference.parse(reference);
   } else {
@@ -626,12 +722,18 @@ export class ModuleSource extends SourceBase {
   private _pathComponents: IParsedPackage | undefined;
   private _packageName: string | undefined;
 
+  /**
+   * @param path The module source path, including the package name.
+   * @param userEscaped If `false`, escapes `path` if needed. If `true` (default), validates `path` is already escaped.
+   */
   public constructor(path: string, userEscaped: boolean = true) {
     super();
     this._escapedPath = escapeModuleSourceIfNeeded(path, this instanceof ParsedModuleSource, userEscaped);
   }
 
-  /** A canonically escaped module source string. */
+  /**
+   * A canonically escaped module source string.
+   */
   public get escapedPath(): string {
     return this._escapedPath;
   }
@@ -664,7 +766,7 @@ export class ModuleSource extends SourceBase {
   }
 
   /**
-   * Returns the non-scope portion of a scoped package name (i.e., `package` in `@scope/package`)
+   * Returns the non-scope portion of a scoped package name (i.e., `package` in `@scope/package`, or `typescript` in `typescript`).
    */
   public get unscopedPackageName(): string {
     return this._getOrParsePathComponents().unscopedPackageName;
@@ -680,7 +782,7 @@ export class ModuleSource extends SourceBase {
   /**
    * Creates a new {@link ModuleSource} from the supplied parts.
    */
-  public static from(parts: ModuleSourceLike</*With*/ false> | string): ModuleSource {
+  public static from(parts: ModuleSourceLike</*With*/ false>): ModuleSource {
     const resolved: ResolvedModuleSourceLike = resolveModuleSourceLike(parts, /*fallbackSource*/ undefined);
     if (resolved instanceof ModuleSource) {
       return resolved;
@@ -695,6 +797,8 @@ export class ModuleSource extends SourceBase {
 
   /**
    * Creates a new {@link ModuleSource} for a scoped package.
+   *
+   * An alias for `ModuleSource.from({ scopeName, unscopedPackageName, importPath })`.
    */
   public static fromScopedPackage(
     scopeName: string | undefined,
@@ -819,7 +923,6 @@ interface IParsedPackage {
 }
 
 function parsePackageName(text: string): IParsedPackage {
-  // eslint-disable-next-line @rushstack/no-new-null
   const parsed: IParsedPackage | null = tryParsePackageName(text);
   if (!parsed) {
     throw new SyntaxError(`Invalid NPM package name: The package name ${JSON.stringify(text)} was invalid`);
@@ -839,7 +942,6 @@ function parsePackageName(text: string): IParsedPackage {
   return parsed;
 }
 
-// eslint-disable-next-line @rushstack/no-new-null
 function tryParsePackageName(text: string): IParsedPackage | null {
   const match: RegExpExecArray | null = packageNameRegExp.exec(text);
   if (!match) {
@@ -880,7 +982,10 @@ function formatModuleSource(
 }
 
 /**
- * Specifies the parts that can be used to construct or update a {@link ModuleSource}.
+ * Parts that can be used to compose or update a {@link ModuleSource}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ModuleSourceParts<With extends boolean> = Parts<
@@ -1039,6 +1144,11 @@ function resolveModuleSourceParts(
 }
 
 /**
+ * A value that can be resolved to a {@link ModuleSource}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ModuleSourceLike<With extends boolean> = ModuleSourceParts<With> | ModuleSource | string;
@@ -1106,6 +1216,10 @@ export namespace Source {
 }
 
 /**
+ * Parts that can be used to compose or update a {@link Source}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type SourceParts<With extends boolean> = ModuleSourceParts<With>;
@@ -1120,6 +1234,11 @@ function resolveSourceParts(
 }
 
 /**
+ * A value that can be resolved to a {@link Source}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type SourceLike<With extends boolean> = GlobalSource | ModuleSourceLike<With>;
@@ -1159,10 +1278,13 @@ export class SymbolReference {
     { meaning, overloadIndex }: Pick<SymbolReferenceParts</*With*/ false>, 'meaning' | 'overloadIndex'> = {}
   ) {
     this.componentPath = component;
-    this.overloadIndex = overloadIndex;
     this.meaning = meaning;
+    this.overloadIndex = overloadIndex;
   }
 
+  /**
+   * Gets whether this reference does not contain a `componentPath`, `meaning`, or `overloadIndex`.
+   */
   public get isEmpty(): boolean {
     return this.componentPath === undefined && this.overloadIndex === undefined && this.meaning === undefined;
   }
@@ -1178,7 +1300,7 @@ export class SymbolReference {
    * Parses a {@link SymbolReference} from the supplied text.
    */
   public static parse(text: string): SymbolReference {
-    const parser: Parser = new Parser(text);
+    const parser: Parser = new Parser(new TextReader(text));
     const symbol: SymbolReference | undefined = parser.tryParseSymbolReference();
     if (parser.errors.length) {
       throw new SyntaxError(`Invalid SymbolReference '${text}':\n  ${parser.errors.join('\n  ')}`);
@@ -1189,15 +1311,29 @@ export class SymbolReference {
     }
   }
 
+  /**
+   * Attempts to parse a {@link SymbolReference} from the supplied text. Returns `undefined` if parsing
+   * fails rather than throwing an error.
+   */
+  public static tryParse(text: string): SymbolReference | undefined {
+    const parser: Parser = new Parser(new TextReader(text));
+    const symbol: SymbolReference | undefined = parser.tryParseSymbolReference();
+    if (!parser.errors.length && parser.eof) {
+      return symbol;
+    }
+  }
+
   /**
    * Creates a new {@link SymbolReference} from the provided parts.
    */
   public static from(parts: SymbolReferenceLike</*With*/ false> | undefined): SymbolReference {
-    const resolved: ResolvedSymbolReferenceLike = resolveSymbolReferenceLike(
+    const resolved: ResolvedSymbolReferenceLike | undefined = resolveSymbolReferenceLike(
       parts,
       /*fallbackSymbol*/ undefined
     );
-    if (typeof resolved === 'string') {
+    if (resolved === undefined) {
+      return new SymbolReference(/*component*/ undefined);
+    } else if (typeof resolved === 'string') {
       return SymbolReference.parse(resolved);
     } else if (resolved instanceof SymbolReference) {
       return resolved;
@@ -1238,6 +1374,9 @@ export class SymbolReference {
 
   /**
    * Gets a {@link SymbolReference} updated with the provided component path.
+   *
+   * An alias for `symbol.with({ componentPath: componentPath ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided component path.
    */
   public withComponentPath(componentPath: ComponentPath | undefined): SymbolReference {
@@ -1246,6 +1385,9 @@ export class SymbolReference {
 
   /**
    * Gets a {@link SymbolReference} updated with the provided meaning.
+   *
+   * An alias for `symbol.with({ meaning: meaning ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided meaning.
    */
   public withMeaning(meaning: Meaning | undefined): SymbolReference {
@@ -1254,18 +1396,26 @@ export class SymbolReference {
 
   /**
    * Gets a {@link SymbolReference} updated with the provided overload index.
+   *
+   * An alias for `symbol.with({ overloadIndex: overloadIndex ?? null })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided overload index.
    */
   public withOverloadIndex(overloadIndex: number | undefined): SymbolReference {
     return this.with({ overloadIndex: overloadIndex ?? null });
   }
 
+  /**
+   * Combines this {@link SymbolReference} with the provided {@link Source} to create a {@link DeclarationReference}.
+   *
+   * An alias for `symbol.toDeclarationReference({ source })`.
+   */
   public withSource(source: Source | undefined): DeclarationReference {
     return this.toDeclarationReference({ source });
   }
 
   /**
-   * Creates a new {@link SymbolReference} that navigates from this SymbolReference to the provided component.
+   * Creates a new {@link SymbolReference} that navigates from this {@link SymbolReference} to the provided {@link Component}.
    */
   public addNavigationStep(
     navigation: Navigation,
@@ -1281,10 +1431,8 @@ export class SymbolReference {
    * Tests whether two {@link SymbolReference} values are equivalent.
    */
   public static equals(left: SymbolReference | undefined, right: SymbolReference | undefined): boolean {
-    if (left === undefined) {
-      return right === undefined || right.isEmpty;
-    } else if (right === undefined) {
-      return left === undefined || left.isEmpty;
+    if (left === undefined || right === undefined) {
+      return left === right;
     } else {
       return (
         ComponentPath.equals(left.componentPath, right.componentPath) &&
@@ -1301,6 +1449,9 @@ export class SymbolReference {
     return SymbolReference.equals(this, other);
   }
 
+  /**
+   * Combines this {@link SymbolReference} with the provided parts to create a {@link DeclarationReference}.
+   */
   public toDeclarationReference(
     parts?: DeclarationReferenceSourceParts</*With*/ false> &
       DeclarationReferenceNavigationParts</*With*/ false>
@@ -1319,9 +1470,53 @@ export class SymbolReference {
     }
     return result;
   }
+
+  /**
+   * Creates an array of {@link DocMemberReference} objects from this symbol.
+   * @internal
+   */
+  public toDocMemberReferences(configuration: TSDocConfiguration): DocMemberReference[] {
+    const memberReferences: DocMemberReference[] = [];
+    if (this.componentPath) {
+      let componentRoot: ComponentPath = this.componentPath;
+      const componentPathRev: ComponentNavigation[] = [];
+      while (componentRoot instanceof ComponentNavigation) {
+        componentPathRev.push(componentRoot);
+        componentRoot = componentRoot.parent;
+      }
+
+      const selector: DocMemberSelector | undefined =
+        componentPathRev.length === 0
+          ? meaningToSelector(configuration, this.meaning, undefined, this.overloadIndex)
+          : undefined;
+
+      memberReferences.push(
+        componentToDocMemberReference(configuration, /*hasDot*/ false, componentRoot.component, selector)
+      );
+
+      for (let i: number = componentPathRev.length - 1; i >= 0; i--) {
+        const segment: ComponentNavigation = componentPathRev[i];
+
+        const selector: DocMemberSelector | undefined =
+          i === 0
+            ? meaningToSelector(configuration, this.meaning, segment.navigation, this.overloadIndex)
+            : undefined;
+
+        memberReferences.push(
+          componentToDocMemberReference(configuration, /*hasDot*/ true, segment.component, selector)
+        );
+      }
+    }
+
+    return memberReferences;
+  }
 }
 
 /**
+ * Parts used to compose or update a {@link SymbolReference}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type SymbolReferenceParts<With extends boolean> = Parts<
@@ -1358,6 +1553,11 @@ function resolveSymbolReferenceParts(
 }
 
 /**
+ * A value that can be resolved to a {@link SymbolReference}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type SymbolReferenceLike<With extends boolean> = string | SymbolReference | SymbolReferenceParts<With>;
@@ -1367,18 +1567,24 @@ type ResolvedSymbolReferenceLike = string | SymbolReference | SymbolReferencePar
 function resolveSymbolReferenceLike(
   symbol: SymbolReferenceLike</*With*/ true> | undefined,
   fallbackSymbol: SymbolReference | undefined
-): ResolvedSymbolReferenceLike {
-  if (symbol instanceof SymbolReference || typeof symbol === 'string') {
+): ResolvedSymbolReferenceLike | undefined {
+  if (symbol === undefined || symbol instanceof SymbolReference || typeof symbol === 'string') {
     return symbol;
-  } else if (symbol === undefined) {
-    return SymbolReference.empty();
   } else {
-    return resolveSymbolReferenceParts(
+    const resolved: SymbolReferenceParts</*With*/ false> = resolveSymbolReferenceParts(
       symbol,
       fallbackSymbol?.componentPath,
       fallbackSymbol?.meaning,
       fallbackSymbol?.overloadIndex
     );
+    if (
+      resolved.componentPath !== undefined ||
+      resolved.meaning !== undefined ||
+      resolved.overloadIndex !== undefined ||
+      fallbackSymbol !== undefined
+    ) {
+      return resolved;
+    }
   }
 }
 
@@ -1394,8 +1600,7 @@ export abstract class ComponentPathBase {
   public abstract readonly kind: string;
   public readonly component: Component;
 
-  private declare _: never; // NOTE: This makes a ComponentPath compare nominally rather than structurally
-  //       which removes its properties from completions in `ComponentPath.from({ ... })`
+  private declare _: never; // NOTE: This makes a ComponentPath compare nominally rather than structurally which removes its properties from completions in `ComponentPath.from({ ... })`
 
   public constructor(component: Component) {
     this.component = component;
@@ -1420,6 +1625,8 @@ export abstract class ComponentPathBase {
 
   /**
    * Combines this {@link ComponentPath} with a {@link Meaning} to create a new {@link SymbolReference}.
+   *
+   * An alias for `componentPath.toSymbolReference({ meaning })`.
    */
   public withMeaning(this: ComponentPath, meaning: Meaning | undefined): SymbolReference {
     return this.toSymbolReference({ meaning });
@@ -1427,6 +1634,8 @@ export abstract class ComponentPathBase {
 
   /**
    * Combines this {@link ComponentPath} with an overload index to create a new {@link SymbolReference}.
+   *
+   * An alias for `componentPath.toSymbolReference({ overloadIndex })`.
    */
   public withOverloadIndex(this: ComponentPath, overloadIndex: number | undefined): SymbolReference {
     return this.toSymbolReference({ overloadIndex });
@@ -1434,6 +1643,8 @@ export abstract class ComponentPathBase {
 
   /**
    * Combines this {@link ComponentPath} with a {@link Source} to create a new {@link DeclarationReference}.
+   *
+   * An alias for `componentPath.toDeclarationReference({ source })`.
    */
   public withSource(this: ComponentPath, source: Source | undefined): DeclarationReference {
     return this.toDeclarationReference({ source });
@@ -1549,6 +1760,9 @@ export class ComponentRoot extends ComponentPathBase {
   /**
    * Returns a {@link ComponentRoot} updated with the provided component.
    * If a part is set to `undefined`, the current value is used.
+   *
+   * An alias for `componentRoot.with({ component })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided component.
    */
   public withComponent(component: ComponentLike</*With*/ false>): ComponentRoot {
@@ -1561,6 +1775,10 @@ export class ComponentRoot extends ComponentPathBase {
 }
 
 /**
+ * Parts used to compose or update a {@link ComponentRoot}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentRootParts<With extends boolean> = Parts<
@@ -1585,6 +1803,11 @@ function resolveComponentRootParts(
 }
 
 /**
+ * A value that can be resolved to a {@link ComponentRoot}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentRootLike<With extends boolean> =
@@ -1698,6 +1921,9 @@ export class ComponentNavigation extends ComponentPathBase {
 
   /**
    * Returns a {@link ComponentNavigation} updated with the provided parent.
+   *
+   * An alias for `componentNav.with({ parent })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided parent.
    */
   public withParent(parent: ComponentPath): ComponentNavigation {
@@ -1706,6 +1932,9 @@ export class ComponentNavigation extends ComponentPathBase {
 
   /**
    * Returns a {@link ComponentNavigation} updated with the provided navigation.
+   *
+   * An alias for `componentNav.with({ navigation })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided navigation.
    */
   public withNavigation(navigation: Navigation): ComponentNavigation {
@@ -1714,6 +1943,9 @@ export class ComponentNavigation extends ComponentPathBase {
 
   /**
    * Returns a {@link ComponentNavigation} updated with the provided component.
+   *
+   * An alias for `componentNav.with({ component })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided component.
    */
   public withComponent(component: ComponentLike</*With*/ false>): ComponentNavigation {
@@ -1751,6 +1983,10 @@ export class ComponentNavigation extends ComponentPathBase {
 }
 
 /**
+ * Parts used to compose or update a {@link ComponentNavigation}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentNavigationParts<With extends boolean> = Parts<
@@ -1795,6 +2031,11 @@ function resolveComponentNavigationParts(
 }
 
 /**
+ * A value that can be resolved to a {@link ComponentNavigation}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentNavigationLike<With extends boolean> =
@@ -1835,7 +2076,7 @@ export namespace ComponentPath {
    * Parses a {@link SymbolReference} from the supplied text.
    */
   export function parse(text: string): ComponentPath {
-    const parser: Parser = new Parser(text);
+    const parser: Parser = new Parser(new TextReader(text));
     const componentPath: ComponentPath = parser.parseComponentPath();
     if (parser.errors.length) {
       throw new SyntaxError(`Invalid ComponentPath '${text}':\n  ${parser.errors.join('\n  ')}`);
@@ -1880,6 +2121,10 @@ export namespace ComponentPath {
 }
 
 /**
+ * Parts that can be used to compose or update a {@link ComponentPath}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentPathParts<With extends boolean> =
@@ -1909,6 +2154,11 @@ function resolveComponentPathParts(
 }
 
 /**
+ * A value that can be resolved to a {@link ComponentPath}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentPathLike<With extends boolean> =
@@ -1965,8 +2215,7 @@ function resolveComponentPathLike(
 export abstract class ComponentBase {
   public abstract readonly kind: string;
 
-  private declare _: never; // NOTE: This makes a Component compare nominally rather than structurally
-  //       which removes its properties from completions in `Component.from({ ... })`
+  private declare _: never; // NOTE: This makes a Component compare nominally rather than structurally which removes its properties from completions in `Component.from({ ... })`
 
   /**
    * Combines this component with the provided parts to create a new {@link Component}.
@@ -2048,6 +2297,8 @@ class ParsedComponentString extends ComponentString {
 }
 
 /**
+ * Parts that can be used to compose or update a {@link ComponentString}.
+ *
  * @beta
  */
 export type ComponentStringParts = Parts<
@@ -2059,6 +2310,7 @@ export type ComponentStringParts = Parts<
 >;
 
 /**
+ * A value that can be resolved to a {@link ComponentString}.
  * @beta
  */
 export type ComponentStringLike = ComponentStringParts | ComponentString | string;
@@ -2123,6 +2375,9 @@ export class ComponentReference extends ComponentBase {
 
   /**
    * Returns a {@link ComponentReference} updated with the provided reference.
+   *
+   * An alias for `componentRef.with({ reference })`.
+   *
    * @returns This object if there were no changes; otherwise, a new object updated with the provided reference.
    */
   public withReference(reference: DeclarationReference): ComponentReference {
@@ -2153,6 +2408,10 @@ export class ComponentReference extends ComponentBase {
 }
 
 /**
+ * Parts that can be used to compose or update a {@link ComponentReference}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentReferenceParts<With extends boolean> = Parts<
@@ -2168,15 +2427,24 @@ function resolveComponentReferenceParts(
   fallbackReference: DeclarationReference | undefined
 ): ComponentReferenceParts</*With*/ false> {
   const { reference = fallbackReference } = parts;
-  if (reference === undefined) {
+  const resolvedReference: ResolvedDeclarationReferenceLike | undefined = resolveDeclarationReferenceLike(
+    reference,
+    fallbackReference
+  );
+  if (resolvedReference === undefined) {
     throw new TypeError("The property 'reference' is required");
   }
   return {
-    reference: resolveDeclarationReferenceLike(reference, fallbackReference)
+    reference: resolvedReference
   };
 }
 
 /**
+ * A value that can be resolved to a {@link ComponentReference}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentReferenceLike<With extends boolean> =
@@ -2228,7 +2496,42 @@ export namespace Component {
   }
 }
 
+function componentToDocMemberReference(
+  configuration: TSDocConfiguration,
+  hasDot: boolean,
+  component: Component,
+  selector: DocMemberSelector | undefined
+): DocMemberReference {
+  const memberIdentifier: DocMemberIdentifier | undefined =
+    component instanceof ComponentString
+      ? new DocMemberIdentifier({ configuration, identifier: component.text })
+      : undefined;
+
+  const memberSymbol: DocMemberSymbol | undefined =
+    component instanceof ComponentReference
+      ? new DocMemberSymbol({
+          configuration,
+          symbolReference: new DocDeclarationReference({
+            configuration,
+            declarationReference: component.reference
+          })
+        })
+      : undefined;
+
+  return new DocMemberReference({
+    configuration,
+    hasDot,
+    memberIdentifier,
+    memberSymbol,
+    selector
+  });
+}
+
 /**
+ * Parts that can be used to compose a {@link Component}.
+ *
+ * @typeParam With - `true` if these parts are used by `with()` (which allows `null` for some parts), `false` if these parts are used by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentParts<With extends boolean> = ComponentStringParts | ComponentReferenceParts<With>;
@@ -2257,6 +2560,11 @@ function resolveComponentParts(
 }
 
 /**
+ * A value that can be resolved to a {@link Component}.
+ *
+ * @typeParam With - `true` if this type is used by `with()` (which allows `null` for some parts), `false` if this value is used
+ * by `from()` (which does not allow `null`).
+ *
  * @beta
  */
 export type ComponentLike<With extends boolean> =
@@ -2336,6 +2644,53 @@ export const enum Meaning {
   ComplexType = 'complex' // Any complex type
 }
 
+function meaningToSelector(
+  configuration: TSDocConfiguration,
+  meaning: Meaning | undefined,
+  navigation: Navigation | undefined,
+  overloadIndex: number | undefined
+): DocMemberSelector | undefined {
+  if (overloadIndex !== undefined) {
+    return new DocMemberSelector({
+      configuration,
+      selector: overloadIndex.toString()
+    });
+  }
+  switch (meaning) {
+    case Meaning.Class:
+    case Meaning.Interface:
+    case Meaning.Namespace:
+    case Meaning.TypeAlias:
+    case Meaning.Function:
+    case Meaning.Enum:
+    case Meaning.Constructor:
+      return new DocMemberSelector({
+        configuration,
+        selector: meaning
+      });
+    case Meaning.Variable:
+      return new DocMemberSelector({
+        configuration,
+        selector: 'variable'
+      });
+    case Meaning.Member:
+    case Meaning.Event:
+      switch (navigation) {
+        case Navigation.Exports:
+          return new DocMemberSelector({
+            configuration,
+            selector: 'static'
+          });
+        case Navigation.Members:
+          return new DocMemberSelector({
+            configuration,
+            selector: 'instance'
+          });
+      }
+      break;
+  }
+}
+
 // #endregion Meaning
 
 // #region Token
@@ -2453,35 +2808,194 @@ function tokenToString(token: Token): string {
 
 // #region Scanner
 
-class Scanner {
-  private _tokenPos: number;
-  private _pos: number;
+interface ICharacterReader {
+  readonly eof: boolean;
+  mark(): number;
+  rewind(marker: number): void;
+  readChar(count?: number): string;
+  peekChar(lookahead?: number): string;
+  readFrom(marker: number): string;
+}
+
+class TextReader implements ICharacterReader {
   private _text: string;
-  private _token: Token;
-  private _stringIsUnterminated: boolean;
+  private _pos: number;
 
   public constructor(text: string) {
+    this._text = text;
     this._pos = 0;
-    this._tokenPos = 0;
-    this._stringIsUnterminated = false;
+  }
+
+  public get eof(): boolean {
+    return this._pos >= this._text.length;
+  }
+
+  public mark(): number {
+    return this._pos;
+  }
+
+  public rewind(marker: number): void {
+    this._pos = marker;
+  }
+
+  public readChar(count: number = 1): string {
+    if (count < 1) throw new RangeError('Argument out of range: count');
+    let ch: string = '';
+    while (count > 0) {
+      if (this.eof) return '';
+      ch = this._text.charAt(this._pos++);
+      count--;
+    }
+    return ch;
+  }
+
+  public peekChar(lookahead: number = 1): string {
+    if (lookahead < 1) throw new RangeError('Argument out of range: lookahead');
+    const marker: number = this.mark();
+    const ch: string = this.readChar(lookahead);
+    this.rewind(marker);
+    return ch;
+  }
+
+  public readFrom(marker: number): string {
+    return this._text.substring(marker, this._pos);
+  }
+}
+
+class TokenReaderNormalizer implements ICharacterReader {
+  private _tokenReader: TokenReader;
+  private _token: DocToken | undefined;
+  private _partialTokenPos: number = 0;
+  private _startMarker: number;
+  private _markerSizes: { [marker: number]: number | undefined } = {};
+
+  public constructor(tokenReader: TokenReader) {
+    this._tokenReader = tokenReader;
+    this._startMarker = tokenReader.createMarker();
+    this._token = tokenReader.peekTokenKind() === TokenKind.EndOfInput ? undefined : tokenReader.peekToken();
+    if (this._token) {
+      this._markerSizes[this._startMarker] = this._token.range.length;
+    }
+  }
+
+  public get eof(): boolean {
+    return this._token === undefined;
+  }
+
+  public mark(): number {
+    const tokenMarker: number = this._tokenReader.createMarker();
+
+    let offset: number = 0;
+    for (let i: number = this._startMarker; i < tokenMarker; i++) {
+      const markerSize: number = this._markerSizes[i] ?? 1;
+      offset += markerSize;
+    }
+
+    offset += this._partialTokenPos;
+    return offset;
+  }
+
+  public rewind(marker: number): void {
+    let tokenMarker: number = this._startMarker;
+    let partialTokenPos: number = 0;
+
+    let offset: number = 0;
+    while (offset < marker) {
+      const markerSize: number = this._markerSizes[tokenMarker] ?? 1;
+      if (offset + markerSize < marker) {
+        offset += markerSize;
+        tokenMarker++;
+      } else {
+        partialTokenPos = marker - offset;
+        break;
+      }
+    }
+
+    this._tokenReader.backtrackToMarker(tokenMarker);
+    this._token = this._tokenReader.peekToken();
+    this._partialTokenPos = partialTokenPos;
+  }
+
+  public readChar(count: number = 1): string {
+    if (count < 1) throw new RangeError('Argument out of range: count');
+    let ch: string = '';
+    while (count > 0) {
+      if (!this._token) return '';
+      if (this._partialTokenPos === this._token.range.length) {
+        if (this._tokenReader.peekTokenKind() === TokenKind.EndOfInput) {
+          this._token = undefined;
+        } else {
+          this._tokenReader.readToken();
+          this._token = this._tokenReader.peekToken();
+        }
+
+        this._partialTokenPos = 0;
+        if (!this._token) {
+          return '';
+        } else {
+          const length: number = this._token.range.length;
+          if (length > 1) {
+            this._markerSizes[this._tokenReader.createMarker()] = length;
+          }
+        }
+      }
+      ch = this._token.toString().charAt(this._partialTokenPos++);
+      count--;
+    }
+    return ch;
+  }
+
+  public peekChar(lookahead: number = 1): string {
+    if (lookahead < 1) throw new RangeError('Argument out of range: lookahead');
+    const tokenMarker: number = this._tokenReader.createMarker();
+    const savedTokenReader: TokenReader = this._tokenReader;
+    const savedToken: DocToken | undefined = this._token;
+    const savedPartialTokenPos: number = this._partialTokenPos;
+    const ch: string = this.readChar(lookahead);
+    this._partialTokenPos = savedPartialTokenPos;
+    this._token = savedToken;
+    this._tokenReader = savedTokenReader;
+    this._tokenReader.backtrackToMarker(tokenMarker);
+    return ch;
+  }
+
+  public readFrom(marker: number): string {
+    const currentMarker: number = this.mark();
+    const savedTokenReader: TokenReader = this._tokenReader;
+    this._tokenReader = savedTokenReader.clone();
+    this.rewind(marker);
+    let text: string = '';
+    while (this.mark() < currentMarker) {
+      text += this.readChar(1);
+    }
+    this._tokenReader = savedTokenReader;
+    return text;
+  }
+}
+
+class Scanner {
+  private _reader: ICharacterReader;
+  private _token: Token;
+  private _tokenMarker: number;
+  private _stringIsUnterminated: boolean;
+
+  public constructor(reader: ICharacterReader) {
+    this._reader = reader;
+    this._tokenMarker = reader.mark();
     this._token = Token.None;
-    this._text = text;
+    this._stringIsUnterminated = false;
   }
 
   public get stringIsUnterminated(): boolean {
     return this._stringIsUnterminated;
   }
 
-  public get text(): string {
-    return this._text;
-  }
-
   public get tokenText(): string {
-    return this._text.slice(this._tokenPos, this._pos);
+    return this._reader.readFrom(this._tokenMarker);
   }
 
   public get eof(): boolean {
-    return this._pos >= this._text.length;
+    return this._reader.eof;
   }
 
   public token(): Token {
@@ -2489,9 +3003,9 @@ class Scanner {
   }
 
   public speculate<T>(cb: (accept: () => void) => T): T {
-    const tokenPos: number = this._tokenPos;
-    const pos: number = this._pos;
-    const text: string = this._text;
+    const tokenMarker: number = this._tokenMarker;
+    const marker: number = this._reader.mark();
+    const reader: ICharacterReader = this._reader;
     const token: Token = this._token;
     const stringIsUnterminated: boolean = this._stringIsUnterminated;
     let accepted: boolean = false;
@@ -2502,21 +3016,21 @@ class Scanner {
       return cb(accept);
     } finally {
       if (!accepted) {
-        this._tokenPos = tokenPos;
-        this._pos = pos;
-        this._text = text;
-        this._token = token;
         this._stringIsUnterminated = stringIsUnterminated;
+        this._token = token;
+        this._reader = reader;
+        this._reader.rewind(marker);
+        this._tokenMarker = tokenMarker;
       }
     }
   }
 
   public scan(): Token {
     if (!this.eof) {
-      this._tokenPos = this._pos;
+      this._tokenMarker = this._reader.mark();
       this._stringIsUnterminated = false;
       while (!this.eof) {
-        const ch: string = this._text.charAt(this._pos++);
+        const ch: string = this._reader.readChar();
         switch (ch) {
           case '{':
             return (this._token = Token.OpenBraceToken);
@@ -2565,11 +3079,11 @@ class Scanner {
     }
     return this.speculate((accept) => {
       if (!this.eof) {
-        this._pos = this._tokenPos;
+        this._reader.rewind(this._tokenMarker);
         this._stringIsUnterminated = false;
         let scanned: 'string' | 'other' | 'none' = 'none';
         while (!this.eof) {
-          const ch: string = this._text[this._pos];
+          const ch: string = this._reader.peekChar(1);
           if (ch === '!') {
             if (scanned === 'none') {
               return this._token;
@@ -2577,7 +3091,7 @@ class Scanner {
             accept();
             return (this._token = Token.ModuleSource);
           }
-          this._pos++;
+          this._reader.readChar();
           if (ch === '"') {
             if (scanned === 'other') {
               // strings not allowed after scanning any other characters
@@ -2650,7 +3164,7 @@ class Scanner {
 
   private scanString(): void {
     while (!this.eof) {
-      const ch: string = this._text.charAt(this._pos++);
+      const ch: string = this._reader.readChar();
       switch (ch) {
         case '"':
           return;
@@ -2673,31 +3187,23 @@ class Scanner {
       return;
     }
 
-    const ch: string = this._text.charAt(this._pos);
+    const ch: string = this._reader.peekChar(1);
 
     // EscapeSequence:: CharacterEscapeSequence
     if (isCharacterEscapeSequence(ch)) {
-      this._pos++;
+      this._reader.readChar(1);
       return;
     }
 
     // EscapeSequence:: `0` [lookahead != DecimalDigit]
-    if (
-      ch === '0' &&
-      (this._pos + 1 === this._text.length || !isDecimalDigit(this._text.charAt(this._pos + 1)))
-    ) {
-      this._pos++;
+    if (ch === '0' && !isDecimalDigit(this._reader.peekChar(2))) {
+      this._reader.readChar(1);
       return;
     }
 
     // EscapeSequence:: HexEscapeSequence
-    if (
-      ch === 'x' &&
-      this._pos + 3 <= this._text.length &&
-      isHexDigit(this._text.charAt(this._pos + 1)) &&
-      isHexDigit(this._text.charAt(this._pos + 2))
-    ) {
-      this._pos += 3;
+    if (ch === 'x' && isHexDigit(this._reader.peekChar(2)) && isHexDigit(this._reader.peekChar(3))) {
+      this._reader.readChar(3);
       return;
     }
 
@@ -2705,27 +3211,29 @@ class Scanner {
     // UnicodeEscapeSequence:: `u` Hex4Digits
     if (
       ch === 'u' &&
-      this._pos + 5 <= this._text.length &&
-      isHexDigit(this._text.charAt(this._pos + 1)) &&
-      isHexDigit(this._text.charAt(this._pos + 2)) &&
-      isHexDigit(this._text.charAt(this._pos + 3)) &&
-      isHexDigit(this._text.charAt(this._pos + 4))
+      isHexDigit(this._reader.peekChar(2)) &&
+      isHexDigit(this._reader.peekChar(3)) &&
+      isHexDigit(this._reader.peekChar(4)) &&
+      isHexDigit(this._reader.peekChar(5))
     ) {
-      this._pos += 5;
+      this._reader.readChar(5);
       return;
     }
 
     // EscapeSequence:: UnicodeEscapeSequence
     // UnicodeEscapeSequence:: `u` `{` CodePoint `}`
-    if (ch === 'u' && this._pos + 4 <= this._text.length && this._text.charAt(this._pos + 1) === '{') {
-      let hexDigits: string = this._text.charAt(this._pos + 2);
+    if (ch === 'u' && this._reader.peekChar(2) === '{') {
+      let hexDigits: string = this._reader.peekChar(3);
       if (isHexDigit(hexDigits)) {
-        for (let i: number = this._pos + 3; i < this._text.length; i++) {
-          const ch2: string = this._text.charAt(i);
+        for (
+          let i: number = 4, ch2: string = this._reader.peekChar(i);
+          ch2 !== '';
+          i++, ch2 = this._reader.peekChar(i)
+        ) {
           if (ch2 === '}') {
             const mv: number = parseInt(hexDigits, 16);
             if (mv <= 0x10ffff) {
-              this._pos = i + 1;
+              this._reader.readChar(i + 1);
               return;
             }
             break;
@@ -2741,12 +3249,12 @@ class Scanner {
   }
 
   private scanText(): void {
-    while (this._pos < this._text.length) {
-      const ch: string = this._text.charAt(this._pos);
+    while (!this._reader.eof) {
+      const ch: string = this._reader.peekChar();
       if (isPunctuator(ch) || ch === '"') {
         return;
       }
-      this._pos++;
+      this._reader.readChar();
     }
   }
 }
@@ -2863,10 +3371,12 @@ function isPunctuator(ch: string): boolean {
 class Parser {
   private _errors: string[];
   private _scanner: Scanner;
+  private _fallback: boolean;
 
-  public constructor(text: string) {
+  public constructor(reader: ICharacterReader, fallback: boolean = false) {
     this._errors = [];
-    this._scanner = new Scanner(text);
+    this._fallback = fallback;
+    this._scanner = new Scanner(reader);
     this._scanner.scan();
   }
 
@@ -2916,7 +3426,11 @@ class Parser {
       case Token.String:
         return this.parseString();
       default:
-        return this.parseComponentCharacters();
+        const text: string | undefined = this.parseComponentCharacters();
+        if (text === undefined) {
+          return this.fail('One or more characters expected', '');
+        }
+        return text;
     }
   }
 
@@ -3055,11 +3569,14 @@ class Parser {
     }
   }
 
-  private parseComponentCharacters(): string {
-    let text: string = '';
+  private parseComponentCharacters(): string | undefined {
+    let text: string | undefined;
     for (;;) {
       switch (this._scanner.token()) {
         case Token.Text:
+          if (text === undefined) {
+            text = '';
+          }
           text += this.parseText();
           break;
         default:
@@ -3082,7 +3599,11 @@ class Parser {
   }
 
   private parseText(): string {
-    return this.parseTokenString(Token.Text, 'Text');
+    const text: string = this.parseTokenString(Token.Text, 'Text');
+    if (this._fallback && StringChecks.isSystemSelector(text)) {
+      return this.fail('No system selectors in fallback parsing', text);
+    }
+    return text;
   }
 
   private parseString(): string {
@@ -3102,7 +3623,11 @@ class Parser {
     this.expectToken(Token.OpenBracketToken);
     const reference: DeclarationReference = this.parseDeclarationReference();
     this.expectToken(Token.CloseBracketToken);
-    return new ComponentReference(reference);
+    const component: ComponentReference = new ComponentReference(reference);
+    if (this._fallback && reference.isEmpty) {
+      return this.fail('No empty brackets in fallback parsing', component);
+    }
+    return component;
   }
 
   private optionalToken(token: Token): boolean {
@@ -3159,11 +3684,9 @@ function ensureScopeName(scopeName: string): string {
 }
 
 interface ObjectConstructorWithSetPrototypeOf extends ObjectConstructor {
-  // eslint-disable-next-line @rushstack/no-new-null
   setPrototypeOf?(obj: object, proto: object | null): object;
 }
 
-// eslint-disable-next-line @rushstack/no-new-null
 const setPrototypeOf:
   | ((obj: object, proto: object | null) => object)
   | undefined = (Object as ObjectConstructorWithSetPrototypeOf).setPrototypeOf;
@@ -3175,21 +3698,41 @@ function tryCast<T>(value: unknown, type: new (...args: any[]) => T): T | undefi
 
 /**
  * Describes the parts that can be used in a `from()` or `with()` call.
+ *
+ * In a `with()` call, all optional parts can also be `null`, and all non-optional parts become optional.
+ *
+ * @typeParam With - If `true, indicates these parts are used in a `with()` call.
+ *
  * @beta
  */
 export type Parts<With extends boolean, T> = [With] extends [false]
   ? T
-  : T extends unknown
+  : T extends unknown // NOTE: Distributes over `T`
   ? Partial<T>
   : never;
 
 /**
  * If a part can be removed via a `with()` call, marks that part with `| null`
+ *
+ * @typeParam With - If `true, indicates this part is used in a `with()` call.
+ *
  * @beta
  */
 export type Part<With extends boolean, T> = [With] extends [false] ? T : T | null;
 
+/**
+ * Distributes over `T` to get all possible keys of `T`.
+ */
 type AllKeysOf<T> = T extends unknown ? keyof T : never;
+
+/**
+ * Distributes over `T` to get all possible values of `T` with the key `P`.
+ */
+type AllValuesOf<T, P> = T extends unknown ? (P extends keyof T ? T[P] : undefined) : never;
+
+/**
+ * Distributes over `T` to get all possible properties of every `T`.
+ */
 type AllParts<T> = {
-  [P in AllKeysOf<T>]: T extends unknown ? (P extends keyof T ? T[P] : undefined) : never;
+  [P in AllKeysOf<T>]: AllValuesOf<T, P>;
 };
diff --git a/tsdoc/src/beta/__tests__/DeclarationReference.test.ts b/tsdoc/src/beta/__tests__/DeclarationReference.test.ts
index 5e10da16..d82ab54d 100644
--- a/tsdoc/src/beta/__tests__/DeclarationReference.test.ts
+++ b/tsdoc/src/beta/__tests__/DeclarationReference.test.ts
@@ -153,6 +153,22 @@ describe('parser', () => {
       DeclarationReference.parse('@scope/foo');
     }).toThrow();
   });
+  it.each`
+    text
+    ${'!bar..baz'}
+    ${'!bar.#baz'}
+    ${'!bar.~baz'}
+    ${'!bar#.baz'}
+    ${'!bar##baz'}
+    ${'!bar#~baz'}
+    ${'!bar~.baz'}
+    ${'!bar~#baz'}
+    ${'!bar~~baz'}
+  `('parse invalid symbol $text', ({ text }) => {
+    expect(() => {
+      DeclarationReference.parse(text);
+    }).toThrow();
+  });
 });
 
 describe('DeclarationReference', () => {
@@ -1609,6 +1625,7 @@ describe('ComponentPathBase', () => {
     });
     expect(declref.source).toBe(source);
     expect(declref.navigation).toBe(Navigation.Exports);
+    expect(declref.symbol).toBeDefined();
     expect(declref.symbol?.componentPath).toBe(component);
     expect(declref.symbol?.meaning).toBe(Meaning.Variable);
     expect(declref.symbol?.overloadIndex).toBe(0);
diff --git a/tsdoc/src/configuration/TSDocConfiguration.ts b/tsdoc/src/configuration/TSDocConfiguration.ts
index 93dc7b7a..21b44e8b 100644
--- a/tsdoc/src/configuration/TSDocConfiguration.ts
+++ b/tsdoc/src/configuration/TSDocConfiguration.ts
@@ -15,6 +15,7 @@ export class TSDocConfiguration {
   private readonly _validation: TSDocValidationConfiguration;
   private readonly _docNodeManager: DocNodeManager;
   private readonly _supportedHtmlElements: Set<string>;
+  private _parseBetaDeclarationReferences: boolean | 'prefer' = false;
 
   public constructor() {
     this._tagDefinitions = [];
@@ -254,4 +255,12 @@ export class TSDocConfiguration {
     }
     throw new Error('The specified TSDocTagDefinition is not defined for this TSDocConfiguration');
   }
+
+  public get parseBetaDeclarationReferences(): boolean | 'prefer' {
+    return this._parseBetaDeclarationReferences;
+  }
+
+  public set parseBetaDeclarationReferences(value: boolean | 'prefer') {
+    this._parseBetaDeclarationReferences = value;
+  }
 }
diff --git a/tsdoc/src/emitters/TSDocEmitter.ts b/tsdoc/src/emitters/TSDocEmitter.ts
index bec376d2..5d8cacf1 100644
--- a/tsdoc/src/emitters/TSDocEmitter.ts
+++ b/tsdoc/src/emitters/TSDocEmitter.ts
@@ -144,15 +144,19 @@ export class TSDocEmitter {
 
       case DocNodeKind.DeclarationReference:
         const docDeclarationReference: DocDeclarationReference = docNode as DocDeclarationReference;
-        this._writeContent(docDeclarationReference.packageName);
-        this._writeContent(docDeclarationReference.importPath);
-        if (
-          docDeclarationReference.packageName !== undefined ||
-          docDeclarationReference.importPath !== undefined
-        ) {
-          this._writeContent('#');
+        if (docDeclarationReference.declarationReference) {
+          this._writeContent(docDeclarationReference.declarationReference.toString());
+        } else {
+          this._writeContent(docDeclarationReference.packageName);
+          this._writeContent(docDeclarationReference.importPath);
+          if (
+            docDeclarationReference.packageName !== undefined ||
+            docDeclarationReference.importPath !== undefined
+          ) {
+            this._writeContent('#');
+          }
+          this._renderNodes(docDeclarationReference.memberReferences);
         }
-        this._renderNodes(docDeclarationReference.memberReferences);
         break;
 
       case DocNodeKind.ErrorText:
diff --git a/tsdoc/src/nodes/DocDeclarationReference.ts b/tsdoc/src/nodes/DocDeclarationReference.ts
index 4c213163..734c2343 100644
--- a/tsdoc/src/nodes/DocDeclarationReference.ts
+++ b/tsdoc/src/nodes/DocDeclarationReference.ts
@@ -3,6 +3,7 @@ import { DocMemberReference } from './DocMemberReference';
 import { TokenSequence } from '../parser/TokenSequence';
 import { DocExcerpt, ExcerptKind } from './DocExcerpt';
 import { StringBuilder } from '../emitters/StringBuilder';
+import { DeclarationReference, ModuleSource } from '../beta/DeclarationReference';
 
 /**
  * Constructor parameters for {@link DocDeclarationReference}.
@@ -13,6 +14,14 @@ export interface IDocDeclarationReferenceParameters extends IDocNodeParameters {
   memberReferences?: DocMemberReference[];
 }
 
+/**
+ * Constructor parameters for {@link DocDeclarationReference}.
+ * @beta
+ */
+export interface IBetaDocDeclarationReferenceParameters extends IDocNodeParameters {
+  declarationReference: DeclarationReference;
+}
+
 /**
  * Constructor parameters for {@link DocDeclarationReference}.
  */
@@ -24,6 +33,15 @@ export interface IDocDeclarationReferenceParsedParameters extends IDocNodeParsed
   memberReferences?: DocMemberReference[];
 }
 
+/**
+ * Constructor parameters for {@link DocDeclarationReference}.
+ * @beta
+ */
+export interface IBetaDocDeclarationReferenceParsedParameters extends IDocNodeParsedParameters {
+  declarationReferenceExcerpt: TokenSequence;
+  declarationReference?: DeclarationReference;
+}
+
 /**
  * Represents a declaration reference.
  *
@@ -41,54 +59,75 @@ export class DocDeclarationReference extends DocNode {
   private readonly _importHashExcerpt: DocExcerpt | undefined;
   private readonly _spacingAfterImportHashExcerpt: DocExcerpt | undefined;
 
-  private readonly _memberReferences: DocMemberReference[];
+  private _memberReferences: DocMemberReference[] | undefined;
+
+  private readonly _declarationReference: DeclarationReference | undefined;
+  private readonly _declarationReferenceExcerpt: DocExcerpt | undefined;
 
   /**
    * Don't call this directly.  Instead use {@link TSDocParser}
    * @internal
    */
   public constructor(
-    parameters: IDocDeclarationReferenceParameters | IDocDeclarationReferenceParsedParameters
+    parameters:
+      | IDocDeclarationReferenceParameters
+      | IDocDeclarationReferenceParsedParameters
+      | IBetaDocDeclarationReferenceParameters
+      | IBetaDocDeclarationReferenceParsedParameters
   ) {
     super(parameters);
 
     if (DocNode.isParsedParameters(parameters)) {
-      if (parameters.packageNameExcerpt) {
-        this._packageNameExcerpt = new DocExcerpt({
-          configuration: this.configuration,
-          excerptKind: ExcerptKind.DeclarationReference_PackageName,
-          content: parameters.packageNameExcerpt
-        });
-      }
-      if (parameters.importPathExcerpt) {
-        this._importPathExcerpt = new DocExcerpt({
-          configuration: this.configuration,
-          excerptKind: ExcerptKind.DeclarationReference_ImportPath,
-          content: parameters.importPathExcerpt
-        });
-      }
-      if (parameters.importHashExcerpt) {
-        this._importHashExcerpt = new DocExcerpt({
-          configuration: this.configuration,
-          excerptKind: ExcerptKind.DeclarationReference_ImportHash,
-          content: parameters.importHashExcerpt
-        });
-      }
-      if (parameters.spacingAfterImportHashExcerpt) {
-        this._spacingAfterImportHashExcerpt = new DocExcerpt({
+      if ('declarationReferenceExcerpt' in parameters) {
+        this._declarationReferenceExcerpt = new DocExcerpt({
           configuration: this.configuration,
-          excerptKind: ExcerptKind.Spacing,
-          content: parameters.spacingAfterImportHashExcerpt
+          excerptKind: ExcerptKind.DeclarationReference_DeclarationReference,
+          content: parameters.declarationReferenceExcerpt
         });
+        this._declarationReference =
+          parameters.declarationReference ??
+          DeclarationReference.parse(this._declarationReferenceExcerpt.content.toString());
+      } else {
+        if (parameters.packageNameExcerpt) {
+          this._packageNameExcerpt = new DocExcerpt({
+            configuration: this.configuration,
+            excerptKind: ExcerptKind.DeclarationReference_PackageName,
+            content: parameters.packageNameExcerpt
+          });
+        }
+        if (parameters.importPathExcerpt) {
+          this._importPathExcerpt = new DocExcerpt({
+            configuration: this.configuration,
+            excerptKind: ExcerptKind.DeclarationReference_ImportPath,
+            content: parameters.importPathExcerpt
+          });
+        }
+        if (parameters.importHashExcerpt) {
+          this._importHashExcerpt = new DocExcerpt({
+            configuration: this.configuration,
+            excerptKind: ExcerptKind.DeclarationReference_ImportHash,
+            content: parameters.importHashExcerpt
+          });
+        }
+        if (parameters.spacingAfterImportHashExcerpt) {
+          this._spacingAfterImportHashExcerpt = new DocExcerpt({
+            configuration: this.configuration,
+            excerptKind: ExcerptKind.Spacing,
+            content: parameters.spacingAfterImportHashExcerpt
+          });
+        }
+        if (parameters.memberReferences) {
+          this._memberReferences = parameters.memberReferences.slice();
+        }
       }
+    } else if ('declarationReference' in parameters) {
+      this._declarationReference = parameters.declarationReference;
     } else {
       this._packageName = parameters.packageName;
       this._importPath = parameters.importPath;
-    }
-
-    this._memberReferences = [];
-    if (parameters.memberReferences) {
-      this._memberReferences.push(...parameters.memberReferences);
+      if (parameters.memberReferences) {
+        this._memberReferences = parameters.memberReferences.slice();
+      }
     }
   }
 
@@ -103,6 +142,12 @@ export class DocDeclarationReference extends DocNode {
    * Example: `"@scope/my-package"`
    */
   public get packageName(): string | undefined {
+    if (this.declarationReference) {
+      if (this.declarationReference.source instanceof ModuleSource) {
+        return this.declarationReference.source.packageName;
+      }
+      return undefined;
+    }
     if (this._packageName === undefined) {
       if (this._packageNameExcerpt !== undefined) {
         this._packageName = this._packageNameExcerpt.content.toString();
@@ -121,6 +166,11 @@ export class DocDeclarationReference extends DocNode {
    * Example: `"../path2/path2"`
    */
   public get importPath(): string | undefined {
+    if (this.declarationReference) {
+      if (this.declarationReference.source instanceof ModuleSource) {
+        return this.declarationReference.source.importPath;
+      }
+    }
     if (this._importPath === undefined) {
       if (this._importPathExcerpt !== undefined) {
         this._importPath = this._importPathExcerpt.content.toString();
@@ -135,9 +185,21 @@ export class DocDeclarationReference extends DocNode {
    * because the reference refers to a module.
    */
   public get memberReferences(): ReadonlyArray<DocMemberReference> {
+    if (!this._memberReferences) {
+      this._memberReferences =
+        this._declarationReference?.symbol?.toDocMemberReferences(this.configuration) ?? [];
+    }
     return this._memberReferences;
   }
 
+  /**
+   * Gets the beta DeclarationReference for this reference.
+   * @beta
+   */
+  public get declarationReference(): DeclarationReference | undefined {
+    return this._declarationReference;
+  }
+
   /**
    * Generates the TSDoc representation of this declaration reference.
    */
@@ -151,13 +213,15 @@ export class DocDeclarationReference extends DocNode {
 
   /** @override */
   protected onGetChildNodes(): ReadonlyArray<DocNode | undefined> {
-    return [
-      this._packageNameExcerpt,
-      this._importPathExcerpt,
-      this._importHashExcerpt,
-      this._spacingAfterImportHashExcerpt,
-      ...this._memberReferences
-    ];
+    return this._declarationReferenceExcerpt
+      ? [this._declarationReferenceExcerpt]
+      : [
+          this._packageNameExcerpt,
+          this._importPathExcerpt,
+          this._importHashExcerpt,
+          this._spacingAfterImportHashExcerpt,
+          ...(this._memberReferences ?? [])
+        ];
   }
 }
 
diff --git a/tsdoc/src/nodes/DocExcerpt.ts b/tsdoc/src/nodes/DocExcerpt.ts
index df21c29b..06ed0abe 100644
--- a/tsdoc/src/nodes/DocExcerpt.ts
+++ b/tsdoc/src/nodes/DocExcerpt.ts
@@ -19,6 +19,7 @@ export enum ExcerptKind {
   DeclarationReference_PackageName = 'DeclarationReference_PackageName',
   DeclarationReference_ImportPath = 'DeclarationReference_ImportPath',
   DeclarationReference_ImportHash = 'DeclarationReference_ImportHash',
+  DeclarationReference_DeclarationReference = 'DeclarationReference_DeclarationReference',
 
   /**
    * Input characters that were reported as an error and do not appear to be part of a valid expression.
diff --git a/tsdoc/src/parser/NodeParser.ts b/tsdoc/src/parser/NodeParser.ts
index 6777fffc..a046da38 100644
--- a/tsdoc/src/parser/NodeParser.ts
+++ b/tsdoc/src/parser/NodeParser.ts
@@ -45,6 +45,7 @@ import { TSDocTagDefinition, TSDocTagSyntaxKind } from '../configuration/TSDocTa
 import { StandardTags } from '../details/StandardTags';
 import { PlainTextEmitter } from '../emitters/PlainTextEmitter';
 import { TSDocMessageId } from './TSDocMessageId';
+import { DeclarationReference } from '../beta/DeclarationReference';
 
 interface IFailure {
   // (We use "failureMessage" instead of "errorMessage" here so that DocErrorText doesn't
@@ -1251,6 +1252,53 @@ export class NodeParser {
     return !!parameters.codeDestination;
   }
 
+  private _parseBetaDeclarationReference(
+    tokenReader: TokenReader,
+    fallback: boolean
+  ): DocDeclarationReference | undefined {
+    tokenReader.assertAccumulatedSequenceIsEmpty();
+    const marker: number = tokenReader.createMarker();
+    try {
+      const declarationReference: DeclarationReference | undefined = DeclarationReference.tryParse(
+        tokenReader,
+        fallback
+      );
+      if (!tokenReader.isAccumulatedSequenceEmpty()) {
+        const declarationReferenceExcerpt: TokenSequence = tokenReader.extractAccumulatedSequence();
+        return new DocDeclarationReference({
+          parsed: true,
+          configuration: this._configuration,
+          declarationReferenceExcerpt,
+          declarationReference
+        });
+      }
+    } catch {
+      // do nothing
+    }
+
+    tokenReader.backtrackToMarker(marker);
+    return undefined;
+  }
+
+  private _tryParseBetaDeclarationReferenceAtMarker(
+    tokenReader: TokenReader,
+    marker: number
+  ): DocDeclarationReference | undefined {
+    if (this._parserContext.configuration.parseBetaDeclarationReferences === true) {
+      const betaReader: TokenReader = tokenReader.clone();
+      betaReader.backtrackToMarker(marker);
+      const node: DocDeclarationReference | undefined = this._parseBetaDeclarationReference(
+        betaReader,
+        /*fallback*/ true
+      );
+      if (node) {
+        tokenReader.updateFrom(betaReader);
+        return node;
+      }
+    }
+    return undefined;
+  }
+
   private _parseDeclarationReference(
     tokenReader: TokenReader,
     tokenSequenceForErrorContext: TokenSequence,
@@ -1258,10 +1306,22 @@ export class NodeParser {
   ): DocDeclarationReference | undefined {
     tokenReader.assertAccumulatedSequenceIsEmpty();
 
+    if (this._parserContext.configuration.parseBetaDeclarationReferences === 'prefer') {
+      const node: DocDeclarationReference | undefined = this._parseBetaDeclarationReference(
+        tokenReader,
+        /*fallback*/ false
+      );
+      if (node) {
+        return node;
+      }
+    }
+
+    const marker: number = tokenReader.createMarker();
+    const logMarker: number = this._parserContext.log.createMarker();
+
     // The package name can contain characters that look like a member reference.  This means we need to scan forwards
     // to see if there is a "#".  However, we need to be careful not to match a "#" that is part of a quoted expression.
 
-    const marker: number = tokenReader.createMarker();
     let hasHash: boolean = false;
 
     // A common mistake is to forget the "#" for package name or import path.  The telltale sign
@@ -1305,6 +1365,21 @@ export class NodeParser {
           // so don't set lookingForImportCharacters = false
           tokenReader.readToken();
           break;
+        case TokenKind.OtherPunctuation:
+          if (
+            this._parserContext.configuration.parseBetaDeclarationReferences === true &&
+            tokenReader.peekToken().toString() === '!'
+          ) {
+            // '!' has no meaning in a TSDoc declaration reference. This could be a beta DeclarationReference, so try to parse one
+            const node: DocDeclarationReference | undefined = this._tryParseBetaDeclarationReferenceAtMarker(
+              tokenReader,
+              marker
+            );
+            if (node) {
+              return node;
+            }
+          }
+        // falls through
         default:
           // Once we reach something other than AsciiWord and Period, then the meaning of
           // slashes and at-signs is no longer obvious.
@@ -1317,6 +1392,16 @@ export class NodeParser {
     if (!hasHash && sawImportCharacters) {
       // We saw characters that will be a syntax error if interpreted as a member reference,
       // but would make sense as a package name or import path, but we did not find a "#"
+
+      // This could be a beta DeclarationReference, so try to parse one
+      const node: DocDeclarationReference | undefined = this._tryParseBetaDeclarationReferenceAtMarker(
+        tokenReader,
+        marker
+      );
+      if (node) {
+        return node;
+      }
+
       this._parserContext.log.addMessageForTokenSequence(
         TSDocMessageId.ReferenceMissingHash,
         'The declaration reference appears to contain a package name or import path,' +
@@ -1428,6 +1513,15 @@ export class NodeParser {
       spacingAfterImportHashExcerpt = this._tryReadSpacingAndNewlines(tokenReader);
 
       if (packageNameExcerpt === undefined && importPathExcerpt === undefined) {
+        // This could be a beta DeclarationReference, so try to parse one
+        const node: DocDeclarationReference | undefined = this._tryParseBetaDeclarationReferenceAtMarker(
+          tokenReader,
+          marker
+        );
+        if (node) {
+          return node;
+        }
+
         this._parserContext.log.addMessageForTokenSequence(
           TSDocMessageId.ReferenceHashSyntax,
           'The hash character must be preceded by a package name or import path',
@@ -1459,6 +1553,16 @@ export class NodeParser {
           );
 
           if (!memberReference) {
+            // This could be a beta DeclarationReference, so try to parse one
+            const node: DocDeclarationReference | undefined = this._tryParseBetaDeclarationReferenceAtMarker(
+              tokenReader,
+              marker
+            );
+            if (node) {
+              this._parserContext.log.rollbackToMarker(logMarker);
+              return node;
+            }
+
             return undefined;
           }
 
@@ -1474,6 +1578,15 @@ export class NodeParser {
       importPathExcerpt === undefined &&
       memberReferences.length === 0
     ) {
+      // This could be a beta DeclarationReference, so try to parse one
+      const node: DocDeclarationReference | undefined = this._tryParseBetaDeclarationReferenceAtMarker(
+        tokenReader,
+        marker
+      );
+      if (node) {
+        return node;
+      }
+
       // We didn't find any parts of a declaration reference
       this._parserContext.log.addMessageForTokenSequence(
         TSDocMessageId.MissingReference,
diff --git a/tsdoc/src/parser/ParserMessageLog.ts b/tsdoc/src/parser/ParserMessageLog.ts
index a7b7a864..c50b7f0a 100644
--- a/tsdoc/src/parser/ParserMessageLog.ts
+++ b/tsdoc/src/parser/ParserMessageLog.ts
@@ -83,4 +83,20 @@ export class ParserMessageLog {
       })
     );
   }
+
+  /**
+   * Returns a value that can be used to rollback the message log to a specific point in time.
+   */
+  public createMarker(): number {
+    return this._messages.length;
+  }
+
+  /**
+   * Rolls back the message log to a specific point in time.
+   */
+  public rollbackToMarker(marker: number): void {
+    if (marker >= 0 && marker < this._messages.length) {
+      this._messages.length = marker;
+    }
+  }
 }
diff --git a/tsdoc/src/parser/TokenReader.ts b/tsdoc/src/parser/TokenReader.ts
index b7ad1a98..2d9aea21 100644
--- a/tsdoc/src/parser/TokenReader.ts
+++ b/tsdoc/src/parser/TokenReader.ts
@@ -204,4 +204,29 @@ export class TokenReader {
       this._accumulatedStartIndex = marker;
     }
   }
+
+  /**
+   * Create a copy of the token reader at the same position.
+   */
+  public clone(): TokenReader {
+    const clone: TokenReader = new TokenReader(this._parserContext);
+    clone._readerStartIndex = this._readerStartIndex;
+    clone._readerEndIndex = this._readerEndIndex;
+    clone._currentIndex = this._currentIndex;
+    clone._accumulatedStartIndex = this._accumulatedStartIndex;
+    return clone;
+  }
+
+  /**
+   * Update this reader to match the same position as another reader with the same context.
+   */
+  public updateFrom(other: TokenReader): void {
+    if (other._parserContext !== this._parserContext) {
+      throw new Error('The other token reader must use the same parser context');
+    }
+    this._readerStartIndex = other._readerStartIndex;
+    this._readerEndIndex = other._readerEndIndex;
+    this._currentIndex = other._currentIndex;
+    this._accumulatedStartIndex = other._accumulatedStartIndex;
+  }
 }
diff --git a/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts b/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts
index 7067b9d8..2f02eec0 100644
--- a/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts
+++ b/tsdoc/src/parser/__tests__/NodeParserLinkTag.test.ts
@@ -1,3 +1,4 @@
+import { TSDocConfiguration } from '../../configuration/TSDocConfiguration';
 import { TestHelpers } from './TestHelpers';
 
 test('00 Link text: positive examples', () => {
@@ -94,3 +95,18 @@ test('07 Declaration reference with import path only: negative examples', () =>
     ['/**', ' * {@link /path1#}', ' * {@link /path1 path2#}', ' */'].join('\n')
   );
 });
+
+test('08 Parse beta declaration references (fallback)', () => {
+  const config: TSDocConfiguration = new TSDocConfiguration();
+  config.parseBetaDeclarationReferences = true;
+  TestHelpers.parseAndMatchNodeParserSnapshot(
+    ['/**', ' * {@link foo!Bar:class}', ' * {@link Bar.foo}', ' */'].join('\n'),
+    config
+  );
+});
+
+test('09 Parse beta declaration references (preferred)', () => {
+  const config: TSDocConfiguration = new TSDocConfiguration();
+  config.parseBetaDeclarationReferences = 'prefer';
+  TestHelpers.parseAndMatchNodeParserSnapshot(['/**', ' * {@link Bar.foo}', ' */'].join('\n'), config);
+});
diff --git a/tsdoc/src/parser/__tests__/__snapshots__/NodeParserLinkTag.test.ts.snap b/tsdoc/src/parser/__tests__/__snapshots__/NodeParserLinkTag.test.ts.snap
index 6eafc8a1..86a5874d 100644
--- a/tsdoc/src/parser/__tests__/__snapshots__/NodeParserLinkTag.test.ts.snap
+++ b/tsdoc/src/parser/__tests__/__snapshots__/NodeParserLinkTag.test.ts.snap
@@ -1683,3 +1683,201 @@ Object {
   },
 }
 `;
+
+exports[`08 Parse beta declaration references (fallback) 1`] = `
+Object {
+  "buffer": "/**[n] * {@link foo!Bar:class}[n] * {@link Bar.foo}[n] */",
+  "gaps": Array [],
+  "lines": Array [
+    "{@link foo!Bar:class}",
+    "{@link Bar.foo}",
+  ],
+  "logMessages": Array [],
+  "nodes": Object {
+    "kind": "Comment",
+    "nodes": Array [
+      Object {
+        "kind": "Section",
+        "nodes": Array [
+          Object {
+            "kind": "Paragraph",
+            "nodes": Array [
+              Object {
+                "kind": "LinkTag",
+                "nodes": Array [
+                  Object {
+                    "kind": "Excerpt: InlineTag_OpeningDelimiter",
+                    "nodeExcerpt": "{",
+                  },
+                  Object {
+                    "kind": "Excerpt: InlineTag_TagName",
+                    "nodeExcerpt": "@link",
+                  },
+                  Object {
+                    "kind": "Excerpt: Spacing",
+                    "nodeExcerpt": " ",
+                  },
+                  Object {
+                    "kind": "DeclarationReference",
+                    "nodes": Array [
+                      Object {
+                        "kind": "Excerpt: DeclarationReference_DeclarationReference",
+                        "nodeExcerpt": "foo!Bar:class",
+                      },
+                    ],
+                  },
+                  Object {
+                    "kind": "Excerpt: InlineTag_ClosingDelimiter",
+                    "nodeExcerpt": "}",
+                  },
+                ],
+              },
+              Object {
+                "kind": "SoftBreak",
+                "nodes": Array [
+                  Object {
+                    "kind": "Excerpt: SoftBreak",
+                    "nodeExcerpt": "[n]",
+                  },
+                ],
+              },
+              Object {
+                "kind": "LinkTag",
+                "nodes": Array [
+                  Object {
+                    "kind": "Excerpt: InlineTag_OpeningDelimiter",
+                    "nodeExcerpt": "{",
+                  },
+                  Object {
+                    "kind": "Excerpt: InlineTag_TagName",
+                    "nodeExcerpt": "@link",
+                  },
+                  Object {
+                    "kind": "Excerpt: Spacing",
+                    "nodeExcerpt": " ",
+                  },
+                  Object {
+                    "kind": "DeclarationReference",
+                    "nodes": Array [
+                      Object {
+                        "kind": "MemberReference",
+                        "nodes": Array [
+                          Object {
+                            "kind": "MemberIdentifier",
+                            "nodes": Array [
+                              Object {
+                                "kind": "Excerpt: MemberIdentifier_Identifier",
+                                "nodeExcerpt": "Bar",
+                              },
+                            ],
+                          },
+                        ],
+                      },
+                      Object {
+                        "kind": "MemberReference",
+                        "nodes": Array [
+                          Object {
+                            "kind": "Excerpt: MemberReference_Dot",
+                            "nodeExcerpt": ".",
+                          },
+                          Object {
+                            "kind": "MemberIdentifier",
+                            "nodes": Array [
+                              Object {
+                                "kind": "Excerpt: MemberIdentifier_Identifier",
+                                "nodeExcerpt": "foo",
+                              },
+                            ],
+                          },
+                        ],
+                      },
+                    ],
+                  },
+                  Object {
+                    "kind": "Excerpt: InlineTag_ClosingDelimiter",
+                    "nodeExcerpt": "}",
+                  },
+                ],
+              },
+              Object {
+                "kind": "SoftBreak",
+                "nodes": Array [
+                  Object {
+                    "kind": "Excerpt: SoftBreak",
+                    "nodeExcerpt": "[n]",
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+}
+`;
+
+exports[`09 Parse beta declaration references (preferred) 1`] = `
+Object {
+  "buffer": "/**[n] * {@link Bar.foo}[n] */",
+  "gaps": Array [],
+  "lines": Array [
+    "{@link Bar.foo}",
+  ],
+  "logMessages": Array [],
+  "nodes": Object {
+    "kind": "Comment",
+    "nodes": Array [
+      Object {
+        "kind": "Section",
+        "nodes": Array [
+          Object {
+            "kind": "Paragraph",
+            "nodes": Array [
+              Object {
+                "kind": "LinkTag",
+                "nodes": Array [
+                  Object {
+                    "kind": "Excerpt: InlineTag_OpeningDelimiter",
+                    "nodeExcerpt": "{",
+                  },
+                  Object {
+                    "kind": "Excerpt: InlineTag_TagName",
+                    "nodeExcerpt": "@link",
+                  },
+                  Object {
+                    "kind": "Excerpt: Spacing",
+                    "nodeExcerpt": " ",
+                  },
+                  Object {
+                    "kind": "DeclarationReference",
+                    "nodes": Array [
+                      Object {
+                        "kind": "Excerpt: DeclarationReference_DeclarationReference",
+                        "nodeExcerpt": "Bar.foo",
+                      },
+                    ],
+                  },
+                  Object {
+                    "kind": "Excerpt: InlineTag_ClosingDelimiter",
+                    "nodeExcerpt": "}",
+                  },
+                ],
+              },
+              Object {
+                "kind": "SoftBreak",
+                "nodes": Array [
+                  Object {
+                    "kind": "Excerpt: SoftBreak",
+                    "nodeExcerpt": "[n]",
+                  },
+                ],
+              },
+            ],
+          },
+        ],
+      },
+    ],
+  },
+}
+`;