Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "api",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"cwd": "${workspaceFolder}/packages/api",
"args": ["story-source"],
"args": ["-t", "'class generics'"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
Expand Down
39 changes: 26 additions & 13 deletions packages/api-docs/src/props/full-prop-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class PropTypeNodes {

public resolvedProp(prop: PropType): PropType {
if (prop.parent) {
const parent = this.config.propLinks.getPropLink(prop.parent.name);
const key = prop.parent.token ? prop.parent.token : prop.parent.name;
const parent = this.config.propLinks.getPropLink(key);
if (parent && isClassLikeProp(parent)) {
const p = parent.properties?.find((p) => p.name === prop.name);
if (p) {
Expand Down Expand Up @@ -96,9 +97,10 @@ export class PropTypeNodes {
return collapsibleNode(typeProp, name);
}
private isLinkedProp(prop: PropType): boolean {
const propName = typeof prop.type === 'string' ? prop.type : prop.name;
if (propName) {
const linkedProp = this.config.propLinks.getPropLink(propName);
const key = prop.token ? prop.token : prop.name;

if (key) {
const linkedProp = this.config.propLinks.getPropLink(key);
if (linkedProp && linkedProp !== prop) {
return true;
}
Expand All @@ -107,17 +109,28 @@ export class PropTypeNodes {
}
private getType(prop: PropType): DocumentationNode[] {
const propName = typeof prop.type === 'string' ? prop.type : prop.name;
const token = prop.token;
let linkedProp: PropType | undefined;

if (typeof propName === 'string') {
const linkedProp = this.config.propLinks.getPropLink(propName);
if (linkedProp) {
return [
this.config.propLinks.propLink({
name: propName,
loc: linkedProp.loc,
}),
];
}
linkedProp = this.config.propLinks.getPropLink(propName);
}

// Prefer token lookup
if (typeof token === 'string') {
linkedProp = this.config.propLinks.getPropLink(token);
}

if (linkedProp && propName && token) {
return [
this.config.propLinks.propLink({
name: propName,
loc: linkedProp.loc,
token: linkedProp.token,
}),
];
}

return this.extractTypeNode(prop);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/api-docs/src/utility/prop-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export class PropLinks {
public propLink(prop: PropParent): DocumentationNode {
const typeText = inlineCodeNode(prop.name);
if (typeof prop.name === 'string') {
const link = this.getPropLink(prop.name);
const key = prop.token ? prop.token : prop.name;
const link = this.getPropLink(key);
return linkNode(
[typeText],
link ? `#${link.name?.toLowerCase()}` : undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@
},
"license": "MIT",
"dependencies": {
"object-hash": "^3.0.0",
"@structured-types/typescript-config": "^3.46.9",
"deepmerge": "^4.2.2",
"path-browserify": "^1.0.1"
},
"devDependencies": {
"@types/object-hash": "^2.2.1",
"@types/path-browserify": "^1.0.0",
"typescript": "^4.5.0"
},
Expand Down
177 changes: 110 additions & 67 deletions packages/api/src/SymbolParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { mergeNodeComments } from './jsdoc/mergeJSDoc';
import { parseJSDocTag } from './jsdoc/parseJSDocTags';
import { isTypeProp, ObjectProp } from './types';
import { ClassLikeProp, HasValueProp, SourcePositions } from '.';
import { createHash } from './create-hash';

export class SymbolParser implements ISymbolParser {
public checker: ts.TypeChecker;
Expand Down Expand Up @@ -96,15 +97,15 @@ export class SymbolParser implements ISymbolParser {

private addParentSymbol(
name: string,
node: ts.Node,
symbol: ts.Symbol,
options: ParseOptions,
) {
if (options.collectHelpers) {
if (!this._helpers[name]) {
const prop = { name };
this._helpers[name] = prop;
return this.addRefSymbol(prop, symbol, true);
}
const id = this.getId(node, name, options);
const prop = { name, token: id };
this._helpers[id] = prop;
return this.addRefSymbol(prop, symbol, true);
}
return undefined;
}
Expand All @@ -114,10 +115,17 @@ export class SymbolParser implements ISymbolParser {
symbol: ts.Symbol,
options: ParseOptions,
) {
return (
this.addParentSymbol(name, symbol, options) ||
this.addRefSymbol({ name }, symbol, false)
);
const declaration = getSymbolDeclaration(symbol);

if (declaration) {
const prop = this.addParentSymbol(name, declaration, symbol, options);

if (prop) {
return prop;
}
}

return this.addRefSymbol({ name }, symbol, false);
}
private getParent(
node: ts.Node,
Expand Down Expand Up @@ -154,8 +162,18 @@ export class SymbolParser implements ISymbolParser {
(typeof parentProp.type === 'string'
? parentProp.type
: undefined);
const propParent: PropParent = { name };
this.addParentSymbol(name, (parent as any).symbol, options);
let propParent: PropParent = { name };
const prop = this.addParentSymbol(
name,
parent,
(parent as any).symbol,
options,
);

if (prop) {
propParent = { name, token: prop.token };
}

if (parentName !== name) {
const loc = this.parseFilePath(options, false, parent);
if (loc) {
Expand Down Expand Up @@ -199,6 +217,7 @@ export class SymbolParser implements ISymbolParser {
},
};
}

private parseFilePath = (
options: ParseOptions,
isTopLevel: boolean,
Expand All @@ -210,71 +229,89 @@ export class SymbolParser implements ISymbolParser {
node &&
(isTopLevel || options.collectInnerLocations)
) {
const typeNode =
ts.isVariableDeclaration(node) &&
node.initializer &&
ts.isIdentifier(node.initializer)
? getSymbolDeclaration(this.getSymbolAtLocation(node.initializer))
: node;
if (typeNode) {
const source = typeNode.getSourceFile();
if (!location) {
location = {};
}
location.filePath = source.fileName;
location = this.getLocation(node, options);
}
return location;
};

private getId(node: ts.Node, fallback: string, options: ParseOptions) {
const location = this.getLocation(node, options);

if (location && location.filePath && location.loc) {
return createHash(fallback, {
filePath: location.filePath,
start: location.loc.start,
end: location.loc.end,
});
}

if (options.collectSourceInfo === 'body') {
const fn = getInitializer(typeNode) || typeNode;
return fallback;
}

private getLocation(node: ts.Node, options: ParseOptions) {
let location: SourceLocation | undefined = undefined;
const typeNode =
ts.isVariableDeclaration(node) &&
node.initializer &&
ts.isIdentifier(node.initializer)
? getSymbolDeclaration(this.getSymbolAtLocation(node.initializer))
: node;
if (typeNode) {
const source = typeNode.getSourceFile();
if (!location) {
location = {};
}
location.filePath = source.fileName;

if (options.collectSourceInfo === 'body') {
const fn = getInitializer(typeNode) || typeNode;
if (
ts.isArrowFunction(fn) ||
ts.isFunctionExpression(fn) ||
ts.isGetAccessorDeclaration(fn) ||
ts.isConstructorDeclaration(fn) ||
ts.isMethodDeclaration(fn) ||
ts.isFunctionDeclaration(fn)
) {
let startPost = fn.parameters.pos;
let start = source.getLineAndCharacterOfPosition(startPost);
const newLineChar =
ts.getDefaultFormatCodeSettings().newLineCharacter || /\r?\n/;
const line = fn.getSourceFile().text.split(newLineChar)[start.line];
if (
ts.isArrowFunction(fn) ||
ts.isFunctionExpression(fn) ||
ts.isGetAccessorDeclaration(fn) ||
ts.isConstructorDeclaration(fn) ||
ts.isMethodDeclaration(fn) ||
ts.isFunctionDeclaration(fn)
start.character > 0 &&
(line[start.character - 1] === '(' ||
line[start.character - 1] === ' ')
) {
let startPost = fn.parameters.pos;
let start = source.getLineAndCharacterOfPosition(startPost);
const newLineChar =
ts.getDefaultFormatCodeSettings().newLineCharacter || /\r?\n/;
const line = fn.getSourceFile().text.split(newLineChar)[start.line];
if (
start.character > 0 &&
(line[start.character - 1] === '(' ||
line[start.character - 1] === ' ')
) {
startPost -= 1;
start = source.getLineAndCharacterOfPosition(startPost);
}
while (
start.character < line.length &&
line[start.character] === ' '
) {
startPost += 1;
start = source.getLineAndCharacterOfPosition(startPost);
}
const end = source.getLineAndCharacterOfPosition(
(fn.body || getInitializer(fn) || fn).getEnd(),
);

location.loc = this.adjustLocation(start, end);
return location;
startPost -= 1;
start = source.getLineAndCharacterOfPosition(startPost);
}
}
const nameNode =
ts.getNameOfDeclaration(typeNode as ts.Declaration) || typeNode;
if (nameNode) {
const start = source.getLineAndCharacterOfPosition(
nameNode.getStart(),
while (
start.character < line.length &&
line[start.character] === ' '
) {
startPost += 1;
start = source.getLineAndCharacterOfPosition(startPost);
}
const end = source.getLineAndCharacterOfPosition(
(fn.body || getInitializer(fn) || fn).getEnd(),
);
const end = source.getLineAndCharacterOfPosition(nameNode.getEnd());

location.loc = this.adjustLocation(start, end);
return location;
}
}
const nameNode =
ts.getNameOfDeclaration(typeNode as ts.Declaration) || typeNode;
if (nameNode) {
const start = source.getLineAndCharacterOfPosition(nameNode.getStart());
const end = source.getLineAndCharacterOfPosition(nameNode.getEnd());
location.loc = this.adjustLocation(start, end);
return location;
}
}
return location;
};
}

public parseProperties(
properties: ts.NodeArray<
| ts.ClassElement
Expand Down Expand Up @@ -499,7 +536,13 @@ export class SymbolParser implements ISymbolParser {
if (this.internalSymbol(symbol) !== undefined) {
this.addRefSymbol({ name }, symbol, false);
} else {
this.addParentSymbol(name, symbol, options);
const node = getSymbolDeclaration(symbol);
if (node) {
const prop = this.addParentSymbol(name, node, symbol, options);
if (prop) {
p.token = prop.token!;
}
}
}
}
});
Expand Down
8 changes: 8 additions & 0 deletions packages/api/src/create-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import hash from 'object-hash';

export function createHash(
name: string,
content: Record<string, unknown>,
): string {
return `${name}:${hash(content).slice(0, 10)}`;
}
20 changes: 19 additions & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getSymbolDeclaration,
} from './ts-utils';
import { SymbolParser } from './SymbolParser';
import { createHash } from './create-hash';

export * from './jsdoc';
export * from './ts';
Expand Down Expand Up @@ -185,7 +186,23 @@ export const analyzeFiles = (
if (collectHelpers) {
// only return parents that are not already exported from the same file
const helpers: Record<string, PropType> = Object.keys(parser.helpers)
.filter((name) => parsed[name] === undefined)
.filter((helperName) => {
const { name, token } = parser.helpers[helperName];

if (options.collectHelpers && name) {
const parsedNode = parsed[name];
return (
parsedNode === undefined ||
createHash(name, {
filePath: parsedNode.loc?.filePath,
start: parsedNode.loc?.loc?.start,
end: parsedNode.loc?.loc?.end,
}) !== token
);
}

return parsed[helperName] === undefined;
})
.reduce((acc, name) => ({ ...acc, [name]: parser.helpers[name] }), {});
if (Object.keys(helpers).length) {
parsed = Object.keys(parsed).reduce((acc, key) => {
Expand All @@ -194,6 +211,7 @@ export const analyzeFiles = (
[key]: consolidateParentProps([parsed[key]], helpers)[0],
};
}, {});

parsed.__helpers = Object.keys(helpers).reduce((acc, key) => {
return {
...acc,
Expand Down
Loading