Template base para proyectos SaaS multi-tenant. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React/Vite) - apps/database (PostgreSQL DDL) - docs/ (Documentación) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1281 lines
42 KiB
JavaScript
1281 lines
42 KiB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
// See LICENSE in the project root for license information.
|
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
|
/* eslint-disable @typescript-eslint/naming-convention */
|
|
// NOTE: See DeclarationReference.grammarkdown for information on the underlying grammar.
|
|
import { StringChecks } from '../parser/StringChecks';
|
|
/**
|
|
* Represents a reference to a declaration.
|
|
* @beta
|
|
*/
|
|
export class DeclarationReference {
|
|
constructor(source, navigation, symbol) {
|
|
this._source = source;
|
|
this._navigation = navigation;
|
|
this._symbol = symbol;
|
|
}
|
|
get source() {
|
|
return this._source;
|
|
}
|
|
get navigation() {
|
|
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;
|
|
}
|
|
get symbol() {
|
|
return this._symbol;
|
|
}
|
|
get isEmpty() {
|
|
return this.source === undefined && this.symbol === undefined;
|
|
}
|
|
static parse(text) {
|
|
const parser = new Parser(text);
|
|
const reference = parser.parseDeclarationReference();
|
|
if (parser.errors.length) {
|
|
throw new SyntaxError(`Invalid DeclarationReference '${text}':\n ${parser.errors.join('\n ')}`);
|
|
}
|
|
if (!parser.eof) {
|
|
throw new SyntaxError(`Invalid DeclarationReference '${text}'`);
|
|
}
|
|
return reference;
|
|
}
|
|
static parseComponent(text) {
|
|
if (text[0] === '[') {
|
|
return ComponentReference.parse(text);
|
|
}
|
|
else {
|
|
return new ComponentString(text, true);
|
|
}
|
|
}
|
|
/**
|
|
* Determines whether the provided string is a well-formed symbol navigation component string.
|
|
*/
|
|
static isWellFormedComponentString(text) {
|
|
const scanner = new Scanner(text);
|
|
return scanner.scan() === Token.String
|
|
? scanner.scan() === Token.EofToken
|
|
: scanner.token() === Token.Text
|
|
? scanner.scan() === Token.EofToken
|
|
: scanner.token() === Token.EofToken;
|
|
}
|
|
/**
|
|
* Escapes a string for use as a symbol navigation component. If the string contains any of `!.#~:,"{}()@` or starts
|
|
* with `[`, it is enclosed in quotes.
|
|
*/
|
|
static escapeComponentString(text) {
|
|
if (text.length === 0) {
|
|
return '""';
|
|
}
|
|
const ch = text.charAt(0);
|
|
if (ch === '[' || ch === '"' || !this.isWellFormedComponentString(text)) {
|
|
return JSON.stringify(text);
|
|
}
|
|
return text;
|
|
}
|
|
/**
|
|
* Unescapes a string used as a symbol navigation component.
|
|
*/
|
|
static unescapeComponentString(text) {
|
|
if (text.length >= 2 && text.charAt(0) === '"' && text.charAt(text.length - 1) === '"') {
|
|
try {
|
|
return JSON.parse(text);
|
|
}
|
|
catch (_a) {
|
|
throw new SyntaxError(`Invalid Component '${text}'`);
|
|
}
|
|
}
|
|
if (!this.isWellFormedComponentString(text)) {
|
|
throw new SyntaxError(`Invalid Component '${text}'`);
|
|
}
|
|
return text;
|
|
}
|
|
/**
|
|
* Determines whether the provided string is a well-formed module source string. The string may not
|
|
* have a trailing `!` character.
|
|
*/
|
|
static isWellFormedModuleSourceString(text) {
|
|
const scanner = new Scanner(text + '!');
|
|
return (scanner.rescanModuleSource() === Token.ModuleSource &&
|
|
!scanner.stringIsUnterminated &&
|
|
scanner.scan() === Token.ExclamationToken &&
|
|
scanner.scan() === Token.EofToken);
|
|
}
|
|
/**
|
|
* Escapes a string for use as a module source. If the string contains any of `!"` it is enclosed in quotes.
|
|
*/
|
|
static escapeModuleSourceString(text) {
|
|
if (text.length === 0) {
|
|
return '""';
|
|
}
|
|
const ch = text.charAt(0);
|
|
if (ch === '"' || !this.isWellFormedModuleSourceString(text)) {
|
|
return JSON.stringify(text);
|
|
}
|
|
return text;
|
|
}
|
|
/**
|
|
* Unescapes a string used as a module source. The string may not have a trailing `!` character.
|
|
*/
|
|
static unescapeModuleSourceString(text) {
|
|
if (text.length >= 2 && text.charAt(0) === '"' && text.charAt(text.length - 1) === '"') {
|
|
try {
|
|
return JSON.parse(text);
|
|
}
|
|
catch (_a) {
|
|
throw new SyntaxError(`Invalid Module source '${text}'`);
|
|
}
|
|
}
|
|
if (!this.isWellFormedModuleSourceString(text)) {
|
|
throw new SyntaxError(`Invalid Module source '${text}'`);
|
|
}
|
|
return text;
|
|
}
|
|
static empty() {
|
|
return new DeclarationReference();
|
|
}
|
|
static package(packageName, importPath) {
|
|
return new DeclarationReference(ModuleSource.fromPackage(packageName, importPath));
|
|
}
|
|
static module(path, userEscaped) {
|
|
return new DeclarationReference(new ModuleSource(path, userEscaped));
|
|
}
|
|
static global() {
|
|
return new DeclarationReference(GlobalSource.instance);
|
|
}
|
|
static from(base) {
|
|
return base || this.empty();
|
|
}
|
|
withSource(source) {
|
|
return this._source === source ? this : new DeclarationReference(source, this._navigation, this._symbol);
|
|
}
|
|
withNavigation(navigation) {
|
|
return this._navigation === navigation
|
|
? this
|
|
: new DeclarationReference(this._source, navigation, this._symbol);
|
|
}
|
|
withSymbol(symbol) {
|
|
return this._symbol === symbol ? this : new DeclarationReference(this._source, this._navigation, symbol);
|
|
}
|
|
withComponentPath(componentPath) {
|
|
return this.withSymbol(this.symbol ? this.symbol.withComponentPath(componentPath) : new SymbolReference(componentPath));
|
|
}
|
|
withMeaning(meaning) {
|
|
if (!this.symbol) {
|
|
if (meaning === undefined) {
|
|
return this;
|
|
}
|
|
return this.withSymbol(SymbolReference.empty().withMeaning(meaning));
|
|
}
|
|
return this.withSymbol(this.symbol.withMeaning(meaning));
|
|
}
|
|
withOverloadIndex(overloadIndex) {
|
|
if (!this.symbol) {
|
|
if (overloadIndex === undefined) {
|
|
return this;
|
|
}
|
|
return this.withSymbol(SymbolReference.empty().withOverloadIndex(overloadIndex));
|
|
}
|
|
return this.withSymbol(this.symbol.withOverloadIndex(overloadIndex));
|
|
}
|
|
addNavigationStep(navigation, component) {
|
|
if (this.symbol) {
|
|
return this.withSymbol(this.symbol.addNavigationStep(navigation, component));
|
|
}
|
|
if (navigation === Navigation.Members) {
|
|
navigation = Navigation.Exports;
|
|
}
|
|
const symbol = new SymbolReference(new ComponentRoot(Component.from(component)));
|
|
return new DeclarationReference(this.source, navigation, symbol);
|
|
}
|
|
toString() {
|
|
const navigation = this._source instanceof ModuleSource && this._symbol && this.navigation === Navigation.Locals
|
|
? '~'
|
|
: '';
|
|
return `${this.source || ''}${navigation}${this.symbol || ''}`;
|
|
}
|
|
}
|
|
/**
|
|
* Indicates the symbol table from which to resolve the next symbol component.
|
|
* @beta
|
|
*/
|
|
export var Navigation;
|
|
(function (Navigation) {
|
|
Navigation["Exports"] = ".";
|
|
Navigation["Members"] = "#";
|
|
Navigation["Locals"] = "~";
|
|
})(Navigation || (Navigation = {}));
|
|
/**
|
|
* Represents a module.
|
|
* @beta
|
|
*/
|
|
export class ModuleSource {
|
|
constructor(path, userEscaped = true) {
|
|
this.escapedPath =
|
|
this instanceof ParsedModuleSource ? path : escapeModuleSourceIfNeeded(path, userEscaped);
|
|
}
|
|
get path() {
|
|
return this._path || (this._path = DeclarationReference.unescapeModuleSourceString(this.escapedPath));
|
|
}
|
|
get packageName() {
|
|
return this._getOrParsePathComponents().packageName;
|
|
}
|
|
get scopeName() {
|
|
const scopeName = this._getOrParsePathComponents().scopeName;
|
|
return scopeName ? '@' + scopeName : '';
|
|
}
|
|
get unscopedPackageName() {
|
|
return this._getOrParsePathComponents().unscopedPackageName;
|
|
}
|
|
get importPath() {
|
|
return this._getOrParsePathComponents().importPath || '';
|
|
}
|
|
static fromScopedPackage(scopeName, unscopedPackageName, importPath) {
|
|
let packageName = unscopedPackageName;
|
|
if (scopeName) {
|
|
if (scopeName.charAt(0) === '@') {
|
|
scopeName = scopeName.slice(1);
|
|
}
|
|
packageName = `@${scopeName}/${unscopedPackageName}`;
|
|
}
|
|
const parsed = { packageName, scopeName: scopeName || '', unscopedPackageName };
|
|
return this._fromPackageName(parsed, packageName, importPath);
|
|
}
|
|
static fromPackage(packageName, importPath) {
|
|
return this._fromPackageName(parsePackageName(packageName), packageName, importPath);
|
|
}
|
|
static _fromPackageName(parsed, packageName, importPath) {
|
|
if (!parsed) {
|
|
throw new Error('Parsed package must be provided.');
|
|
}
|
|
const packageNameError = StringChecks.explainIfInvalidPackageName(packageName);
|
|
if (packageNameError) {
|
|
throw new SyntaxError(`Invalid NPM package name: ${packageNameError}`);
|
|
}
|
|
let path = packageName;
|
|
if (importPath) {
|
|
if (invalidImportPathRegExp.test(importPath)) {
|
|
throw new SyntaxError(`Invalid import path '${importPath}`);
|
|
}
|
|
path += '/' + importPath;
|
|
parsed.importPath = importPath;
|
|
}
|
|
const source = new ModuleSource(path);
|
|
source._pathComponents = parsed;
|
|
return source;
|
|
}
|
|
toString() {
|
|
return `${this.escapedPath}!`;
|
|
}
|
|
_getOrParsePathComponents() {
|
|
if (!this._pathComponents) {
|
|
const path = this.path;
|
|
const parsed = parsePackageName(path);
|
|
if (parsed && !StringChecks.explainIfInvalidPackageName(parsed.packageName)) {
|
|
this._pathComponents = parsed;
|
|
}
|
|
else {
|
|
this._pathComponents = {
|
|
packageName: '',
|
|
scopeName: '',
|
|
unscopedPackageName: '',
|
|
importPath: path
|
|
};
|
|
}
|
|
}
|
|
return this._pathComponents;
|
|
}
|
|
}
|
|
class ParsedModuleSource extends ModuleSource {
|
|
}
|
|
// 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"]
|
|
// 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 = /^((?:@([^/]+?)\/)?([^/]+?))(?:\/(.+))?$/;
|
|
// no leading './' or '.\'
|
|
// no leading '../' or '..\'
|
|
// no leading '/' or '\'
|
|
// not '.' or '..'
|
|
const invalidImportPathRegExp = /^(\.\.?([\\/]|$)|[\\/])/;
|
|
// eslint-disable-next-line @rushstack/no-new-null
|
|
function parsePackageName(text) {
|
|
const match = packageNameRegExp.exec(text);
|
|
if (!match) {
|
|
return match;
|
|
}
|
|
const [, packageName = '', scopeName = '', unscopedPackageName = '', importPath] = match;
|
|
return { packageName, scopeName, unscopedPackageName, importPath };
|
|
}
|
|
/**
|
|
* Represents the global scope.
|
|
* @beta
|
|
*/
|
|
export class GlobalSource {
|
|
constructor() { }
|
|
toString() {
|
|
return '!';
|
|
}
|
|
}
|
|
GlobalSource.instance = new GlobalSource();
|
|
/**
|
|
* @beta
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace,@typescript-eslint/no-redeclare
|
|
export var Component;
|
|
(function (Component) {
|
|
function from(value) {
|
|
if (typeof value === 'string') {
|
|
return new ComponentString(value);
|
|
}
|
|
if (value instanceof DeclarationReference) {
|
|
return new ComponentReference(value);
|
|
}
|
|
return value;
|
|
}
|
|
Component.from = from;
|
|
})(Component || (Component = {}));
|
|
/**
|
|
* @beta
|
|
*/
|
|
export class ComponentString {
|
|
constructor(text, userEscaped) {
|
|
this.text = this instanceof ParsedComponentString ? text : escapeComponentIfNeeded(text, userEscaped);
|
|
}
|
|
toString() {
|
|
return this.text;
|
|
}
|
|
}
|
|
class ParsedComponentString extends ComponentString {
|
|
}
|
|
/**
|
|
* @beta
|
|
*/
|
|
export class ComponentReference {
|
|
constructor(reference) {
|
|
this.reference = reference;
|
|
}
|
|
static parse(text) {
|
|
if (text.length > 2 && text.charAt(0) === '[' && text.charAt(text.length - 1) === ']') {
|
|
return new ComponentReference(DeclarationReference.parse(text.slice(1, -1)));
|
|
}
|
|
throw new SyntaxError(`Invalid component reference: '${text}'`);
|
|
}
|
|
withReference(reference) {
|
|
return this.reference === reference ? this : new ComponentReference(reference);
|
|
}
|
|
toString() {
|
|
return `[${this.reference}]`;
|
|
}
|
|
}
|
|
/**
|
|
* @beta
|
|
*/
|
|
export class ComponentPathBase {
|
|
constructor(component) {
|
|
this.component = component;
|
|
}
|
|
addNavigationStep(navigation, component) {
|
|
// tslint:disable-next-line:no-use-before-declare
|
|
return new ComponentNavigation(this, navigation, Component.from(component));
|
|
}
|
|
}
|
|
/**
|
|
* @beta
|
|
*/
|
|
export class ComponentRoot extends ComponentPathBase {
|
|
withComponent(component) {
|
|
return this.component === component ? this : new ComponentRoot(Component.from(component));
|
|
}
|
|
toString() {
|
|
return this.component.toString();
|
|
}
|
|
}
|
|
/**
|
|
* @beta
|
|
*/
|
|
export class ComponentNavigation extends ComponentPathBase {
|
|
constructor(parent, navigation, component) {
|
|
super(component);
|
|
this.parent = parent;
|
|
this.navigation = navigation;
|
|
}
|
|
withParent(parent) {
|
|
return this.parent === parent ? this : new ComponentNavigation(parent, this.navigation, this.component);
|
|
}
|
|
withNavigation(navigation) {
|
|
return this.navigation === navigation
|
|
? this
|
|
: new ComponentNavigation(this.parent, navigation, this.component);
|
|
}
|
|
withComponent(component) {
|
|
return this.component === component
|
|
? this
|
|
: new ComponentNavigation(this.parent, this.navigation, Component.from(component));
|
|
}
|
|
toString() {
|
|
return `${this.parent}${formatNavigation(this.navigation)}${this.component}`;
|
|
}
|
|
}
|
|
/**
|
|
* @beta
|
|
*/
|
|
export var Meaning;
|
|
(function (Meaning) {
|
|
Meaning["Class"] = "class";
|
|
Meaning["Interface"] = "interface";
|
|
Meaning["TypeAlias"] = "type";
|
|
Meaning["Enum"] = "enum";
|
|
Meaning["Namespace"] = "namespace";
|
|
Meaning["Function"] = "function";
|
|
Meaning["Variable"] = "var";
|
|
Meaning["Constructor"] = "constructor";
|
|
Meaning["Member"] = "member";
|
|
Meaning["Event"] = "event";
|
|
Meaning["CallSignature"] = "call";
|
|
Meaning["ConstructSignature"] = "new";
|
|
Meaning["IndexSignature"] = "index";
|
|
Meaning["ComplexType"] = "complex"; // Any complex type
|
|
})(Meaning || (Meaning = {}));
|
|
/**
|
|
* Represents a reference to a TypeScript symbol.
|
|
* @beta
|
|
*/
|
|
export class SymbolReference {
|
|
constructor(component, { meaning, overloadIndex } = {}) {
|
|
this.componentPath = component;
|
|
this.overloadIndex = overloadIndex;
|
|
this.meaning = meaning;
|
|
}
|
|
static empty() {
|
|
return new SymbolReference(/*component*/ undefined);
|
|
}
|
|
withComponentPath(componentPath) {
|
|
return this.componentPath === componentPath
|
|
? this
|
|
: new SymbolReference(componentPath, {
|
|
meaning: this.meaning,
|
|
overloadIndex: this.overloadIndex
|
|
});
|
|
}
|
|
withMeaning(meaning) {
|
|
return this.meaning === meaning
|
|
? this
|
|
: new SymbolReference(this.componentPath, {
|
|
meaning,
|
|
overloadIndex: this.overloadIndex
|
|
});
|
|
}
|
|
withOverloadIndex(overloadIndex) {
|
|
return this.overloadIndex === overloadIndex
|
|
? this
|
|
: new SymbolReference(this.componentPath, {
|
|
meaning: this.meaning,
|
|
overloadIndex
|
|
});
|
|
}
|
|
addNavigationStep(navigation, component) {
|
|
if (!this.componentPath) {
|
|
throw new Error('Cannot add a navigation step to an empty symbol reference.');
|
|
}
|
|
return new SymbolReference(this.componentPath.addNavigationStep(navigation, component));
|
|
}
|
|
toString() {
|
|
let result = `${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;
|
|
}
|
|
}
|
|
var Token;
|
|
(function (Token) {
|
|
Token[Token["None"] = 0] = "None";
|
|
Token[Token["EofToken"] = 1] = "EofToken";
|
|
// Punctuator
|
|
Token[Token["OpenBraceToken"] = 2] = "OpenBraceToken";
|
|
Token[Token["CloseBraceToken"] = 3] = "CloseBraceToken";
|
|
Token[Token["OpenParenToken"] = 4] = "OpenParenToken";
|
|
Token[Token["CloseParenToken"] = 5] = "CloseParenToken";
|
|
Token[Token["OpenBracketToken"] = 6] = "OpenBracketToken";
|
|
Token[Token["CloseBracketToken"] = 7] = "CloseBracketToken";
|
|
Token[Token["ExclamationToken"] = 8] = "ExclamationToken";
|
|
Token[Token["DotToken"] = 9] = "DotToken";
|
|
Token[Token["HashToken"] = 10] = "HashToken";
|
|
Token[Token["TildeToken"] = 11] = "TildeToken";
|
|
Token[Token["ColonToken"] = 12] = "ColonToken";
|
|
Token[Token["CommaToken"] = 13] = "CommaToken";
|
|
Token[Token["AtToken"] = 14] = "AtToken";
|
|
Token[Token["DecimalDigits"] = 15] = "DecimalDigits";
|
|
Token[Token["String"] = 16] = "String";
|
|
Token[Token["Text"] = 17] = "Text";
|
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
Token[Token["ModuleSource"] = 18] = "ModuleSource";
|
|
// Keywords
|
|
Token[Token["ClassKeyword"] = 19] = "ClassKeyword";
|
|
Token[Token["InterfaceKeyword"] = 20] = "InterfaceKeyword";
|
|
Token[Token["TypeKeyword"] = 21] = "TypeKeyword";
|
|
Token[Token["EnumKeyword"] = 22] = "EnumKeyword";
|
|
Token[Token["NamespaceKeyword"] = 23] = "NamespaceKeyword";
|
|
Token[Token["FunctionKeyword"] = 24] = "FunctionKeyword";
|
|
Token[Token["VarKeyword"] = 25] = "VarKeyword";
|
|
Token[Token["ConstructorKeyword"] = 26] = "ConstructorKeyword";
|
|
Token[Token["MemberKeyword"] = 27] = "MemberKeyword";
|
|
Token[Token["EventKeyword"] = 28] = "EventKeyword";
|
|
Token[Token["CallKeyword"] = 29] = "CallKeyword";
|
|
Token[Token["NewKeyword"] = 30] = "NewKeyword";
|
|
Token[Token["IndexKeyword"] = 31] = "IndexKeyword";
|
|
Token[Token["ComplexKeyword"] = 32] = "ComplexKeyword"; // 'complex'
|
|
})(Token || (Token = {}));
|
|
function tokenToString(token) {
|
|
switch (token) {
|
|
case Token.OpenBraceToken:
|
|
return '{';
|
|
case Token.CloseBraceToken:
|
|
return '}';
|
|
case Token.OpenParenToken:
|
|
return '(';
|
|
case Token.CloseParenToken:
|
|
return ')';
|
|
case Token.OpenBracketToken:
|
|
return '[';
|
|
case Token.CloseBracketToken:
|
|
return ']';
|
|
case Token.ExclamationToken:
|
|
return '!';
|
|
case Token.DotToken:
|
|
return '.';
|
|
case Token.HashToken:
|
|
return '#';
|
|
case Token.TildeToken:
|
|
return '~';
|
|
case Token.ColonToken:
|
|
return ':';
|
|
case Token.CommaToken:
|
|
return ',';
|
|
case Token.AtToken:
|
|
return '@';
|
|
case Token.ClassKeyword:
|
|
return 'class';
|
|
case Token.InterfaceKeyword:
|
|
return 'interface';
|
|
case Token.TypeKeyword:
|
|
return 'type';
|
|
case Token.EnumKeyword:
|
|
return 'enum';
|
|
case Token.NamespaceKeyword:
|
|
return 'namespace';
|
|
case Token.FunctionKeyword:
|
|
return 'function';
|
|
case Token.VarKeyword:
|
|
return 'var';
|
|
case Token.ConstructorKeyword:
|
|
return 'constructor';
|
|
case Token.MemberKeyword:
|
|
return 'member';
|
|
case Token.EventKeyword:
|
|
return 'event';
|
|
case Token.CallKeyword:
|
|
return 'call';
|
|
case Token.NewKeyword:
|
|
return 'new';
|
|
case Token.IndexKeyword:
|
|
return 'index';
|
|
case Token.ComplexKeyword:
|
|
return 'complex';
|
|
case Token.None:
|
|
return '<none>';
|
|
case Token.EofToken:
|
|
return '<eof>';
|
|
case Token.DecimalDigits:
|
|
return '<decimal digits>';
|
|
case Token.String:
|
|
return '<string>';
|
|
case Token.Text:
|
|
return '<text>';
|
|
case Token.ModuleSource:
|
|
return '<module source>';
|
|
}
|
|
}
|
|
class Scanner {
|
|
constructor(text) {
|
|
this._pos = 0;
|
|
this._tokenPos = 0;
|
|
this._stringIsUnterminated = false;
|
|
this._token = Token.None;
|
|
this._text = text;
|
|
}
|
|
get stringIsUnterminated() {
|
|
return this._stringIsUnterminated;
|
|
}
|
|
get text() {
|
|
return this._text;
|
|
}
|
|
get tokenText() {
|
|
return this._text.slice(this._tokenPos, this._pos);
|
|
}
|
|
get eof() {
|
|
return this._pos >= this._text.length;
|
|
}
|
|
token() {
|
|
return this._token;
|
|
}
|
|
speculate(cb) {
|
|
const tokenPos = this._tokenPos;
|
|
const pos = this._pos;
|
|
const text = this._text;
|
|
const token = this._token;
|
|
const stringIsUnterminated = this._stringIsUnterminated;
|
|
let accepted = false;
|
|
try {
|
|
const accept = () => {
|
|
accepted = true;
|
|
};
|
|
return cb(accept);
|
|
}
|
|
finally {
|
|
if (!accepted) {
|
|
this._tokenPos = tokenPos;
|
|
this._pos = pos;
|
|
this._text = text;
|
|
this._token = token;
|
|
this._stringIsUnterminated = stringIsUnterminated;
|
|
}
|
|
}
|
|
}
|
|
scan() {
|
|
if (!this.eof) {
|
|
this._tokenPos = this._pos;
|
|
this._stringIsUnterminated = false;
|
|
while (!this.eof) {
|
|
const ch = this._text.charAt(this._pos++);
|
|
switch (ch) {
|
|
case '{':
|
|
return (this._token = Token.OpenBraceToken);
|
|
case '}':
|
|
return (this._token = Token.CloseBraceToken);
|
|
case '(':
|
|
return (this._token = Token.OpenParenToken);
|
|
case ')':
|
|
return (this._token = Token.CloseParenToken);
|
|
case '[':
|
|
return (this._token = Token.OpenBracketToken);
|
|
case ']':
|
|
return (this._token = Token.CloseBracketToken);
|
|
case '!':
|
|
return (this._token = Token.ExclamationToken);
|
|
case '.':
|
|
return (this._token = Token.DotToken);
|
|
case '#':
|
|
return (this._token = Token.HashToken);
|
|
case '~':
|
|
return (this._token = Token.TildeToken);
|
|
case ':':
|
|
return (this._token = Token.ColonToken);
|
|
case ',':
|
|
return (this._token = Token.CommaToken);
|
|
case '@':
|
|
return (this._token = Token.AtToken);
|
|
case '"':
|
|
this.scanString();
|
|
return (this._token = Token.String);
|
|
default:
|
|
this.scanText();
|
|
return (this._token = Token.Text);
|
|
}
|
|
}
|
|
}
|
|
return (this._token = Token.EofToken);
|
|
}
|
|
rescanModuleSource() {
|
|
switch (this._token) {
|
|
case Token.ModuleSource:
|
|
case Token.ExclamationToken:
|
|
case Token.EofToken:
|
|
return this._token;
|
|
}
|
|
return this.speculate((accept) => {
|
|
if (!this.eof) {
|
|
this._pos = this._tokenPos;
|
|
this._stringIsUnterminated = false;
|
|
let scanned = 'none';
|
|
while (!this.eof) {
|
|
const ch = this._text[this._pos];
|
|
if (ch === '!') {
|
|
if (scanned === 'none') {
|
|
return this._token;
|
|
}
|
|
accept();
|
|
return (this._token = Token.ModuleSource);
|
|
}
|
|
this._pos++;
|
|
if (ch === '"') {
|
|
if (scanned === 'other') {
|
|
// strings not allowed after scanning any other characters
|
|
return this._token;
|
|
}
|
|
scanned = 'string';
|
|
this.scanString();
|
|
}
|
|
else {
|
|
if (scanned === 'string') {
|
|
// no other tokens allowed after string
|
|
return this._token;
|
|
}
|
|
scanned = 'other';
|
|
if (!isPunctuator(ch)) {
|
|
this.scanText();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return this._token;
|
|
});
|
|
}
|
|
rescanMeaning() {
|
|
if (this._token === Token.Text) {
|
|
const tokenText = this.tokenText;
|
|
switch (tokenText) {
|
|
case 'class':
|
|
return (this._token = Token.ClassKeyword);
|
|
case 'interface':
|
|
return (this._token = Token.InterfaceKeyword);
|
|
case 'type':
|
|
return (this._token = Token.TypeKeyword);
|
|
case 'enum':
|
|
return (this._token = Token.EnumKeyword);
|
|
case 'namespace':
|
|
return (this._token = Token.NamespaceKeyword);
|
|
case 'function':
|
|
return (this._token = Token.FunctionKeyword);
|
|
case 'var':
|
|
return (this._token = Token.VarKeyword);
|
|
case 'constructor':
|
|
return (this._token = Token.ConstructorKeyword);
|
|
case 'member':
|
|
return (this._token = Token.MemberKeyword);
|
|
case 'event':
|
|
return (this._token = Token.EventKeyword);
|
|
case 'call':
|
|
return (this._token = Token.CallKeyword);
|
|
case 'new':
|
|
return (this._token = Token.NewKeyword);
|
|
case 'index':
|
|
return (this._token = Token.IndexKeyword);
|
|
case 'complex':
|
|
return (this._token = Token.ComplexKeyword);
|
|
}
|
|
}
|
|
return this._token;
|
|
}
|
|
rescanDecimalDigits() {
|
|
if (this._token === Token.Text) {
|
|
const tokenText = this.tokenText;
|
|
if (/^\d+$/.test(tokenText)) {
|
|
return (this._token = Token.DecimalDigits);
|
|
}
|
|
}
|
|
return this._token;
|
|
}
|
|
scanString() {
|
|
while (!this.eof) {
|
|
const ch = this._text.charAt(this._pos++);
|
|
switch (ch) {
|
|
case '"':
|
|
return;
|
|
case '\\':
|
|
this.scanEscapeSequence();
|
|
break;
|
|
default:
|
|
if (isLineTerminator(ch)) {
|
|
this._stringIsUnterminated = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
this._stringIsUnterminated = true;
|
|
}
|
|
scanEscapeSequence() {
|
|
if (this.eof) {
|
|
this._stringIsUnterminated = true;
|
|
return;
|
|
}
|
|
const ch = this._text.charAt(this._pos);
|
|
// EscapeSequence:: CharacterEscapeSequence
|
|
if (isCharacterEscapeSequence(ch)) {
|
|
this._pos++;
|
|
return;
|
|
}
|
|
// EscapeSequence:: `0` [lookahead != DecimalDigit]
|
|
if (ch === '0' &&
|
|
(this._pos + 1 === this._text.length || !isDecimalDigit(this._text.charAt(this._pos + 1)))) {
|
|
this._pos++;
|
|
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;
|
|
return;
|
|
}
|
|
// EscapeSequence:: UnicodeEscapeSequence
|
|
// 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))) {
|
|
this._pos += 5;
|
|
return;
|
|
}
|
|
// EscapeSequence:: UnicodeEscapeSequence
|
|
// UnicodeEscapeSequence:: `u` `{` CodePoint `}`
|
|
if (ch === 'u' && this._pos + 4 <= this._text.length && this._text.charAt(this._pos + 1) === '{') {
|
|
let hexDigits = this._text.charAt(this._pos + 2);
|
|
if (isHexDigit(hexDigits)) {
|
|
for (let i = this._pos + 3; i < this._text.length; i++) {
|
|
const ch2 = this._text.charAt(i);
|
|
if (ch2 === '}') {
|
|
const mv = parseInt(hexDigits, 16);
|
|
if (mv <= 0x10ffff) {
|
|
this._pos = i + 1;
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
if (!isHexDigit(ch2)) {
|
|
hexDigits += ch2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this._stringIsUnterminated = true;
|
|
}
|
|
scanText() {
|
|
while (this._pos < this._text.length) {
|
|
const ch = this._text.charAt(this._pos);
|
|
if (isPunctuator(ch) || ch === '"') {
|
|
return;
|
|
}
|
|
this._pos++;
|
|
}
|
|
}
|
|
}
|
|
class Parser {
|
|
constructor(text) {
|
|
this._errors = [];
|
|
this._scanner = new Scanner(text);
|
|
this._scanner.scan();
|
|
}
|
|
get eof() {
|
|
return this.token() === Token.EofToken;
|
|
}
|
|
get errors() {
|
|
return this._errors;
|
|
}
|
|
parseDeclarationReference() {
|
|
let source;
|
|
let navigation;
|
|
let symbol;
|
|
if (this.optionalToken(Token.ExclamationToken)) {
|
|
// Reference to global symbol
|
|
source = GlobalSource.instance;
|
|
}
|
|
else if (this._scanner.rescanModuleSource() === Token.ModuleSource) {
|
|
source = this.parseModuleSource();
|
|
// Check for optional `~` navigation token.
|
|
if (this.optionalToken(Token.TildeToken)) {
|
|
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);
|
|
}
|
|
parseModuleSourceString() {
|
|
this._scanner.rescanModuleSource();
|
|
return this.parseTokenString(Token.ModuleSource, 'Module source');
|
|
}
|
|
parseComponentString() {
|
|
switch (this._scanner.token()) {
|
|
case Token.String:
|
|
return this.parseString();
|
|
default:
|
|
return this.parseComponentCharacters();
|
|
}
|
|
}
|
|
token() {
|
|
return this._scanner.token();
|
|
}
|
|
parseModuleSource() {
|
|
const source = this.parseModuleSourceString();
|
|
this.expectToken(Token.ExclamationToken);
|
|
return new ParsedModuleSource(source, /*userEscaped*/ true);
|
|
}
|
|
parseSymbol() {
|
|
const component = this.parseComponentRest(this.parseRootComponent());
|
|
return this.parseSymbolRest(component);
|
|
}
|
|
parseSymbolRest(component) {
|
|
let meaning;
|
|
let overloadIndex;
|
|
if (this.optionalToken(Token.ColonToken)) {
|
|
meaning = this.tryParseMeaning();
|
|
overloadIndex = this.tryParseOverloadIndex(!!meaning);
|
|
}
|
|
return new SymbolReference(component, { meaning, overloadIndex });
|
|
}
|
|
parseRootComponent() {
|
|
if (!this.isStartOfComponent()) {
|
|
return this.fail('Component expected', new ComponentRoot(new ComponentString('', /*userEscaped*/ true)));
|
|
}
|
|
const component = this.parseComponent();
|
|
return new ComponentRoot(component);
|
|
}
|
|
parseComponentRest(component) {
|
|
for (;;) {
|
|
switch (this.token()) {
|
|
case Token.DotToken:
|
|
case Token.HashToken:
|
|
case Token.TildeToken:
|
|
const navigation = this.parseNavigation();
|
|
const right = this.parseComponent();
|
|
component = new ComponentNavigation(component, navigation, right);
|
|
break;
|
|
default:
|
|
return component;
|
|
}
|
|
}
|
|
}
|
|
parseNavigation() {
|
|
switch (this._scanner.token()) {
|
|
case Token.DotToken: {
|
|
this._scanner.scan();
|
|
return Navigation.Exports;
|
|
}
|
|
case Token.HashToken: {
|
|
this._scanner.scan();
|
|
return Navigation.Members;
|
|
}
|
|
case Token.TildeToken: {
|
|
this._scanner.scan();
|
|
return Navigation.Locals;
|
|
}
|
|
default: {
|
|
return this.fail("Expected '.', '#', or '~'", Navigation.Exports);
|
|
}
|
|
}
|
|
}
|
|
tryParseMeaning() {
|
|
switch (this._scanner.rescanMeaning()) {
|
|
case Token.ClassKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Class;
|
|
}
|
|
case Token.InterfaceKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Interface;
|
|
}
|
|
case Token.TypeKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.TypeAlias;
|
|
}
|
|
case Token.EnumKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Enum;
|
|
}
|
|
case Token.NamespaceKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Namespace;
|
|
}
|
|
case Token.FunctionKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Function;
|
|
}
|
|
case Token.VarKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Variable;
|
|
}
|
|
case Token.ConstructorKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Constructor;
|
|
}
|
|
case Token.MemberKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Member;
|
|
}
|
|
case Token.EventKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.Event;
|
|
}
|
|
case Token.CallKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.CallSignature;
|
|
}
|
|
case Token.NewKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.ConstructSignature;
|
|
}
|
|
case Token.IndexKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.IndexSignature;
|
|
}
|
|
case Token.ComplexKeyword: {
|
|
this._scanner.scan();
|
|
return Meaning.ComplexType;
|
|
}
|
|
}
|
|
}
|
|
tryParseOverloadIndex(hasMeaning) {
|
|
if (this.optionalToken(Token.OpenParenToken)) {
|
|
const overloadIndex = this.parseDecimalDigits();
|
|
this.expectToken(Token.CloseParenToken);
|
|
return overloadIndex;
|
|
}
|
|
else if (!hasMeaning) {
|
|
return this.parseDecimalDigits();
|
|
}
|
|
return undefined;
|
|
}
|
|
parseDecimalDigits() {
|
|
switch (this._scanner.rescanDecimalDigits()) {
|
|
case Token.DecimalDigits:
|
|
const value = +this._scanner.tokenText;
|
|
this._scanner.scan();
|
|
return value;
|
|
default:
|
|
return this.fail('Decimal digit expected', 0);
|
|
}
|
|
}
|
|
isStartOfComponent() {
|
|
switch (this.token()) {
|
|
case Token.Text:
|
|
case Token.String:
|
|
case Token.OpenBracketToken:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
parseComponentCharacters() {
|
|
let text = '';
|
|
for (;;) {
|
|
switch (this._scanner.token()) {
|
|
case Token.Text:
|
|
text += this.parseText();
|
|
break;
|
|
default:
|
|
return text;
|
|
}
|
|
}
|
|
}
|
|
parseTokenString(token, tokenString) {
|
|
if (this._scanner.token() === token) {
|
|
const text = this._scanner.tokenText;
|
|
const stringIsUnterminated = this._scanner.stringIsUnterminated;
|
|
this._scanner.scan();
|
|
if (stringIsUnterminated) {
|
|
return this.fail(`${tokenString || tokenToString(token)} is unterminated`, text);
|
|
}
|
|
return text;
|
|
}
|
|
return this.fail(`${tokenString || tokenToString(token)} expected`, '');
|
|
}
|
|
parseText() {
|
|
return this.parseTokenString(Token.Text, 'Text');
|
|
}
|
|
parseString() {
|
|
return this.parseTokenString(Token.String, 'String');
|
|
}
|
|
parseComponent() {
|
|
switch (this._scanner.token()) {
|
|
case Token.OpenBracketToken:
|
|
return this.parseBracketedComponent();
|
|
default:
|
|
return new ParsedComponentString(this.parseComponentString(), /*userEscaped*/ true);
|
|
}
|
|
}
|
|
parseBracketedComponent() {
|
|
this.expectToken(Token.OpenBracketToken);
|
|
const reference = this.parseDeclarationReference();
|
|
this.expectToken(Token.CloseBracketToken);
|
|
return new ComponentReference(reference);
|
|
}
|
|
optionalToken(token) {
|
|
if (this._scanner.token() === token) {
|
|
this._scanner.scan();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
expectToken(token, message) {
|
|
if (this._scanner.token() !== token) {
|
|
const expected = tokenToString(token);
|
|
const actual = tokenToString(this._scanner.token());
|
|
return this.fail(message || `Expected token '${expected}', received '${actual}' instead.`, undefined);
|
|
}
|
|
this._scanner.scan();
|
|
}
|
|
fail(message, fallback) {
|
|
this._errors.push(message);
|
|
return fallback;
|
|
}
|
|
}
|
|
function formatNavigation(navigation) {
|
|
switch (navigation) {
|
|
case Navigation.Exports:
|
|
return '.';
|
|
case Navigation.Members:
|
|
return '#';
|
|
case Navigation.Locals:
|
|
return '~';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
function isCharacterEscapeSequence(ch) {
|
|
return isSingleEscapeCharacter(ch) || isNonEscapeCharacter(ch);
|
|
}
|
|
function isSingleEscapeCharacter(ch) {
|
|
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) {
|
|
return !isEscapeCharacter(ch) && !isLineTerminator(ch);
|
|
}
|
|
function isEscapeCharacter(ch) {
|
|
switch (ch) {
|
|
case 'x':
|
|
case 'u':
|
|
return true;
|
|
default:
|
|
return isSingleEscapeCharacter(ch) || isDecimalDigit(ch);
|
|
}
|
|
}
|
|
function isLineTerminator(ch) {
|
|
switch (ch) {
|
|
case '\r':
|
|
case '\n':
|
|
// TODO: <LS>, <PS>
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
function isDecimalDigit(ch) {
|
|
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) {
|
|
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) {
|
|
switch (ch) {
|
|
case '{':
|
|
case '}':
|
|
case '(':
|
|
case ')':
|
|
case '[':
|
|
case ']':
|
|
case '!':
|
|
case '.':
|
|
case '#':
|
|
case '~':
|
|
case ':':
|
|
case ',':
|
|
case '@':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
function escapeComponentIfNeeded(text, userEscaped) {
|
|
if (userEscaped) {
|
|
if (!DeclarationReference.isWellFormedComponentString(text)) {
|
|
throw new SyntaxError(`Invalid Component '${text}'`);
|
|
}
|
|
return text;
|
|
}
|
|
return DeclarationReference.escapeComponentString(text);
|
|
}
|
|
function escapeModuleSourceIfNeeded(text, userEscaped) {
|
|
if (userEscaped) {
|
|
if (!DeclarationReference.isWellFormedModuleSourceString(text)) {
|
|
throw new SyntaxError(`Invalid Module source '${text}'`);
|
|
}
|
|
return text;
|
|
}
|
|
return DeclarationReference.escapeModuleSourceString(text);
|
|
}
|
|
//# sourceMappingURL=DeclarationReference.js.map
|