-
Notifications
You must be signed in to change notification settings - Fork 148
Adds ':call', ':new', and ':index' and fixes a parsing bug. #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds ':call', ':new', and ':index' and fixes a parsing bug. #173
Conversation
| `new` // SymbolFlags.Signature (for __new) | ||
| `index` // SymbolFlags.Signature (for __index) | ||
| `signature` // SymbolFlags.Signature (deprecated) | ||
| `type` // Any complex type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rbuckton In microsoft/rushstack#1406 (comment) I am proposing the following list of meanings:
callclassconstructorenumfunctionimember(coversMethodSignatureandPropertySignature)indexinterfacemember(coversEnumMember,Method, andProperty)namespacenewtype(replacestypealiasbecause I believe it's more intuitive to use TypeScript keywords than compiler internals)var
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imember- unnecessary,memberworks fine for these.var- I suppose its shorter thanvariabletype- There is already atypeand it has a different purpose thantypealias. "type" is also a very general term, while a "type alias" is what atypekeyword produces. I disagree that we should use the keyword over the semantic meaning here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imember- unnecessary,memberworks fine for these.
This seems like a counterexample:
interface X {
y: number; // X#y:member
}
class X {
public y: number; // X#y:member
}How would you handle it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as TypeScript is concerned, they are the same member. If you try to change the type of one to "string", you get a "duplicate identifier" error because they collide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
case in point:
interface X {
y: number;
}
interface X {
y: number; // change to `string` to get the same "duplicate identifier" error
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a counterproposal:
callforSyntaxKind.Signaturewith name__callnewforSyntaxKind.Signaturewith name__newindexforSyntaxKind.Signaturewith name__indexvariablebecomesvartypebecomescomplextypealiasbecomestypeenummemberbecomesmembermembersatisfies the following meanings:- Method
- MethodSignature
- Property
- PropertySignature
- GetAccessor
- SetAccessor
- EnumMember
There is a possible conflict in meanings for member, but it seems trivial as TypeScript still treats them as the same member:
class C {
get x(): number;
}
interface C {
x: number;
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it more, the class+interface merge is fairly bizarre. Nobody can implement the interface without also implementing the class members. It's a bit of a puzzle how the documentation system should even represent that. I agree that imember won't be needed or meaningful in practice.
👍 I'm good with this proposal.
| `call` // SymbolFlags.Signature (for __call) | ||
| `new` // SymbolFlags.Signature (for __new) | ||
| `index` // SymbolFlags.Signature (for __index) | ||
| `signature` // SymbolFlags.Signature (deprecated) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can simply delete signature. This API is "beta".
| return reference; | ||
| } | ||
|
|
||
| public static makeSafeComponent(text: string): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rbuckton in microsoft/rushstack#1406 (comment) I am asking that .addNavigationStep() should treat its input as an unquoted identifier, rather than parsing it.
Current design:
.addNavigationStep("[X]")-->[X](i.e. parsed as an ECMAScript symbol).addNavigationStep("[X")-->"[X"(i.e. a quoted identifier).addNavigationStep("\"X\"")-->"X".addNavigationStep("X")-->X
What I think is a better design:
.addNavigationStep("[X]")-->"[X]"(still an identifier, not reinterpreted as an ECMAScript symbol expression).addNavigationStep("[X")-->"[X".addNavigationStep("\"X\"")-->X(quotes removed because they are redundant).addNavigationStep("X")-->X
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My proposal should also eliminate the unexceptional exception (catch { // do nothing }).
We can probably use StringChecks.explainIfInvalidUnquotedIdentifier() to determine whether quotes are needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.addNavigationStep("[X]") -->"[X]"`
I completely disagree. One of the things I wanted to fix with all of this was having proper names for computed property names, as I have a large number of APIs that I am using api-extractor/api-documenter for that use unique symbol types, and I fully intended for this to address those use cases as well.
.addNavigationStep("\"X\"") -->X`
I also disagree with this. It should be what was written, or encoded if what was written isn't a valid component.
Consider a URL: If I create a URL from "http://foo.com/bar%2520baz" and decode it, then I end up with "http://foo.com/bar%20baz", which could easily be misinterpreted as "http://foo.com/bar baz" if you pass it to another API. However, if I create a URL from "http://foo.com/bar baz" it must be encoded as "http://foo.com/bar%20baz" because the space character is not permitted.
I think we should reserve quotes by default, so a quoted string containing a quoted string should be escaped:
.addNavigationStep("[X]")->[X].addNavigationStep("[X")->"[X".addNavigationStep("\"X\"")->"\"X\"".addNavigationStep("X")->X
If we need to pass through quotes, we should add an optional argument to addNavigationStep to indicate the string is already escaped (in which case we should only validate it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh... I did not realize that in your DeclarationReference API, .addNavigationStep("[X]") is the only way to make an ECMAScript symbol. I was confused because in the TSDoc parser, the AST and API have special nodes for representing an ECMAScript symbol and its internal components.
For example, this:
/** {@link my-control-library#Button.[UISymbols.toNumberPrimitive]} */...gets parsed as this (in the TSDoc playground):
- LinkTag
* InlineTag_OpeningDelimiter="{"
* InlineTag_TagName="@link"
* Spacing=" "
- DeclarationReference
* DeclarationReference_PackageName="my-control-library"
* DeclarationReference_ImportHash="#"
- MemberReference
- MemberIdentifier
* MemberIdentifier_Identifier="Button"
- MemberReference
* MemberReference_Dot="."
- MemberSymbol
* DocMemberSymbol_LeftBracket="["
- DeclarationReference
- MemberReference
- MemberIdentifier
* MemberIdentifier_Identifier="UISymbols"
- MemberReference
* MemberReference_Dot="."
- MemberIdentifier
* MemberIdentifier_Identifier="toNumberPrimitive"
* DocMemberSymbol_RightBracket="]"
* InlineTag_ClosingDelimiter="}"
The UISymbols.toNumberPrimitive substring becomes a second declaration reference nested inside the outer one, and that is what TSDoc's DocMemberSymbol constructor API receives.
For a "beta" prototype, I'm fine with your current approach of representing a symbol as a string, but we would need to fix it longer term. Otherwise it may be awkward for the caller to construct an input like [a.[b]."[c]"]. (The TypeScript compiler may provide utilities that help build expressions like this, but strictly speaking, it's making TypeScript expressions not UID expressions; there may be edge cases where the escaping rules differ. Ideally the TypeScript AST need to be transformed into TSDoc AST.)
I completely disagree. One of the things I wanted to fix with all of this was having proper names for computed property names, as I have a large number of APIs that I am using api-extractor/api-documenter for that use
unique symboltypes, and I fully intended for this to address those use cases as well.
I agree that ECMAScript symbols should be handled better by API Extractor. But the current implementation is somewhat of a hack. Here's how the name .api.json field ends up for some various inputs:
export declare const c: unique symbol;
export declare class X {
"a": string; // "name": "a"
"b[": string; // "name": "b["
[c]: string; // "name": "[c]"
d: string; // "name": "d"
"[e]": string; // "name": "[e]"
}The reason is that this code calls tryGetLateBoundName() only when a symbol is encountered, which is technically mixing escaping contexts.
If we pass this through to addNavigationStep() directly then "[e]" will get misinterpreted.
I think we should reserve quotes by default, so a quoted string containing a quoted string should be escaped:
.addNavigationStep("[X]")->[X].addNavigationStep("[X")->"[X".addNavigationStep("\"X\"")->"\"X\"".addNavigationStep("X")->XIf we need to pass through quotes, we should add an optional argument to
addNavigationStepto indicate the string is already escaped (in which case we should only validate it).
If we're saying that addNavigationStep() receives a TypeScript-like expression, then .addNavigationStep("[X") should throw an exception. It is not a valid expression. Silently adding quotes to it will hide an escaping mistake in the caller, which is likely to cause even more confusion. In my example above, API Extractor is escaping incorrectly. We need to fix that, not patch it up in one particular code path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TypeScript only allows late-bound symbols that can be referenced via an entity name, so only a or a.b.c are allowed. Element access is disallowed in an entity name, so a.[b]."[c]" wouldn't be supported.
If we pass this through to addNavigationStep() directly then "[e]" will get misinterpreted.
If that's true, then we should also always quote anything with [] and require the user to provide a value to an optional argument indicating the user did the escaping work. I will have something for that in an update shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed an update to PR microsoft/rushstack#1406 showing how the output looks with the new selectors.
I'm investigating how hard it is to get API Extractor to make the names like this:
export declare class X {
"a": string; // "name": "a"
"b[": string; // "name": "\"b[\""
[c]: string; // "name": "[c]"
d: string; // "name": "d"
"[e]": string; // "name": "\"[e]\""
}It may need to be a separate PR, since there seem to be a number of places where names are built (and currently only 2 places consider symbols).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heh quoting can be declaration-specific:
class X {
public "f1"(x: string): void;
public f1(x: boolean): void;
public 'f1'(x: string | boolean): void {
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to work okay (using StringChecks.explainIfInvalidUnquotedIdentifier:
if (StringChecks.explainIfInvalidUnquotedIdentifier(followedSymbol.name)) {
localName = JSON.stringify(followedSymbol.name); //<====
} else {
localName = followedSymbol.name;
}
if (TypeScriptHelpers.isWellKnownSymbolName(followedSymbol.name)) {
// TypeScript binds well-known ECMAScript symbols like "Symbol.iterator" as "__@iterator".
// This converts a string like "__@iterator" into the property name "[Symbol.iterator]".
localName = `[Symbol.${localName.slice(3)}]`;
} else {
const isUniqueSymbol: boolean = TypeScriptHelpers.isUniqueSymbolName(followedSymbol.name);
for (const declaration of followedSymbol.declarations || []) {
const declarationName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration);
if (declarationName && ts.isIdentifier(declarationName)) {
localName = declarationName.getText().trim();
break;
}
if (isUniqueSymbol && declarationName && ts.isComputedPropertyName(declarationName)) {
const lateBoundName: string | undefined = TypeScriptHelpers.tryGetLateBoundName(declarationName);
if (lateBoundName) {
localName = lateBoundName;
break;
}
}
}
}If you don't see any problems with it, I'll open a separate PR that moves this name-calculator into a utility, and fixes up everywhere that names get made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like symbols and malformed identifiers can only occur as (possibly static) members of a class or interface. Thus there are less places to worry about than I expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the PR: microsoft/rushstack#1410
octogonz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See notes
|
This update looks good. Post a comment whenever it's ready and we'll get it merged+published. |
Changes to BracketedComponent syntax
|
I'm done with changes for this PR. |
|
Before merging let me finish testing it against my API Extractor branch. |
|
As planned, API Extractor PR microsoft/rushstack#1406 now is generating JSON fields like this: export declare class X {
"a": string; // "name": "a"
"b[": string; // "name": "\"b[\""
[c.d]: string; // "name": "[c.d]"
e: string; // "name": "e"
"[f]": string; // "name": "\"[f]\""
}In the future the name will be generalized into a nesting JSON expression, but for right now the field is a text string (and is now consistently escaped). @rbuckton But now I cannot figure out how to use your API to assemble these names: const goal = DeclarationReference.parse('package!"a-b".[c.d].e');
console.log(goal.toString()) // package!"a-b".[c.d].e
const inputNames = ["package", '"a-b"', '[c-d]', 'e' ];
const step0 = DeclarationReference.package(inputNames[0]);
console.log(step0.toString()) // package!
const step1 = step0.addNavigationStep(Navigation.Exports, inputNames[1]);
console.log(step1.toString()) // package!"\"a-b\""
const step2 = step1.addNavigationStep(Navigation.Members, inputNames[2]);
console.log(step2.toString()) // package!"\"a-b\""#"[c-d]"
const step3 = step2.addNavigationStep(Navigation.Members, inputNames[3]);
console.log(step3.toString()) // package!"\"a-b\""#"[c-d]"#eWe want for Seems like the code below should fix it by unescaping const inputNames = ["package", '"a-b"', '[c-d]', 'e' ];
const step0 = DeclarationReference.package(inputNames[0]);
console.log(step0.toString()) // package!
const step1 = step0.addNavigationStep(Navigation.Exports, DeclarationReference.parse(inputNames[1]));
console.log(step1.toString()) // package!["a-b"]
const step2 = step1.addNavigationStep(Navigation.Members, DeclarationReference.parse(inputNames[2]));
console.log(step2.toString()) // package!["a-b"]#[[c-d]]
const step3 = step2.addNavigationStep(Navigation.Members, DeclarationReference.parse(inputNames[3]));
console.log(step3.toString()) // package!["a-b"]#[[c-d]]#[e]Is this a bug? I would expect |
|
@rbuckton In my PR I was able to get the correct output by doing this: ApiMethod.ts (ae-new-canonical-references branch) /** @beta @override */
public buildCanonicalReference(): DeclarationReference {
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(this.isStatic ? Navigation.Exports : Navigation.Members, this._getCanonicalReferenceName())
.withMeaning(Meaning.Member)
.withOverloadIndex(this.overloadIndex);
}ApiNameMixin.ts (ae-new-canonical-references branch): /** @internal */
public _getCanonicalReferenceName(): string | DeclarationReference {
const name: string = this.name;
if (name[0] === '"') {
return JSON.parse(name);
}
if (name[0] === '[') {
// Unwrap the [] and parse the reference
return DeclarationReference.parse(name.substr(1, name.length - 2));
}
return name;
}The resulting UIDs look pretty good. So we just need to sort out this API issue and then we can merge both PRs. |
|
Passing |
|
Consider this excerpt from api-documenter-test.api.json from my PR: {
"kind": "PropertySignature",
"canonicalReference": "api-documenter-test!IDocInterface3#[EcmaSmbols.example]:member",
"docComment": "/**\n * ECMAScript symbol\n */\n",
"excerptTokens": ...,
"releaseTag": "Public",
"name": "[EcmaSmbols.example]",
"propertyTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
}
},What kind of thing is the Allowable values:
Forbidden values:
Thus although a name expression string looks like a API Extractor generates a name expression using custom code in AstSymbolTable.getLocalNameForSymbol(). For now, this code doesn't need any help: The symbol strings are made by The awkward part is How would you feel about these changes?
|
That's actually how it works. Generally you would pass either a |
|
How to parse a |
|
A Component is either a ComponentString or a ComponentReference:
|
function parseComponent(s: string): Component {
if (s[0] === '[') return ComponentReference.parse(s);
return new ComponentString(s);
}Like this? Should this be a member of |
|
I added the |
|
@rbuckton FYI I noticed a bug that |
Adds ':call', ':new', and ':index' per microsoft/rushstack#1406 (comment).
Fixes a parsing bug per microsoft/rushstack#1406 (comment).