├── images ├── demo.png ├── vscode-demo.png ├── annotated-example-matrix.png └── controls-transitive-links.png ├── .gitignore ├── lerna.json ├── .prettierrc.json ├── packages ├── source-viz-vscode │ ├── .vscodeignore │ ├── icon.png │ ├── public │ │ ├── src │ │ │ ├── index.tsx │ │ │ └── components │ │ │ │ └── App.tsx │ │ ├── tsconfig.json │ │ └── webpack.config.js │ ├── .vscode │ │ ├── settings.json │ │ ├── tasks.json │ │ └── launch.json │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ └── src │ │ └── extension.ts ├── source-viz-core │ ├── src │ │ ├── components │ │ │ ├── matrix │ │ │ │ ├── constants.ts │ │ │ │ ├── Cell.tsx │ │ │ │ ├── Row.tsx │ │ │ │ ├── HoverHighlightGroup.tsx │ │ │ │ ├── Column.tsx │ │ │ │ ├── HoverTargetGroup.tsx │ │ │ │ ├── ZoomWrapper.tsx │ │ │ │ ├── MeasureText.tsx │ │ │ │ ├── Grid.tsx │ │ │ │ └── Matrix.tsx │ │ │ ├── controls │ │ │ │ ├── Radio.tsx │ │ │ │ ├── Checkbox.tsx │ │ │ │ ├── Controls.tsx │ │ │ │ ├── Sort.tsx │ │ │ │ ├── TransitiveLinkFilters.tsx │ │ │ │ └── ColumnFilters.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── ConfigurableMatrix.tsx │ │ │ └── SourceMatrix.tsx │ │ ├── index.ts │ │ ├── core │ │ │ ├── utils │ │ │ │ ├── getUniques.ts │ │ │ │ ├── getDescendants.ts │ │ │ │ ├── getUniques.spec.ts │ │ │ │ ├── getAncestors.ts │ │ │ │ └── getDescendants.spec.ts │ │ │ ├── types.ts │ │ │ ├── parsers │ │ │ │ └── typescript │ │ │ │ │ ├── class-based │ │ │ │ │ ├── analyzeClassSource.ts │ │ │ │ │ ├── getClassMembers.ts │ │ │ │ │ ├── analyzeClassSource.spec.ts │ │ │ │ │ └── getClassMembers.spec.ts │ │ │ │ │ ├── module-based │ │ │ │ │ ├── analyzeModuleSource.ts │ │ │ │ │ ├── getImports.ts │ │ │ │ │ ├── getTopLevelFunctions.ts │ │ │ │ │ ├── getImports.spec.ts │ │ │ │ │ ├── analyzeModuleSource.spec.ts │ │ │ │ │ ├── getTopLevelFunctionVariables.ts │ │ │ │ │ ├── getTopLevelFunctions.spec.ts │ │ │ │ │ └── getTopLevelFunctionVariables.spec.ts │ │ │ │ │ ├── getReferencingMethods.ts │ │ │ │ │ ├── TypescriptMember.ts │ │ │ │ │ ├── TypescriptAst.spec.ts │ │ │ │ │ ├── TypescriptMember.spec.ts │ │ │ │ │ └── TypescriptAst.ts │ │ │ ├── view-model │ │ │ │ ├── view-config.ts │ │ │ │ ├── matrix-model.ts │ │ │ │ ├── getSortByEmptyRowsLastFunc.ts │ │ │ │ ├── getMatrixModel.ts │ │ │ │ └── sort.ts │ │ │ ├── groupPublicMethodsToReferencedMembers.ts │ │ │ └── groupPublicMethodsToReferencedMembers.spec.ts │ │ └── getTypeEmoji.ts │ ├── tsconfig.json │ └── package.json └── source-viz-web │ ├── src │ ├── index.tsx │ ├── components │ │ ├── Title.tsx │ │ ├── SourceEditor.tsx │ │ ├── SourcePanel.tsx │ │ ├── GithubLogo.tsx │ │ ├── Description.tsx │ │ ├── App.tsx │ │ ├── Mode.tsx │ │ └── VsCodeLogo.tsx │ ├── examples.ts │ └── index.html │ ├── tsconfig.json │ ├── webpack.config.js │ └── package.json ├── tsconfig.json ├── package.json ├── .github └── workflows │ └── node.js.yml └── README.md /images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowchimp/source-viz/HEAD/images/demo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | input 4 | TODO.md 5 | sample-data.json 6 | out 7 | release.sh 8 | -------------------------------------------------------------------------------- /images/vscode-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowchimp/source-viz/HEAD/images/vscode-demo.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.8", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | **/src 3 | **/tsconfig.json 4 | **/webpack.config.js 5 | -------------------------------------------------------------------------------- /images/annotated-example-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowchimp/source-viz/HEAD/images/annotated-example-matrix.png -------------------------------------------------------------------------------- /images/controls-transitive-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowchimp/source-viz/HEAD/images/controls-transitive-links.png -------------------------------------------------------------------------------- /packages/source-viz-vscode/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowchimp/source-viz/HEAD/packages/source-viz-vscode/icon.png -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/constants.ts: -------------------------------------------------------------------------------- 1 | export const rowGridMargin = 6; 2 | export const columnGridMargin = 28; 3 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { AnalysisMode } from './core/view-model/view-config'; 2 | export { SourceMatrix } from './components/SourceMatrix'; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "sourceMap": true 6 | }, 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { App } from './components/App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/public/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { App } from './components/App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/source-viz-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2015", "es2016", "es2017", "dom"], 5 | "jsx": "react" 6 | }, 7 | "files": ["src/index.tsx"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/public/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["es2015", "es2016", "es2017", "dom"], 5 | "jsx": "react" 6 | }, 7 | "files": ["src/index.tsx"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/Cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function Cell({ x, y, cellWidth, cellHeight }) { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/utils/getUniques.ts: -------------------------------------------------------------------------------- 1 | function flatten(arr: T[][]): T[] { 2 | return arr.reduce((acc, val) => acc.concat(val), []); 3 | } 4 | 5 | export function getUniques(...args: T[][]) { 6 | return Array.from(new Set(flatten(args))); 7 | } 8 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/types.ts: -------------------------------------------------------------------------------- 1 | export enum MemberType { 2 | dependency = 'dependency', 3 | privateMethod = 'privateMethod', 4 | publicMethod = 'publicMethod', 5 | } 6 | 7 | export interface MemberInfo { 8 | label: string; 9 | type: MemberType; 10 | referencingMethods: string[]; 11 | } 12 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/public/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AnalysisMode, SourceMatrix } from 'source-viz-core'; 3 | 4 | export function App() { 5 | // @ts-ignore 6 | const source = window.scoutCode; 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /packages/source-viz-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "dist", 6 | "lib": ["es2015", "es2016", "es2017", "dom"], 7 | "jsx": "react" 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules", "**/*.spec.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "build": "lerna run build", 9 | "test": "lerna run test", 10 | "deploy": "lerna run deploy" 11 | }, 12 | "devDependencies": { 13 | "lerna": "^3.22.1", 14 | "prettier": "2.1.2", 15 | "typescript": "^3.9.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/utils/getDescendants.ts: -------------------------------------------------------------------------------- 1 | export function getDescendants(root: T, getChildren: (item: T) => T[]): T[] { 2 | const results = []; 3 | inner(root); 4 | return results; 5 | 6 | function inner(item: T): void { 7 | getChildren(item).forEach((x) => { 8 | if (!results.includes(x)) { 9 | results.push(x); 10 | inner(x); 11 | } 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/controls/Radio.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function Radio({ text, type, group, onChange, activeType }) { 4 | return ( 5 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/utils/getUniques.spec.ts: -------------------------------------------------------------------------------- 1 | import { getUniques } from './getUniques'; 2 | 3 | describe('getUniques', function () { 4 | it('gets uniques correctly', function () { 5 | const expected = [1, 7, 3, 5, 9, 8]; 6 | 7 | const result = getUniques([1, 7, 3], [5, 1], [9, 8, 3, 9]); 8 | 9 | expect(result).toHaveLength(expected.length); 10 | expect(result).toEqual(expect.arrayContaining(expected)); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/getTypeEmoji.ts: -------------------------------------------------------------------------------- 1 | import { MemberType } from './core/types'; 2 | 3 | export function getTypeEmoji(type: MemberType) { 4 | if (type == MemberType.dependency) { 5 | return '\u{1F4E6}'; // :package: 6 | } 7 | if (type == MemberType.privateMethod) { 8 | return '\u{1F510}'; // :closed_lock_with_key: 9 | } 10 | if (type == MemberType.publicMethod) { 11 | return '\u{1F513}'; // :unlock: 12 | } 13 | return ''; 14 | } 15 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { GithubLogo } from './GithubLogo'; 3 | import { VsCodeLogo } from './VsCodeLogo'; 4 | 5 | const pkg = require('../../package.json'); 6 | 7 | export function Title() { 8 | return ( 9 |

10 | Source Viz (v{pkg.version}){' '} 11 | 12 |

13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/public/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 2 | 3 | module.exports = { 4 | entry: './src/index.tsx', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/, 11 | }, 12 | ], 13 | }, 14 | resolve: { 15 | extensions: ['.tsx', '.ts', '.js'], 16 | }, 17 | plugins: [new CleanWebpackPlugin()], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/utils/getAncestors.ts: -------------------------------------------------------------------------------- 1 | export function getAncestors(node: T, getParent: (node: T) => T): T[] { 2 | const ancestors = []; 3 | forEachAncestor(node, (x) => ancestors.push(x)); 4 | return ancestors; 5 | 6 | function forEachAncestor(node: T, callback: (node: T) => void): void { 7 | const parent = getParent(node); 8 | if (!parent) { 9 | return; 10 | } 11 | callback(parent); 12 | forEachAncestor(parent, callback); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/Row.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { rowGridMargin } from './constants'; 3 | 4 | export function Row({ label, y, width, cellHeight }) { 5 | return ( 6 | 7 | 8 | 15 | {label} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "lib": ["es6"], 6 | "outDir": "out", 7 | "strict": true /* enable all strict type-checking options */ 8 | /* Additional Checks */ 9 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 10 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 11 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 12 | }, 13 | "files": ["src/extension.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/SourceEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { UnControlled as CodeMirror } from 'react-codemirror2'; 3 | import 'codemirror/mode/javascript/javascript'; 4 | 5 | export function SourceEditor({ initialCode, onChange, onSubmit }) { 6 | return ( 7 | onChange(value)} 10 | options={{ 11 | lineNumbers: true, 12 | mode: 'application/typescript', 13 | extraKeys: { 14 | 'Cmd-Enter': onSubmit, 15 | 'Alt-Enter': onSubmit, 16 | }, 17 | }} 18 | /> 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/SourcePanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { SourceEditor } from './SourceEditor'; 3 | 4 | export function SourcePanel({ initialCode, code, onCodeChange, onAnalyze }) { 5 | return ( 6 |
7 | 12 | 15 | {code && code.length && ( 16 |
{code.split('\n').length} lines
17 | )} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/class-based/analyzeClassSource.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { getClassMembers } from './getClassMembers'; 3 | import { MemberInfo } from '../../../types'; 4 | import { TypescriptAst } from '../TypescriptAst'; 5 | 6 | export function analyzeClassSource(code: string): MemberInfo[] { 7 | const ast = new TypescriptAst(code); 8 | const nodes = ast.nodes; 9 | const myClass = nodes.find(ts.isClassDeclaration); 10 | if (!myClass) { 11 | return []; 12 | } 13 | const members = getClassMembers(myClass, ast); 14 | return members.map((x) => x.getMemberInfo(ast, members)); 15 | } 16 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/analyzeModuleSource.ts: -------------------------------------------------------------------------------- 1 | import { MemberInfo } from '../../../types'; 2 | import { TypescriptAst } from '../TypescriptAst'; 3 | import { getImports } from './getImports'; 4 | import { getTopLevelFunctions } from './getTopLevelFunctions'; 5 | import { getTopLevelFunctionVariables } from './getTopLevelFunctionVariables'; 6 | 7 | export function analyzeModuleSource(code: string): MemberInfo[] { 8 | const ast = new TypescriptAst(code); 9 | const members = [ 10 | ...getImports(ast), 11 | ...getTopLevelFunctions(ast), 12 | ...getTopLevelFunctionVariables(ast), 13 | ]; 14 | return members.map((x) => x.getMemberInfo(ast, members)); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Use Node.js 12 | uses: actions/setup-node@v1 13 | with: 14 | node-version: 14 15 | - run: yarn install 16 | - run: yarn test 17 | - run: yarn run build 18 | - name: Deploy 19 | if: success() && startsWith( github.ref, 'refs/tags/v') 20 | run: npm run deploy 21 | env: 22 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 23 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 24 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 25 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/controls/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface CheckboxProps { 4 | text: string; 5 | count?: number; 6 | type: string; 7 | group: string; 8 | onChange: (value: string, checked: boolean) => void; 9 | activeTypes: Set; 10 | } 11 | 12 | export function Checkbox(props: CheckboxProps) { 13 | return ( 14 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/source-viz-web/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/index.tsx', 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.tsx?$/, 10 | use: 'ts-loader', 11 | exclude: /node_modules/, 12 | }, 13 | { 14 | test: /\.html$/, 15 | use: 'html-loader', 16 | }, 17 | ], 18 | }, 19 | resolve: { 20 | extensions: ['.tsx', '.ts', '.js'], 21 | }, 22 | plugins: [ 23 | new CleanWebpackPlugin(), 24 | new HtmlWebPackPlugin({ 25 | template: './src/index.html', 26 | filename: './index.html', 27 | cache: false, 28 | }), 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/HoverHighlightGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function HoverHighlightGroup({ 4 | x, 5 | y, 6 | cellWidth, 7 | cellHeight, 8 | width, 9 | height, 10 | }) { 11 | if (typeof x != 'number' || typeof y != 'number') { 12 | return null; 13 | } 14 | 15 | return ( 16 | 17 | 25 | , 26 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface ErrorBoundaryProps { 4 | renderError(error: Error): React.ReactNode; 5 | children: React.ReactNode; 6 | } 7 | 8 | interface ErrorBoundaryState { 9 | error?: Error; 10 | } 11 | 12 | export class ErrorBoundary extends React.Component< 13 | ErrorBoundaryProps, 14 | ErrorBoundaryState 15 | > { 16 | constructor(props) { 17 | super(props); 18 | this.state = {}; 19 | } 20 | 21 | static getDerivedStateFromError(error: Error) { 22 | return { 23 | error, 24 | }; 25 | } 26 | 27 | render() { 28 | if (this.state.error) { 29 | return this.props.renderError(this.state.error); 30 | } 31 | 32 | return this.props.children; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/view-model/view-config.ts: -------------------------------------------------------------------------------- 1 | import { MemberType } from '../types'; 2 | 3 | export enum AnalysisMode { 4 | class = 'class', 5 | module = 'module', 6 | } 7 | 8 | export enum SortType { 9 | alphabetical = 'alphabetical', 10 | random = 'random', 11 | louvain = 'louvain', 12 | } 13 | 14 | export interface ViewConfig { 15 | sort: SortType; 16 | columnFilters: Set; 17 | transitiveLinkFilters: Set; 18 | cellWidth: number; 19 | cellHeight: number; 20 | } 21 | 22 | export const defaultViewConfig: ViewConfig = { 23 | sort: SortType.louvain, 24 | columnFilters: new Set([MemberType.dependency, MemberType.privateMethod]), 25 | transitiveLinkFilters: new Set([MemberType.privateMethod]), 26 | cellWidth: 30, 27 | cellHeight: 30, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/Column.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { getTypeEmoji } from '../../getTypeEmoji'; 3 | import { columnGridMargin } from './constants'; 4 | 5 | export function Column({ label, type, x, height, cellWidth }) { 6 | return ( 7 | 8 | 9 | 16 | {label} 17 | 18 | 25 | {getTypeEmoji(type)} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/view-model/matrix-model.ts: -------------------------------------------------------------------------------- 1 | export interface RowItem { 2 | label: string; 3 | type: string; 4 | } 5 | 6 | export interface Link { 7 | row: string; 8 | column: RowItem; 9 | } 10 | 11 | export interface MatrixModel { 12 | rows: string[]; 13 | columns: RowItem[]; 14 | links: Link[]; 15 | } 16 | 17 | export function createMatrixModel( 18 | rowsToRowItems: Map, 19 | ): MatrixModel { 20 | const rows = [], 21 | columns = [], 22 | links = []; 23 | 24 | rowsToRowItems.forEach((rowItems, { label: row }) => { 25 | rows.push(row); 26 | rowItems.forEach((column) => { 27 | if (!columns.some((x) => x.label == column.label)) { 28 | columns.push(column); 29 | } 30 | links.push({ row, column }); 31 | }); 32 | }); 33 | 34 | return { rows, columns, links }; 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Source Viz 2 | 3 | **Source Viz** is a tool for visualizing the relationship between a module's public API and its implementation details. 4 | 5 | You can use it to 6 | * Make changes with confidence 7 | * Quickly get a sense of what a module does (i.e. what are its exported functions) and how (i.e. using which helper functions \ imports). 8 | * Find refactoring opportunities 9 | * Identify how to split-up a large file (e.g. if a single public method uses a completely different code path than the rest of the public methods). 10 | 11 | ![Demo screenshot](/images/demo.png) 12 | 13 | ## How to use it? 14 | 15 | **Source Viz** is available as 16 | 17 | * [A VSCode extension](https://marketplace.visualstudio.com/items?itemName=cowchimp.source-viz-vscode) 18 | * [An online playground \ Demo](https://source-viz.netlify.app) 19 | 20 | ## License 21 | 22 | MIT 23 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/getImports.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { TypescriptAst } from '../TypescriptAst'; 3 | import { MemberType } from '../../../types'; 4 | import { TypescriptMember } from '../TypescriptMember'; 5 | 6 | export function getImports(ast: TypescriptAst): TypescriptMember[] { 7 | const imports = ast.nodes.filter(ts.isImportClause); 8 | const importNamedDeclarations: ts.NamedDeclaration[] = imports.reduce( 9 | (acc, importClause) => { 10 | if (importClause.name) { 11 | acc.push(importClause); 12 | return acc; 13 | } 14 | 15 | if (ts.isNamespaceImport(importClause.namedBindings)) { 16 | acc.push(importClause.namedBindings); 17 | return acc; 18 | } 19 | 20 | return acc.concat(importClause.namedBindings.elements); 21 | }, 22 | [], 23 | ); 24 | return importNamedDeclarations.map( 25 | (x) => new TypescriptMember(x, MemberType.dependency), 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/source-viz-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source-viz-core", 3 | "private": true, 4 | "version": "0.2.8", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "dependencies": { 10 | "lodash.difference": "^4.5.0", 11 | "lodash.flatten": "^4.4.0", 12 | "lodash.range": "^3.2.0", 13 | "lodash.shuffle": "^4.2.0", 14 | "louvain": "^1.2.0", 15 | "react": "^16.3.2", 16 | "react-adopt": "^0.6.0", 17 | "react-svg-pan-zoom": "^3.8.0", 18 | "react-virtualized": "^9.22.2", 19 | "typescript": "^3.9.7" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^26.0.14", 23 | "@types/react": "^16.3.12", 24 | "jest": "^26.4.2", 25 | "ts-jest": "^26.3.0" 26 | }, 27 | "jest": { 28 | "transform": { 29 | "^.+\\.tsx?$": "ts-jest" 30 | }, 31 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 32 | "moduleFileExtensions": [ 33 | "ts", 34 | "tsx", 35 | "js", 36 | "jsx", 37 | "json", 38 | "node" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/GithubLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function GithubLogo() { 4 | return ( 5 | 6 | 13 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/view-model/getSortByEmptyRowsLastFunc.ts: -------------------------------------------------------------------------------- 1 | import { Link } from './matrix-model'; 2 | 3 | export function getSortByEmptyRowsLastFunc( 4 | rows: string[], 5 | links: Link[], 6 | ): (a: string, b: string) => number { 7 | const { emptyRows, nonEmptyRows } = splitEmptyAndNonEmptyRows(rows, links); 8 | 9 | return (a, b) => { 10 | if (emptyRows.includes(a) && nonEmptyRows.includes(b)) { 11 | return 1; 12 | } 13 | if (emptyRows.includes(b) && nonEmptyRows.includes(a)) { 14 | return -1; 15 | } 16 | return 0; 17 | }; 18 | 19 | function splitEmptyAndNonEmptyRows(rows: string[], links: Link[]) { 20 | return rows.reduce( 21 | (acc, row) => { 22 | if (isEmptyRow(row, links)) { 23 | acc.emptyRows.push(row); 24 | } else { 25 | acc.nonEmptyRows.push(row); 26 | } 27 | return acc; 28 | }, 29 | { emptyRows: [], nonEmptyRows: [] }, 30 | ); 31 | } 32 | 33 | function isEmptyRow(row: string, links: Link[]): boolean { 34 | return !links.some((x) => x.row === row); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/controls/Controls.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Sort } from './Sort'; 3 | import { ColumnFilters } from './ColumnFilters'; 4 | import { TransitiveLinkFilters } from './TransitiveLinkFilters'; 5 | import { ViewConfig } from '../../core/view-model/view-config'; 6 | import { MemberInfo } from '../../core/types'; 7 | 8 | interface ControlsProps { 9 | viewConfig: ViewConfig; 10 | onChange: (o: {}) => void; 11 | members: MemberInfo[]; 12 | } 13 | 14 | export function Controls(props: ControlsProps) { 15 | const { viewConfig, onChange, members } = props; 16 | 17 | return ( 18 |
19 | onChange({ columnFilters: x })} 22 | members={members} 23 | /> 24 | onChange({ sort: x })} /> 25 | onChange({ transitiveLinkFilters: x })} 28 | /> 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/view-model/getMatrixModel.ts: -------------------------------------------------------------------------------- 1 | import { sort } from './sort'; 2 | import { ViewConfig } from './view-config'; 3 | import { MemberInfo, MemberType } from '../types'; 4 | import { groupPublicMethodsToReferencedMembers } from '../groupPublicMethodsToReferencedMembers'; 5 | import { createMatrixModel, MatrixModel } from './matrix-model'; 6 | 7 | export function getMatrixModel( 8 | members: MemberInfo[], 9 | viewConfig: ViewConfig, 10 | ): MatrixModel { 11 | const membersToLinkedMembers = groupPublicMethodsToReferencedMembers( 12 | members, 13 | viewConfig.transitiveLinkFilters, 14 | ); 15 | 16 | filter(membersToLinkedMembers, viewConfig.columnFilters); 17 | 18 | const model = createMatrixModel(membersToLinkedMembers); 19 | 20 | sort(model, viewConfig.sort); 21 | 22 | return model; 23 | } 24 | 25 | function filter( 26 | model: Map, 27 | columnFilters: Set, 28 | ): void { 29 | model.forEach((value, key) => { 30 | return model.set( 31 | key, 32 | value.filter((y) => columnFilters.has(y.type)), 33 | ); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/HoverTargetGroup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as flatten from 'lodash.flatten'; 3 | import * as range from 'lodash.range'; 4 | 5 | export function HoverTargetGroup({ 6 | cellWidth, 7 | cellHeight, 8 | width, 9 | height, 10 | onMouseEnter, 11 | }) { 12 | const coordinates = flatten( 13 | range(0, width, cellHeight).map((x) => 14 | range(0, height, cellWidth).map((y) => ({ 15 | x, 16 | y, 17 | })), 18 | ), 19 | ); 20 | 21 | return ( 22 | 23 | {coordinates.map(({ x, y }) => 24 | HoverTarget({ 25 | x, 26 | y, 27 | cellWidth, 28 | cellHeight, 29 | onMouseEnter, 30 | }), 31 | )} 32 | 33 | ); 34 | } 35 | 36 | function HoverTarget({ x, y, cellWidth, cellHeight, onMouseEnter }) { 37 | return ( 38 | onMouseEnter(x, y)} 46 | /> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/source-viz-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source-viz-web", 3 | "private": true, 4 | "version": "0.2.8", 5 | "author": { 6 | "name": "cowchimp", 7 | "url": "http://blog.cowchimp.com" 8 | }, 9 | "repository": "cowchimp/source-viz", 10 | "scripts": { 11 | "start": "webpack -w --mode development --output ./dist/bundle.js --devtool inline-source-map", 12 | "build": "webpack --mode production --output ./dist/bundle.[hash].js", 13 | "deploy": "netlify deploy --dir=dist --prod" 14 | }, 15 | "dependencies": { 16 | "codemirror": "^5.18.2", 17 | "react": "^16.3.2", 18 | "react-codemirror2": "^5.1.0", 19 | "react-dom": "^16.3.2", 20 | "source-viz-core": "^0.2.8" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^16.3.12", 24 | "@types/react-dom": "^16.0.5", 25 | "clean-webpack-plugin": "^3.0.0", 26 | "html-loader": "^1.3.0", 27 | "html-webpack-plugin": "^4.4.1", 28 | "netlify-cli": "^2.65.5", 29 | "ts-loader": "^8.0.3", 30 | "typescript": "^3.9.7", 31 | "webpack": "^4.44.2", 32 | "webpack-cli": "^3.3.12" 33 | }, 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/getTopLevelFunctions.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as difference from 'lodash.difference'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | import { MemberType } from '../../../types'; 5 | import { TypescriptMember } from '../TypescriptMember'; 6 | 7 | export function getTopLevelFunctions(ast: TypescriptAst): TypescriptMember[] { 8 | const isPublic = (x: ts.Node) => 9 | x.modifiers && 10 | x.modifiers.some((m) => m.kind == ts.SyntaxKind.ExportKeyword); 11 | const topLevelStatements = ast.nodes.find(ts.isSourceFile) 12 | .statements; 13 | 14 | const functions = topLevelStatements.filter( 15 | ts.isFunctionDeclaration, 16 | ); 17 | const publicFunctions = functions.filter(isPublic); 18 | const privateFunctions = difference(functions, publicFunctions); 19 | 20 | return [ 21 | ...publicFunctions.map( 22 | (x) => new TypescriptMember(x, MemberType.publicMethod), 23 | ), 24 | ...privateFunctions.map( 25 | (x) => new TypescriptMember(x, MemberType.privateMethod), 26 | ), 27 | ]; 28 | } 29 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/utils/getDescendants.spec.ts: -------------------------------------------------------------------------------- 1 | import { getDescendants } from './getDescendants'; 2 | 3 | describe('getDescendants', function () { 4 | it('gets descendants correctly', function () { 5 | const map = new Map(); 6 | map.set('A', ['B', 'C']); 7 | map.set('B', ['C', 'D']); 8 | map.set('C', ['E']); 9 | map.set('E', ['F']); 10 | const expected = ['B', 'C', 'D', 'E', 'F']; 11 | 12 | const result = getDescendants('A', (item) => 13 | map.has(item) ? map.get(item) : [], 14 | ); 15 | 16 | expect(result).toHaveLength(expected.length); 17 | expect(result).toEqual(expect.arrayContaining(expected)); 18 | }); 19 | 20 | it('is not fazed by recursive links', function () { 21 | const map = new Map(); 22 | map.set('A', ['B', 'C']); 23 | map.set('B', ['C', 'D']); 24 | map.set('C', ['E', 'C']); 25 | map.set('E', ['F']); 26 | const expected = ['B', 'C', 'D', 'E', 'F']; 27 | 28 | const result = getDescendants('A', (item) => 29 | map.has(item) ? map.get(item) : [], 30 | ); 31 | 32 | expect(result).toHaveLength(expected.length); 33 | expect(result).toEqual(expect.arrayContaining(expected)); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/ZoomWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom'; 3 | 4 | interface ZoomWrapperProps { 5 | width: number; 6 | height: number; 7 | children: React.ReactNode; 8 | } 9 | 10 | export class ZoomWrapper extends React.Component { 11 | private Viewer: any; 12 | constructor(props) { 13 | super(props); 14 | this.Viewer = null; 15 | } 16 | 17 | componentDidMount() { 18 | this.Viewer.fitToViewer(); 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 | (this.Viewer = Viewer)} 28 | defaultTool="zoom-in" 29 | toolbarProps={{ position: 'left' }} 30 | miniatureProps={{ position: 'none' }} 31 | detectAutoPan={false} 32 | detectWheel={false} 33 | detectPinchGesture={true} 34 | preventPanOutside={true} 35 | background="transparent" 36 | SVGBackground="transparent" 37 | > 38 | {this.props.children} 39 | 40 | 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/controls/Sort.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Radio } from './Radio'; 3 | import { SortType } from '../../core/view-model/view-config'; 4 | 5 | interface SortProps { 6 | onChange: (x: SortType) => void; 7 | active: SortType; 8 | } 9 | 10 | export function Sort(props: SortProps) { 11 | const { onChange, active } = props; 12 | 13 | return ( 14 |
15 | Sort: 16 |
17 | 24 |
25 |
26 | {' '} 33 | 38 | (What is this?) 39 | 40 |
41 |
42 | 49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/ConfigurableMatrix.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { getMatrixModel } from '../core/view-model/getMatrixModel'; 3 | import { Controls } from './controls/Controls'; 4 | import { Matrix } from './matrix/Matrix'; 5 | import { defaultViewConfig, ViewConfig } from '../core/view-model/view-config'; 6 | import { MemberInfo } from '../core/types'; 7 | 8 | interface ConfigurableMatrixProps { 9 | members: MemberInfo[]; 10 | } 11 | 12 | export class ConfigurableMatrix extends React.Component< 13 | ConfigurableMatrixProps, 14 | ViewConfig 15 | > { 16 | constructor(props) { 17 | super(props); 18 | 19 | this.state = defaultViewConfig; 20 | } 21 | 22 | onViewConfigChange = (newState) => { 23 | this.setState(newState); 24 | }; 25 | 26 | getViewModel(members, viewConfig: ViewConfig) { 27 | return Object.assign( 28 | {}, 29 | getMatrixModel([].concat(members), viewConfig), 30 | viewConfig, 31 | ); 32 | } 33 | 34 | render() { 35 | if (!this.props.members || !this.props.members.length) { 36 | return null; 37 | } 38 | 39 | return [ 40 | , 46 | , 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/controls/TransitiveLinkFilters.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Checkbox } from './Checkbox'; 3 | import { MemberType } from '../../core/types'; 4 | import { getTypeEmoji } from '../../getTypeEmoji'; 5 | 6 | interface TransitiveLinkFilterProps { 7 | onChange: (x: Set) => void; 8 | active: Set; 9 | } 10 | 11 | export function TransitiveLinkFilters(props: TransitiveLinkFilterProps) { 12 | return ( 13 |
14 | Transitive links: 15 |
16 | 23 |
24 |
25 | 32 |
33 |
34 | ); 35 | 36 | function onChange(value: MemberType, checked: boolean): void { 37 | const newActiveFilters = new Set(props.active); 38 | newActiveFilters[checked ? 'add' : 'delete'](value); 39 | props.onChange(newActiveFilters); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/getReferencingMethods.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { TypescriptMember } from './TypescriptMember'; 3 | import { TypescriptAst } from './TypescriptAst'; 4 | import { getAncestors } from '../../utils/getAncestors'; 5 | import { MemberType } from '../../types'; 6 | 7 | const validReferencingMemberTypes = [ 8 | MemberType.privateMethod, 9 | MemberType.publicMethod, 10 | ]; 11 | 12 | export function getReferencingMethods( 13 | node: ts.NamedDeclaration, 14 | members: TypescriptMember[], 15 | ast: TypescriptAst, 16 | ): ts.NamedDeclaration[] { 17 | if (!node.name || !ts.isIdentifier(node.name) || !members.length) { 18 | return []; 19 | } 20 | 21 | const potentialReferencingMethods = members 22 | .filter((x) => validReferencingMemberTypes.includes(x.type)) 23 | .map((x) => x.node); 24 | 25 | const references = ast.getReferences(node.name); 26 | return Array.from( 27 | references.reduce((acc, reference) => { 28 | const ancestors = getAncestors(reference, (node) => ast.getParent(node)); 29 | if (ancestors.includes(node)) { 30 | return acc; 31 | } 32 | const referencingMethod = potentialReferencingMethods.find((x) => 33 | ancestors.includes(x), 34 | ); 35 | if (referencingMethod) { 36 | acc.add(referencingMethod); 37 | } 38 | return acc; 39 | }, new Set()), 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/MeasureText.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface MeasureTextProps { 4 | strings: { [k: string]: string }; 5 | className: string; 6 | children(measurements: { [k: string]: number }): React.ReactNode; 7 | } 8 | 9 | export class MeasureText extends React.Component { 10 | myRef = React.createRef(); 11 | 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = {}; 16 | } 17 | 18 | componentDidMount() { 19 | const children = Array.from(this.myRef.current.children); 20 | const textElements = children.filter(isSvgTextElement); 21 | const measurements = textElements.reduce((acc, item) => { 22 | const textLength = item.getComputedTextLength(); 23 | acc[item.dataset.key] = textLength; 24 | return acc; 25 | }, {}); 26 | this.setState({ measurements }); 27 | } 28 | 29 | render() { 30 | return !this.state.measurements ? ( 31 | 32 | {Object.entries(this.props.strings).map(([key, value]) => ( 33 | 34 | {value} 35 | 36 | ))} 37 | 38 | ) : ( 39 |
{this.props.children(this.state.measurements)}
40 | ); 41 | } 42 | } 43 | 44 | function isSvgTextElement(elem: Element): elem is SVGTextElement { 45 | return elem.nodeName === 'text'; 46 | } 47 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/TypescriptMember.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { MemberInfo, MemberType } from '../../types'; 3 | import { TypescriptAst } from './TypescriptAst'; 4 | import { getReferencingMethods } from './getReferencingMethods'; 5 | 6 | export class TypescriptMember { 7 | constructor(public node: ts.NamedDeclaration, public type: MemberType) {} 8 | 9 | public getMemberInfo( 10 | ast: TypescriptAst, 11 | members: TypescriptMember[] = [], 12 | ): MemberInfo { 13 | return { 14 | label: this.getLabel(this.node, ast), 15 | type: this.type, 16 | referencingMethods: getReferencingMethods( 17 | this.node, 18 | members, 19 | ast, 20 | ).map((x) => this.getLabel(x, ast)), 21 | }; 22 | } 23 | 24 | private getLabel(node: ts.NamedDeclaration, ast: TypescriptAst): string { 25 | if ( 26 | this.isAnonymousDefaultFunction(node) || 27 | this.isAnonymousDefaultArrowFunction(node) 28 | ) { 29 | return 'default'; 30 | } 31 | return ast.getName(node); 32 | } 33 | 34 | private isAnonymousDefaultFunction(node: ts.NamedDeclaration) { 35 | return ( 36 | ts.isFunctionDeclaration(node) && 37 | !node.name && 38 | node.modifiers.some((m) => m.kind == ts.SyntaxKind.DefaultKeyword) 39 | ); 40 | } 41 | 42 | private isAnonymousDefaultArrowFunction(node: ts.NamedDeclaration) { 43 | return ts.isExportAssignment(node); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/TypescriptAst.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { TypescriptAst } from './TypescriptAst'; 3 | 4 | const code = `class Foo { 5 | private depC; 6 | 7 | constructor( 8 | private depA, 9 | private depB 10 | ) { 11 | 12 | } 13 | 14 | methodA() { 15 | return this.methodC(); 16 | } 17 | 18 | public methodB() { 19 | console.log(this.methodA()) 20 | const that = this; 21 | if (true) { 22 | that.depB.init(); 23 | } 24 | } 25 | 26 | private methodC() { 27 | return this.depA.init(); 28 | } 29 | 30 | public methodD() { 31 | this.depB.init(); 32 | } 33 | 34 | private methodE() { 35 | return this.methodC(); 36 | } 37 | 38 | public methodF() { 39 | return this.methodE(); 40 | } 41 | } 42 | 43 | class Bar { 44 | constructor( 45 | private foo: Foo 46 | ) {} 47 | 48 | methodA() { 49 | console.log(this.foo.methodA()); 50 | } 51 | }`; 52 | 53 | describe('TypescriptAst', function () { 54 | it('returns parent correctly', function () { 55 | const ast = new TypescriptAst(code); 56 | const myClass = ast.nodes.find(ts.isClassDeclaration); 57 | const methodA = myClass.members 58 | .filter(ts.isMethodDeclaration) 59 | .find((x) => ts.isIdentifier(x.name) && x.name.text == 'methodA'); 60 | 61 | const result = ast.getParent(methodA); 62 | 63 | expect(result).toBe(myClass); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/examples.ts: -------------------------------------------------------------------------------- 1 | import { AnalysisMode } from 'source-viz-core'; 2 | 3 | export const examples = { 4 | [AnalysisMode.class]: `class Foo { 5 | private depC; 6 | 7 | constructor( 8 | private depA, 9 | private depB 10 | ) { 11 | 12 | } 13 | 14 | methodA() { 15 | return this.methodC(); 16 | } 17 | 18 | public methodB() { 19 | console.log(this.methodA()) 20 | const that = this; 21 | if (true) { 22 | that.depB.init(); 23 | } 24 | } 25 | 26 | private methodC() { 27 | return this.depA.init(); 28 | } 29 | 30 | public methodD() { 31 | this.depB.init(); 32 | } 33 | 34 | private methodE() { 35 | return this.methodC(); 36 | } 37 | 38 | public methodF() { 39 | return this.methodE(); 40 | } 41 | } 42 | 43 | class Bar { 44 | constructor( 45 | private foo: Foo 46 | ) {} 47 | 48 | methodA() { 49 | console.log(this.foo.methodA()); 50 | } 51 | }`, 52 | [AnalysisMode.module]: `import depA from './depA'; 53 | import depB from './depB'; 54 | 55 | const depC; 56 | 57 | export function methodA() { 58 | return methodC(); 59 | } 60 | 61 | export function methodB() { 62 | console.log(methodA()); 63 | if (true) { 64 | depB.init(); 65 | } 66 | } 67 | 68 | function methodC() { 69 | return depA.init(); 70 | } 71 | 72 | export function methodD() { 73 | depB.init(); 74 | } 75 | 76 | function methodE() { 77 | return methodC(); 78 | } 79 | 80 | export function methodF() { 81 | return methodE(); 82 | }`, 83 | }; 84 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/getImports.spec.ts: -------------------------------------------------------------------------------- 1 | import { getImports } from './getImports'; 2 | import { MemberType } from '../../../types'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | 5 | describe('getImports', function () { 6 | it('returns default imports as dependencies', function () { 7 | const code = `import foo from './foo'`; 8 | const ast = new TypescriptAst(code); 9 | const result = getImports(ast).map((x) => x.getMemberInfo(ast)); 10 | expect(result).toEqual([ 11 | expect.objectContaining({ 12 | label: 'foo', 13 | type: MemberType.dependency, 14 | }), 15 | ]); 16 | }); 17 | 18 | it('returns named imports as dependencies', function () { 19 | const code = `import { foo, bar } from './foo-bar'`; 20 | const ast = new TypescriptAst(code); 21 | const result = getImports(ast).map((x) => x.getMemberInfo(ast)); 22 | expect(result).toEqual([ 23 | expect.objectContaining({ 24 | label: 'foo', 25 | type: MemberType.dependency, 26 | }), 27 | expect.objectContaining({ 28 | label: 'bar', 29 | type: MemberType.dependency, 30 | }), 31 | ]); 32 | }); 33 | 34 | it('returns namespace imports as dependencies', function () { 35 | const code = `import * as Foo from './Foo'`; 36 | const ast = new TypescriptAst(code); 37 | const result = getImports(ast).map((x) => x.getMemberInfo(ast)); 38 | expect(result).toEqual([ 39 | expect.objectContaining({ 40 | label: 'Foo', 41 | type: MemberType.dependency, 42 | }), 43 | ]); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/Description.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function Description() { 4 | return ( 5 |
6 |

7 | Source Viz is a tool for analyzing and visualizing the relationship 8 | between the public API of a Module\Class and its implementation details 9 | (e.g. private methods, dependencies used). 10 |

11 |

12 | Ideally this visualization can help you spot logical split-points in a 13 | huge Module\Class file by highlighting how one set of public methods 14 | uses completely different code than a different set. 15 |

16 |

17 | To use it just paste your code below and click Analyze. 18 |

19 |

20 | Results are displayed in an{' '} 21 | 25 | Adjacency Matrix 26 | 27 | . 28 |

29 |

30 | Right now it supports analyzing{' '} 31 | 32 | Typescript 33 | {' '} 34 | (and therefore Javascript as well). 35 |
36 | Support for other programming languages is also planned. 37 |

38 |

39 | Feel free to open up an issue on{' '} 40 | 41 | github 42 | {' '} 43 | or reach out on{' '} 44 | 45 | twitter 46 | 47 | . 48 |

49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/README.md: -------------------------------------------------------------------------------- 1 | **Source Viz** is a VSCode extension for visualizing the relationship between a module's public API and its implementation details. 2 | 3 | You can use it to 4 | * Make changes with confidence 5 | * Quickly get a sense of what a module does (i.e. what are its exported functions) and how (i.e. using which helper functions \ imports). 6 | * Find refactoring opportunities 7 | * Identify how to split-up a large file (e.g. if a single public method uses a completely different code path than the rest of the public methods). 8 | 9 | ## Functionality 10 | 11 | Press Cmd\Ctrl+Shift+P and select "**Open Source Viz**" to see the diagram of the file you are currently editing. 12 | 13 | ![Demo screenshot](/images/vscode-demo.png) 14 | 15 | ## Interpreting the results 16 | 17 | The results are displayed in an [Adjacency Matrix](https://en.wikipedia.org/wiki/Adjacency_matrix). 18 | 19 | ![Annotated example matrix](/images/annotated-example-matrix.png) 20 | 21 | * The **rows** represent the module's public API. 22 | These would be its exported functions. 23 | * The **columns** represent the module's internal implementation 24 | These would be: 25 | * 🔐 Non-exported (private) functions 26 | * 📦 Imported members (dependencies) 27 | 28 | Notice that in order to provide an accurate representation of the structure of real-world complex code, Source Viz follows functions as transitive links. For example, even though `depA` is not used directly in the source of `methodA`, it is marked as used by it because `methodA` uses `methodC` and `methodC` does use `depA`. 29 | You can turn off this default behavior by unchecking "Private Method" as "Transitive links" in the controls. 30 | 31 | ![Private Methods as Transitive links](/images/controls-transitive-links.png) 32 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/view-model/sort.ts: -------------------------------------------------------------------------------- 1 | import { jLouvain } from 'louvain'; 2 | import * as shuffle from 'lodash.shuffle'; 3 | import { SortType } from './view-config'; 4 | import { MatrixModel } from './matrix-model'; 5 | import { getSortByEmptyRowsLastFunc } from './getSortByEmptyRowsLastFunc'; 6 | 7 | const sortFuncs = { 8 | [SortType.alphabetical]: sortByAlphabetical, 9 | [SortType.random]: sortByRandom, 10 | [SortType.louvain]: sortByLouvain, 11 | }; 12 | 13 | export function sort(model: MatrixModel, sortType: SortType) { 14 | return sortFuncs[sortType](model); 15 | } 16 | 17 | function sortByAlphabetical(model: MatrixModel) { 18 | model.rows.sort((a, b) => a.localeCompare(b)); 19 | model.columns.sort((a, b) => a.label.localeCompare(b.label)); 20 | } 21 | 22 | function sortByRandom(model: MatrixModel) { 23 | model.rows = shuffle(model.rows); 24 | model.columns = shuffle(model.columns); 25 | } 26 | 27 | function sortByLouvain(model: MatrixModel) { 28 | if (model.links.length === 0) { 29 | return; 30 | } 31 | const nodes = model.rows.concat(model.columns.map((x) => x.label)); 32 | const edges = model.links.map((x) => ({ 33 | source: x.row, 34 | target: x.column.label, 35 | value: 1, 36 | })); 37 | const community = jLouvain().nodes(nodes).edges(edges); 38 | const sorted = community(); 39 | 40 | const sortyByEmptyRowsLastFunc = getSortByEmptyRowsLastFunc( 41 | model.rows, 42 | model.links, 43 | ); 44 | 45 | model.rows.sort((a, b) => { 46 | const sortByEmptyRowsLastResult = sortyByEmptyRowsLastFunc(a, b); 47 | if (sortByEmptyRowsLastResult != 0) { 48 | return sortByEmptyRowsLastResult; 49 | } 50 | return sorted[a] - sorted[b]; 51 | }); 52 | model.columns.sort((a, b) => sorted[a.label] - sorted[b.label]); 53 | } 54 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { SourcePanel } from './SourcePanel'; 3 | import { AnalysisMode, SourceMatrix } from 'source-viz-core'; 4 | import { examples } from '../examples'; 5 | import { Description } from './Description'; 6 | import { Title } from './Title'; 7 | import { Mode } from './Mode'; 8 | 9 | interface AppState { 10 | mode: AnalysisMode; 11 | initialCode: string; 12 | draftCode: string; 13 | code: string; 14 | } 15 | 16 | export class App extends React.Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | const defaultMode = AnalysisMode.module; 21 | this.state = { 22 | mode: defaultMode, 23 | initialCode: examples[defaultMode], 24 | draftCode: examples[defaultMode], 25 | code: '', 26 | }; 27 | } 28 | 29 | onModeChange = (value) => { 30 | this.setState({ 31 | mode: value, 32 | initialCode: examples[value], 33 | }); 34 | }; 35 | 36 | onCodeChange = (newCode) => { 37 | this.setState({ draftCode: newCode }); 38 | }; 39 | 40 | onAnalyze = () => { 41 | this.setState({ 42 | code: this.state.draftCode, 43 | }); 44 | }; 45 | 46 | render() { 47 | return ( 48 |
49 |
50 | 51 | <Description /> 52 | <Mode value={this.state.mode} onChange={this.onModeChange} /> 53 | <SourcePanel 54 | code={this.state.draftCode} 55 | initialCode={this.state.initialCode} 56 | onCodeChange={this.onCodeChange} 57 | onAnalyze={this.onAnalyze} 58 | /> 59 | </div> 60 | <SourceMatrix source={this.state.code} mode={this.state.mode} /> 61 | </div> 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title>Source Viz 6 | 7 | 8 | 37 | 38 | 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source-viz-vscode", 3 | "private": true, 4 | "displayName": "Source Viz", 5 | "description": "A tool for visualizing the relationship between a module's public API and its implementation details", 6 | "version": "0.2.8", 7 | "publisher": "cowchimp", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/cowchimp/source-viz.git" 11 | }, 12 | "engines": { 13 | "vscode": "^1.49.0" 14 | }, 15 | "icon": "icon.png", 16 | "galleryBanner": { 17 | "color": "#708", 18 | "theme": "dark" 19 | }, 20 | "categories": [ 21 | "Visualization" 22 | ], 23 | "activationEvents": [ 24 | "onCommand:source-viz-vscode.openSourceViz" 25 | ], 26 | "main": "./out/extension.js", 27 | "contributes": { 28 | "commands": [ 29 | { 30 | "command": "source-viz-vscode.openSourceViz", 31 | "title": "Open Source Viz" 32 | } 33 | ] 34 | }, 35 | "scripts": { 36 | "vscode:prepublish": "yarn run build", 37 | "build": "yarn run extension:build && yarn run public:build", 38 | "extension:build": "tsc -p ./", 39 | "extension:watch": "tsc -watch -p ./", 40 | "public:start": "cd public && webpack -w --mode development --output ./dist/bundle.js --devtool inline-source-map", 41 | "public:build": "cd public && webpack --mode production --output ./dist/bundle.js", 42 | "deploy": "vsce publish --yarn" 43 | }, 44 | "dependencies": { 45 | "react": "^16.3.2", 46 | "react-dom": "^16.3.2", 47 | "source-viz-core": "^0.2.8" 48 | }, 49 | "devDependencies": { 50 | "@types/glob": "^7.1.3", 51 | "@types/node": "^14.0.27", 52 | "@types/react": "^16.3.12", 53 | "@types/react-dom": "^16.0.5", 54 | "@types/vscode": "^1.49.0", 55 | "clean-webpack-plugin": "^3.0.0", 56 | "glob": "^7.1.6", 57 | "ts-loader": "^8.0.3", 58 | "typescript": "^3.9.7", 59 | "vsce": "^1.81.1", 60 | "vscode-test": "^1.4.0", 61 | "webpack": "^4.44.2", 62 | "webpack-cli": "^3.3.12" 63 | }, 64 | "license": "MIT" 65 | } 66 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/SourceMatrix.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AnalysisMode } from '../core/view-model/view-config'; 3 | import { analyzeClassSource } from '../core/parsers/typescript/class-based/analyzeClassSource'; 4 | import { analyzeModuleSource } from '../core/parsers/typescript/module-based/analyzeModuleSource'; 5 | import { ConfigurableMatrix } from './ConfigurableMatrix'; 6 | import { ErrorBoundary } from './ErrorBoundary'; 7 | 8 | interface SourceMatrixProps { 9 | mode: AnalysisMode; 10 | source: string; 11 | } 12 | 13 | export function SourceMatrix(props: SourceMatrixProps) { 14 | if (!props.source) { 15 | return null; 16 | } 17 | 18 | try { 19 | let members; 20 | switch (props.mode) { 21 | case 'class': 22 | members = analyzeClassSource(props.source); 23 | break; 24 | case 'module': 25 | members = analyzeModuleSource(props.source); 26 | break; 27 | default: 28 | members = []; 29 | } 30 | 31 | if (!members.length) { 32 | return ( 33 | 34 | Couldn't parse code. If this is a file Source Viz should support 35 | please open an issue at{' '} 36 | 37 | https://github.com/cowchimp/source-viz 38 | 39 | . 40 | 41 | ); 42 | } 43 | 44 | return ( 45 | }> 46 | 47 | 48 | ); 49 | } catch (e) { 50 | return ; 51 | } 52 | } 53 | 54 | function InternalError({ e }) { 55 | return ( 56 | 57 | Error message: {e.message} 58 |
59 | Please open an issue at{' '} 60 | 61 | https://github.com/cowchimp/source-viz 62 | 63 | . 64 |
65 | ); 66 | } 67 | 68 | function Error({ children }) { 69 | return
⚠️ {children}
; 70 | } 71 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/class-based/getClassMembers.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { MemberType } from '../../../types'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | import { TypescriptMember } from '../TypescriptMember'; 5 | 6 | export function getClassMembers( 7 | myClass: ts.ClassDeclaration, 8 | ast: TypescriptAst, 9 | ): TypescriptMember[] { 10 | const dependencies = getDependencies(myClass); 11 | const properMethods = myClass.members.filter( 12 | ts.isMethodDeclaration, 13 | ); 14 | const functionProperties = myClass.members 15 | .filter(ts.isPropertyDeclaration) 16 | .filter((x) => x.initializer && ts.isArrowFunction(x.initializer)); 17 | const methods = (properMethods as ts.NamedDeclaration[]).concat( 18 | functionProperties, 19 | ); 20 | const privateMethods = filterByScope(methods, ts.ModifierFlags.Private); 21 | const publicMethods = methods.filter((x) => !privateMethods.includes(x)); 22 | 23 | return [ 24 | ...dependencies.map((x) => new TypescriptMember(x, MemberType.dependency)), 25 | ...privateMethods.map( 26 | (x) => new TypescriptMember(x, MemberType.privateMethod), 27 | ), 28 | ...publicMethods.map( 29 | (x) => new TypescriptMember(x, MemberType.publicMethod), 30 | ), 31 | ]; 32 | } 33 | 34 | function getDependencies(myClass: ts.ClassDeclaration) { 35 | const constructor = myClass.members.find( 36 | ts.isConstructorDeclaration, 37 | ); 38 | if (!constructor) { 39 | return []; 40 | } 41 | const constructorParameters = constructor.parameters; 42 | const privateConstructorParameters = filterByScope( 43 | constructorParameters, 44 | ts.ModifierFlags.Private, 45 | ); 46 | return privateConstructorParameters; 47 | } 48 | 49 | function filterByScope( 50 | nodes: ReadonlyArray, 51 | scope: ts.ModifierFlags, 52 | ): T[] { 53 | return nodes.filter((x) => { 54 | const flags = ts.getCombinedModifierFlags(x); 55 | return flags & scope; 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/analyzeModuleSource.spec.ts: -------------------------------------------------------------------------------- 1 | import { analyzeModuleSource } from './analyzeModuleSource'; 2 | import { MemberType } from '../../../types'; 3 | 4 | const code = `import depA from './depA'; 5 | import depB from './depB'; 6 | 7 | const depC; 8 | 9 | export function methodA() { 10 | return methodC(); 11 | } 12 | 13 | export function methodB() { 14 | console.log(methodA()); 15 | if (true) { 16 | depB.init(); 17 | } 18 | } 19 | 20 | function methodC() { 21 | return depA.init(); 22 | } 23 | 24 | export function methodD() { 25 | depB.init(); 26 | } 27 | 28 | function methodE() { 29 | return methodC(); 30 | } 31 | 32 | export function methodF() { 33 | return methodE(); 34 | }`; 35 | 36 | describe('analyzeModuleSource', function () { 37 | it('analyzes members correctly', function () { 38 | const result = analyzeModuleSource(code); 39 | 40 | expect(result).toEqual( 41 | expect.arrayContaining([ 42 | { 43 | label: 'depA', 44 | type: MemberType.dependency, 45 | referencingMethods: ['methodC'], 46 | }, 47 | { 48 | label: 'depB', 49 | type: MemberType.dependency, 50 | referencingMethods: ['methodB', 'methodD'], 51 | }, 52 | { 53 | label: 'methodA', 54 | type: MemberType.publicMethod, 55 | referencingMethods: ['methodB'], 56 | }, 57 | { 58 | label: 'methodB', 59 | type: MemberType.publicMethod, 60 | referencingMethods: [], 61 | }, 62 | { 63 | label: 'methodD', 64 | type: MemberType.publicMethod, 65 | referencingMethods: [], 66 | }, 67 | { 68 | label: 'methodF', 69 | type: MemberType.publicMethod, 70 | referencingMethods: [], 71 | }, 72 | { 73 | label: 'methodC', 74 | type: MemberType.privateMethod, 75 | referencingMethods: ['methodA', 'methodE'], 76 | }, 77 | { 78 | label: 'methodE', 79 | type: MemberType.privateMethod, 80 | referencingMethods: ['methodF'], 81 | }, 82 | ]), 83 | ); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/Mode.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AnalysisMode } from 'source-viz-core'; 3 | 4 | interface ModeProps { 5 | value: AnalysisMode; 6 | onChange: (string) => void; 7 | } 8 | 9 | interface ModeState { 10 | description: string; 11 | } 12 | 13 | const texts = { 14 | [AnalysisMode.class]: { 15 | title: 'Class-based', 16 | description: 17 | 'For ES6 Classes. Treats constructor parameters as dependencies (Angular 1.x style).', 18 | }, 19 | [AnalysisMode.module]: { 20 | title: 'Module-based', 21 | description: 22 | 'For ES6 Modules. Treats imported modules as dependencies.
Top-level functions are methods. Exported functions are public methods.', 23 | }, 24 | }; 25 | 26 | export class Mode extends React.Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { description: texts[props.value].description }; 30 | } 31 | 32 | onChange = (value: string) => { 33 | this.props.onChange(value); 34 | this.setState({ description: texts[value].description }); 35 | }; 36 | 37 | render() { 38 | return ( 39 |
40 |
41 | Mode: 42 | 49 |
50 |
51 |
52 |
56 |
57 |
58 | ); 59 | } 60 | } 61 | 62 | function ModeOption({ mode }) { 63 | return ; 64 | } 65 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/getTopLevelFunctionVariables.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import * as difference from 'lodash.difference'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | import { MemberType } from '../../../types'; 5 | import { TypescriptMember } from '../TypescriptMember'; 6 | 7 | export function getTopLevelFunctionVariables( 8 | ast: TypescriptAst, 9 | ): TypescriptMember[] { 10 | const topLevelStatements = ast.nodes.find(ts.isSourceFile) 11 | .statements; 12 | 13 | const publicFunctionVariables = getFunctionVariables( 14 | topLevelStatements, 15 | isPublic, 16 | ); 17 | const functionVariables = getFunctionVariables(topLevelStatements); 18 | const privateFunctionVariables = difference( 19 | functionVariables, 20 | publicFunctionVariables, 21 | ); 22 | 23 | const defaultArrowFunction = getDefaultArrowFunction(topLevelStatements); 24 | if (defaultArrowFunction) { 25 | publicFunctionVariables.push(defaultArrowFunction); 26 | } 27 | 28 | return [ 29 | ...publicFunctionVariables.map( 30 | (x) => new TypescriptMember(x, MemberType.publicMethod), 31 | ), 32 | ...privateFunctionVariables.map( 33 | (x) => new TypescriptMember(x, MemberType.privateMethod), 34 | ), 35 | ]; 36 | } 37 | 38 | function getFunctionVariables( 39 | nodes: ReadonlyArray, 40 | filterFunc: (variableStatement: ts.VariableStatement) => boolean = () => true, 41 | ): ts.NamedDeclaration[] { 42 | return nodes 43 | .filter(ts.isVariableStatement) 44 | .filter(filterFunc) 45 | .filter((x) => x.declarationList.declarations.length == 1) 46 | .map((x) => x.declarationList.declarations[0]) 47 | .filter((x) => { 48 | return ( 49 | x.initializer && 50 | (ts.isArrowFunction(x.initializer) || 51 | ts.isFunctionExpression(x.initializer)) 52 | ); 53 | }); 54 | } 55 | 56 | function isPublic(x: ts.Node): boolean { 57 | return ( 58 | x.modifiers && 59 | x.modifiers.some((m) => m.kind == ts.SyntaxKind.ExportKeyword) 60 | ); 61 | } 62 | 63 | function getDefaultArrowFunction( 64 | topLevelStatements: ReadonlyArray, 65 | ): ts.ExportAssignment { 66 | return topLevelStatements.find(ts.isExportAssignment); 67 | } 68 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/controls/ColumnFilters.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Checkbox } from './Checkbox'; 3 | import { getTypeEmoji } from '../../getTypeEmoji'; 4 | import { MemberInfo, MemberType } from '../../core/types'; 5 | 6 | interface ColumnFilterProps { 7 | onChange: (x: Set) => void; 8 | active: Set; 9 | members: MemberInfo[]; 10 | } 11 | 12 | export function ColumnFilters(props: ColumnFilterProps) { 13 | const counts = { 14 | [MemberType.dependency]: props.members.filter( 15 | (x) => x.type == MemberType.dependency, 16 | ).length, 17 | [MemberType.privateMethod]: props.members.filter( 18 | (x) => x.type == MemberType.privateMethod, 19 | ).length, 20 | [MemberType.publicMethod]: props.members.filter( 21 | (x) => x.type == MemberType.publicMethod, 22 | ).length, 23 | }; 24 | 25 | return ( 26 |
27 | Columns: 28 |
29 | 37 |
38 |
39 | 47 |
48 |
49 | 57 |
58 |
59 | ); 60 | 61 | function onChange(value: MemberType, checked: boolean): void { 62 | if (props.active.size == 1 && !checked) { 63 | return; 64 | } 65 | 66 | const newActiveFilters = new Set(props.active); 67 | newActiveFilters[checked ? 'add' : 'delete'](value); 68 | props.onChange(newActiveFilters); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/source-viz-web/src/components/VsCodeLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export function VsCodeLogo() { 4 | return ( 5 | 10 | 16 | 17 | 21 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | 48 | 49 | 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/groupPublicMethodsToReferencedMembers.ts: -------------------------------------------------------------------------------- 1 | import { MemberInfo, MemberType } from './types'; 2 | import { getDescendants } from './utils/getDescendants'; 3 | import { getUniques } from './utils/getUniques'; 4 | import { MapLike } from 'typescript/lib/typescript'; 5 | 6 | export function groupPublicMethodsToReferencedMembers( 7 | members: MemberInfo[], 8 | transitiveLinkFilters: Set, 9 | ): Map { 10 | const referencingMethodsToMembers = getReferencingMethodsToMembers(members); 11 | 12 | return members 13 | .filter((x) => x.type == MemberType.publicMethod) 14 | .reduce((acc, publicMethod) => { 15 | const linkedMembers = getMembersTransitively( 16 | publicMethod, 17 | transitiveLinkFilters, 18 | (m) => referencingMethodsToMembers.get(m) || [], 19 | ); 20 | acc.set(publicMethod, linkedMembers); 21 | return acc; 22 | }, new Map()); 23 | } 24 | 25 | function getMembersTransitively( 26 | root: MemberInfo, 27 | transitiveLinkFilters: Set, 28 | getReferencedMembers: (m: MemberInfo) => MemberInfo[], 29 | ): MemberInfo[] { 30 | const referencedInRoot = getReferencedMembers(root); 31 | 32 | const descendants = getDescendants(root, (m) => { 33 | return getReferencedMembers(m).filter((x) => 34 | transitiveLinkFilters.has(x.type), 35 | ); 36 | }); 37 | const referencedInDescendants = descendants.reduce( 38 | (acc, x) => acc.concat(getReferencedMembers(x)), 39 | [], 40 | ); 41 | 42 | return getUniques(referencedInRoot, referencedInDescendants); 43 | } 44 | 45 | function getReferencingMethodsToMembers( 46 | members: MemberInfo[], 47 | ): Map { 48 | const ret = new Map(); 49 | const labelsToMembers = getLabelsToMembers(members); 50 | 51 | members.forEach((member) => { 52 | member.referencingMethods.forEach((referencingMethodLabel) => { 53 | const referencingMethod = labelsToMembers[referencingMethodLabel]; 54 | 55 | if (!ret.has(referencingMethod)) { 56 | ret.set(referencingMethod, []); 57 | } 58 | ret.get(referencingMethod).push(member); 59 | }); 60 | }); 61 | return ret; 62 | } 63 | 64 | function getLabelsToMembers(members: MemberInfo[]): MapLike { 65 | return members.reduce((acc, m) => { 66 | acc[m.label] = m; 67 | return acc; 68 | }, {}); 69 | } 70 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/getTopLevelFunctions.spec.ts: -------------------------------------------------------------------------------- 1 | import { getTopLevelFunctions } from './getTopLevelFunctions'; 2 | import { MemberType } from '../../../types'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | 5 | describe('getTopLevelFunctions', function () { 6 | it('returns exported functions declarations as public methods', function () { 7 | const code = `export function foo() {}`; 8 | const ast = new TypescriptAst(code); 9 | const result = getTopLevelFunctions(ast).map((x) => x.getMemberInfo(ast)); 10 | expect(result).toEqual([ 11 | expect.objectContaining({ 12 | label: 'foo', 13 | type: MemberType.publicMethod, 14 | }), 15 | ]); 16 | }); 17 | 18 | it('returns non-exported functions declarations as private methods', function () { 19 | const code = `function foo() {}`; 20 | const ast = new TypescriptAst(code); 21 | const result = getTopLevelFunctions(ast).map((x) => x.getMemberInfo(ast)); 22 | expect(result).toEqual([ 23 | expect.objectContaining({ 24 | label: 'foo', 25 | type: MemberType.privateMethod, 26 | }), 27 | ]); 28 | }); 29 | 30 | it('does not return non-top-level functions declarations as methods', function () { 31 | const code = `function foo() { function bar() { } }`; 32 | const ast = new TypescriptAst(code); 33 | const result = getTopLevelFunctions(ast).map((x) => x.getMemberInfo(ast)); 34 | expect(result).not.toContainEqual( 35 | expect.objectContaining({ label: 'bar' }), 36 | ); 37 | }); 38 | 39 | it('returns exported default named functions as public methods', function () { 40 | const code = `export default function foo() {}`; 41 | const ast = new TypescriptAst(code); 42 | const result = getTopLevelFunctions(ast).map((x) => x.getMemberInfo(ast)); 43 | expect(result).toEqual([ 44 | expect.objectContaining({ 45 | label: 'foo', 46 | type: MemberType.publicMethod, 47 | }), 48 | ]); 49 | }); 50 | 51 | it('returns exported default anonymous functions as public methods', function () { 52 | const code = `export default function() {}`; 53 | const ast = new TypescriptAst(code); 54 | const result = getTopLevelFunctions(ast).map((x) => x.getMemberInfo(ast)); 55 | expect(result).toEqual([ 56 | expect.objectContaining({ 57 | label: 'default', 58 | type: MemberType.publicMethod, 59 | }), 60 | ]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/Grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { HoverHighlightGroup } from './HoverHighlightGroup'; 3 | import { Row } from './Row'; 4 | import { Column } from './Column'; 5 | import { Cell } from './Cell'; 6 | import { HoverTargetGroup } from './HoverTargetGroup'; 7 | import { MatrixModel } from '../../core/view-model/matrix-model'; 8 | 9 | interface GridProps extends MatrixModel { 10 | cellWidth: number; 11 | cellHeight: number; 12 | } 13 | 14 | export class Grid extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.state = {}; 19 | } 20 | 21 | onCellMouseEnter = (x, y) => { 22 | this.setState({ highlightX: x, highlightY: y }); 23 | }; 24 | 25 | onMatrixMouseLeave = () => 26 | this.setState({ 27 | highlightX: null, 28 | highlightY: null, 29 | }); 30 | 31 | render() { 32 | const { cellWidth, cellHeight, rows, columns, links } = this.props; 33 | const width = columns.length * cellWidth; 34 | const height = rows.length * cellHeight; 35 | 36 | const xScale = (label) => 37 | columns.findIndex((x) => x.label == label) * cellWidth; 38 | const yScale = (label) => rows.findIndex((x) => x == label) * cellHeight; 39 | 40 | return ( 41 | 42 | 43 | 51 | {rows.map((x) => ( 52 | 59 | ))} 60 | {columns.map((x) => ( 61 | 69 | ))} 70 | {links.map(({ row, column }) => ( 71 | 78 | ))} 79 | 86 | 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/class-based/analyzeClassSource.spec.ts: -------------------------------------------------------------------------------- 1 | import { analyzeClassSource } from './analyzeClassSource'; 2 | import { MemberType } from '../../../types'; 3 | 4 | const code = `class Foo { 5 | private depC; 6 | 7 | constructor( 8 | private depA, 9 | private depB 10 | ) { 11 | 12 | } 13 | 14 | methodA() { 15 | return this.methodC(); 16 | } 17 | 18 | public methodB() { 19 | console.log(this.methodA()) 20 | const that = this; 21 | if (true) { 22 | that.depB.init(); 23 | } 24 | } 25 | 26 | private methodC() { 27 | return this.depA.init(); 28 | } 29 | 30 | public methodD() { 31 | this.depB.init(); 32 | } 33 | 34 | private methodE() { 35 | return this.methodC(); 36 | } 37 | 38 | public methodF() { 39 | return this.methodE(); 40 | } 41 | } 42 | 43 | class Bar { 44 | constructor( 45 | private foo: Foo 46 | ) {} 47 | 48 | methodA() { 49 | console.log(this.foo.methodA()); 50 | } 51 | }`; 52 | 53 | describe('analyzeClassSource', function () { 54 | it('analyzes members correctly', function () { 55 | const result = analyzeClassSource(code); 56 | 57 | expect(result).toEqual( 58 | expect.arrayContaining([ 59 | { 60 | label: 'depA', 61 | type: MemberType.dependency, 62 | referencingMethods: ['methodC'], 63 | }, 64 | { 65 | label: 'depB', 66 | type: MemberType.dependency, 67 | referencingMethods: ['methodB', 'methodD'], 68 | }, 69 | { 70 | label: 'methodA', 71 | type: MemberType.publicMethod, 72 | referencingMethods: ['methodB'], 73 | }, 74 | { 75 | label: 'methodB', 76 | type: MemberType.publicMethod, 77 | referencingMethods: [], 78 | }, 79 | { 80 | label: 'methodD', 81 | type: MemberType.publicMethod, 82 | referencingMethods: [], 83 | }, 84 | { 85 | label: 'methodF', 86 | type: MemberType.publicMethod, 87 | referencingMethods: [], 88 | }, 89 | { 90 | label: 'methodC', 91 | type: MemberType.privateMethod, 92 | referencingMethods: ['methodA', 'methodE'], 93 | }, 94 | { 95 | label: 'methodE', 96 | type: MemberType.privateMethod, 97 | referencingMethods: ['methodF'], 98 | }, 99 | ]), 100 | ); 101 | }); 102 | 103 | it('returns an empty array when passed source with node class', function () { 104 | const result = analyzeClassSource(`console.log('Hello world`); 105 | 106 | expect(result).toEqual([]); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/components/matrix/Matrix.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Grid } from './Grid'; 3 | import { MatrixModel } from '../../core/view-model/matrix-model'; 4 | import { MeasureText } from './MeasureText'; 5 | import { adopt } from 'react-adopt'; 6 | import { AutoSizer } from 'react-virtualized'; 7 | import { columnGridMargin, rowGridMargin } from './constants'; 8 | import { ZoomWrapper } from './ZoomWrapper'; 9 | 10 | interface MatrixProps extends MatrixModel { 11 | cellWidth: number; 12 | cellHeight: number; 13 | } 14 | 15 | const offsets = ({ rows, columns, render }) => { 16 | const longestRow = getLongestString(rows); 17 | const longestColumn = getLongestString(columns.map((x) => x.label)); 18 | return ( 19 | 27 | {render} 28 | 29 | ); 30 | }; 31 | 32 | const viewport = ({ render }) => ( 33 |
34 | 35 | {({ width }) => { 36 | if (width === 0) { 37 | return null; 38 | } 39 | return render({ 40 | width, 41 | }); 42 | }} 43 | 44 |
45 | ); 46 | 47 | const Composed = adopt({ 48 | offsets, 49 | viewport, 50 | }); 51 | 52 | export function Matrix(props: MatrixProps) { 53 | return ( 54 | 55 | {({ offsets: { offsetLeft, offsetTop }, viewport: { width } }) => { 56 | const { rows, columns, links, cellWidth, cellHeight } = props; 57 | 58 | offsetLeft += rowGridMargin; 59 | offsetTop += columnGridMargin; 60 | const canvasOuterWidth = columns.length * cellWidth + offsetLeft; 61 | const canvasOuterHeight = rows.length * cellHeight + offsetTop; 62 | const ratio = canvasOuterWidth / canvasOuterHeight; 63 | 64 | const svg = ( 65 | 66 | 67 | 74 | 75 | 76 | ); 77 | 78 | if (canvasOuterWidth < width) { 79 | return svg; 80 | } 81 | 82 | return ( 83 | 84 | {svg} 85 | 86 | ); 87 | }} 88 | 89 | ); 90 | } 91 | 92 | function getLongestString(labels: string[]): string { 93 | return labels.reduce( 94 | (max, label) => (label.length > max.length ? label : max), 95 | '', 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/TypescriptMember.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { TypescriptAst } from './TypescriptAst'; 3 | import { TypescriptMember } from './TypescriptMember'; 4 | import { MemberType } from '../../types'; 5 | 6 | const code = `class Foo { 7 | constructor( 8 | private ghostBusters, 9 | ) { } 10 | 11 | public callGhostBustersIfNeeded() { 12 | if(isTheresSomethingStrangeInYourNeighborhood()) { 13 | this.ghostBusters.call(); 14 | } 15 | 16 | if(isTheresSomethingWeirdAndItDontLookGood()) { 17 | this.ghostBusters.call(); 18 | } 19 | } 20 | 21 | public handleGhostsIfNeeded() { 22 | this.callGhostBustersIfNeeded(); 23 | } 24 | }`; 25 | 26 | describe('TypescriptMember', function () { 27 | describe('getMemberInfo', function () { 28 | describe('referencingMethods', function () { 29 | it('dedups multiple usages in same method', function () { 30 | const ast = new TypescriptAst(code); 31 | const myClass = find(ast.nodes, ts.isClassDeclaration, 'Foo'); 32 | const parameter = myClass.members.find( 33 | ts.isConstructorDeclaration, 34 | ).parameters[0]; 35 | const member = new TypescriptMember(parameter, MemberType.dependency); 36 | const publicMethod = find( 37 | myClass.members, 38 | ts.isMethodDeclaration, 39 | 'callGhostBustersIfNeeded', 40 | ); 41 | const members = [ 42 | new TypescriptMember(publicMethod, MemberType.publicMethod), 43 | ]; 44 | const memberInfo = member.getMemberInfo(ast, members); 45 | const result = memberInfo.referencingMethods; 46 | expect(result).toEqual(['callGhostBustersIfNeeded']); 47 | }); 48 | 49 | it("does not return the method when passed a method's root node", function () { 50 | const ast = new TypescriptAst(code); 51 | const myClass = find(ast.nodes, ts.isClassDeclaration, 'Foo'); 52 | const method1 = find( 53 | myClass.members, 54 | ts.isMethodDeclaration, 55 | 'callGhostBustersIfNeeded', 56 | ); 57 | const member1 = new TypescriptMember(method1, MemberType.publicMethod); 58 | const method2 = find( 59 | myClass.members, 60 | ts.isMethodDeclaration, 61 | 'handleGhostsIfNeeded', 62 | ); 63 | const member2 = new TypescriptMember(method2, MemberType.publicMethod); 64 | const members = [member1, member2]; 65 | const memberInfo = member1.getMemberInfo(ast, members); 66 | const result = memberInfo.referencingMethods; 67 | expect(result).toEqual(['handleGhostsIfNeeded']); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | function find( 74 | list: ReadonlyArray, 75 | typeGuard: (n: ts.Node) => n is T, 76 | name: string, 77 | ): T { 78 | return list 79 | .filter(typeGuard) 80 | .find((x) => x.name && ts.isIdentifier(x.name) && x.name.text == name); 81 | } 82 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/groupPublicMethodsToReferencedMembers.spec.ts: -------------------------------------------------------------------------------- 1 | import { groupPublicMethodsToReferencedMembers } from './groupPublicMethodsToReferencedMembers'; 2 | import { MemberInfo, MemberType } from './types'; 3 | 4 | const somePublicMethod = { 5 | label: 'somePublicMethod', 6 | type: MemberType.publicMethod, 7 | referencingMethods: [], 8 | }; 9 | const someOtherPublicMethod = { 10 | label: 'someOtherPublicMethod', 11 | type: MemberType.publicMethod, 12 | referencingMethods: ['somePublicMethod'], 13 | }; 14 | const somePrivateMethod = { 15 | label: 'somePrivateMethod', 16 | type: MemberType.privateMethod, 17 | referencingMethods: ['somePublicMethod'], 18 | }; 19 | const someDependency = { 20 | label: 'someDependency', 21 | type: MemberType.dependency, 22 | referencingMethods: ['somePublicMethod'], 23 | }; 24 | const someOtherPrivateMethod = { 25 | label: 'someOtherPrivateMethod', 26 | type: MemberType.privateMethod, 27 | referencingMethods: ['somePrivateMethod'], 28 | }; 29 | const someOtherDependency = { 30 | label: 'someOtherDependency', 31 | type: MemberType.dependency, 32 | referencingMethods: ['someOtherPrivateMethod'], 33 | }; 34 | const someThirdPrivateMethod = { 35 | label: 'someThirdPrivateMethod', 36 | type: MemberType.privateMethod, 37 | referencingMethods: ['someOtherPrivateMethod'], 38 | }; 39 | 40 | const set: MemberInfo[] = [ 41 | somePublicMethod, 42 | someOtherPublicMethod, 43 | somePrivateMethod, 44 | someDependency, 45 | someOtherPrivateMethod, 46 | someOtherDependency, 47 | someThirdPrivateMethod, 48 | ]; 49 | 50 | describe('groupPublicMethodsToReferencedMembers', function () { 51 | describe('no transitive links are passed', function () { 52 | it('returns only directly referenced members', function () { 53 | const result = groupPublicMethodsToReferencedMembers( 54 | set, 55 | new Set(), 56 | ); 57 | 58 | expect(result).toEqual( 59 | new Map([ 60 | [ 61 | somePublicMethod, 62 | [someOtherPublicMethod, somePrivateMethod, someDependency], 63 | ], 64 | [someOtherPublicMethod, []], 65 | ]), 66 | ); 67 | }); 68 | }); 69 | 70 | describe('private methods are used as transitive links', function () { 71 | it('returns directly referenced members as well as those referenced in private methods used', function () { 72 | const result = groupPublicMethodsToReferencedMembers( 73 | set, 74 | new Set([MemberType.privateMethod]), 75 | ); 76 | 77 | expect(result).toEqual( 78 | new Map([ 79 | [ 80 | somePublicMethod, 81 | [ 82 | someOtherPublicMethod, 83 | somePrivateMethod, 84 | someDependency, 85 | someOtherPrivateMethod, 86 | someOtherDependency, 87 | someThirdPrivateMethod, 88 | ], 89 | ], 90 | [someOtherPublicMethod, []], 91 | ]), 92 | ); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/source-viz-vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import type { ExtensionContext, TextEditor, Uri, WebviewPanel } from 'vscode'; 4 | 5 | let panel: WebviewPanel | undefined; 6 | 7 | export function activate(context: vscode.ExtensionContext) { 8 | context.subscriptions.push( 9 | vscode.commands.registerTextEditorCommand( 10 | 'source-viz-vscode.openSourceViz', 11 | (textEditor) => { 12 | if (panel) { 13 | panel.reveal(vscode.ViewColumn.Beside); 14 | return; 15 | } 16 | 17 | panel = vscode.window.createWebviewPanel( 18 | 'sourceVizMain', 19 | 'Source Viz', 20 | vscode.ViewColumn.Beside, 21 | { 22 | enableScripts: true, 23 | retainContextWhenHidden: true, 24 | }, 25 | ); 26 | 27 | const bundleUri = getBundleUri(context, panel); 28 | 29 | panel.webview.html = getWebviewContent(textEditor, bundleUri); 30 | 31 | const changeTextEditorSubscription = vscode.window.onDidChangeActiveTextEditor( 32 | (editor) => { 33 | if (!editor || !panel) { 34 | return; 35 | } 36 | panel.webview.html = getWebviewContent(editor, bundleUri); 37 | }, 38 | ); 39 | 40 | panel.onDidDispose( 41 | () => changeTextEditorSubscription.dispose(), 42 | null, 43 | context.subscriptions, 44 | ); 45 | }, 46 | ), 47 | ); 48 | } 49 | 50 | function getBundleUri(context: ExtensionContext, panel: WebviewPanel) { 51 | const fullPath = path.join(context.extensionPath, 'public/dist/bundle.js'); 52 | const uri = vscode.Uri.file(fullPath); 53 | const bundleUri = panel.webview.asWebviewUri(uri); 54 | return bundleUri; 55 | } 56 | 57 | function getWebviewContent(textEditor: TextEditor, bundleUri: Uri) { 58 | const code = textEditor.document.getText(); 59 | return ` 60 | 61 | 62 | 63 | 67 | 79 | 80 | 81 |
82 | 85 | 86 | 87 | 88 | `; 89 | } 90 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/TypescriptAst.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export class TypescriptAst { 4 | private _sourceFile: ts.SourceFile; 5 | private _nodes: ts.Node[] = []; 6 | private _nodeParents: Map = new Map(); 7 | private _nodePositions: Map = new Map(); 8 | private _languageService: ts.LanguageService; 9 | 10 | constructor(private code: string) { 11 | const fileName = 'source.ts'; 12 | this._sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ES5); 13 | this._languageService = ts.createLanguageService( 14 | new InMemoryLanguageServiceHost({ [fileName]: code }), 15 | ); 16 | this.visit(this._sourceFile, undefined, (node, parent) => { 17 | this._nodes.push(node); 18 | this._nodeParents.set(node, parent); 19 | this._nodePositions.set(node.getStart(this._sourceFile), node); 20 | }); 21 | } 22 | 23 | get nodes(): ts.Node[] { 24 | return this._nodes; 25 | } 26 | 27 | getReferences(identifier: ts.Identifier): ts.Node[] { 28 | const position = identifier.getStart(this._sourceFile); 29 | 30 | const referencedSymbols = this._languageService.findReferences( 31 | this._sourceFile.fileName, 32 | position, 33 | ); 34 | const referenceEntries = referencedSymbols.reduce( 35 | (acc, symbol) => acc.concat(symbol.references), 36 | [], 37 | ); 38 | const referencePositions = referenceEntries.map((x) => x.textSpan.start); 39 | 40 | return referencePositions.map((x) => this._nodePositions.get(x)); 41 | } 42 | 43 | getParent(node: ts.Node): ts.Node | undefined { 44 | return this._nodeParents.get(node); 45 | } 46 | 47 | getFullText(node: ts.Node): string { 48 | return node.getFullText(this._sourceFile); 49 | } 50 | 51 | getName(node: ts.NamedDeclaration): string { 52 | if (!node.name || !ts.isIdentifier(node.name)) { 53 | throw new Error("must be able to parse NamedDeclaration's name"); 54 | } 55 | return node.name.text; 56 | } 57 | 58 | private visit( 59 | node: ts.Node, 60 | parent: ts.Node | undefined, 61 | cb: (node: ts.Node, parent: ts.Node | undefined) => void, 62 | ) { 63 | cb(node, parent); 64 | ts.forEachChild(node, (n) => n && this.visit(n, node, cb)); 65 | } 66 | } 67 | 68 | class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { 69 | constructor(private files: ts.MapLike) {} 70 | 71 | getCompilationSettings(): ts.CompilerOptions { 72 | const opts = ts.getDefaultCompilerOptions(); 73 | opts.noLib = true; 74 | return opts; 75 | } 76 | 77 | getScriptFileNames(): string[] { 78 | return Object.keys(this.files); 79 | } 80 | 81 | getScriptVersion(fileName: string): string { 82 | return '0'; 83 | } 84 | 85 | getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { 86 | return ts.ScriptSnapshot.fromString(this.files[fileName]); 87 | } 88 | 89 | getCurrentDirectory() { 90 | return ''; 91 | } 92 | 93 | getDefaultLibFileName(options: ts.CompilerOptions): string { 94 | return ts.getDefaultLibFilePath(options); 95 | } 96 | 97 | getNewLine(): string { 98 | return '\n'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/class-based/getClassMembers.spec.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { getClassMembers } from './getClassMembers'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | import { MemberType } from '../../../types'; 5 | 6 | describe('getClassMembers', function () { 7 | it('returns private constructor parameters as dependencies', function () { 8 | const code = `class Foo { 9 | private propA; 10 | 11 | constructor( 12 | private propB, 13 | public propC 14 | ) {} 15 | }`; 16 | const ast = new TypescriptAst(code); 17 | const myClass = find(ast.nodes, ts.isClassDeclaration, 'Foo'); 18 | const result = getClassMembers(myClass, ast).map((x) => 19 | x.getMemberInfo(ast), 20 | ); 21 | expect(result).toEqual([ 22 | expect.objectContaining({ 23 | label: 'propB', 24 | type: MemberType.dependency, 25 | }), 26 | ]); 27 | }); 28 | 29 | it('returns private methods as such', function () { 30 | const code = `class Foo { 31 | private methodA() { } 32 | public methodB() { } 33 | methodC() { } 34 | protected methodD() { } 35 | }`; 36 | const ast = new TypescriptAst(code); 37 | const myClass = find(ast.nodes, ts.isClassDeclaration, 'Foo'); 38 | const result = getClassMembers(myClass, ast) 39 | .map((x) => x.getMemberInfo(ast)) 40 | .filter((x) => x.type == MemberType.privateMethod) 41 | .map((x) => x.label); 42 | expect(result).toEqual(['methodA']); 43 | }); 44 | 45 | it('returns any type of methods except private methods as public methods', function () { 46 | const code = `class Foo { 47 | private methodA() { } 48 | public methodB() { } 49 | methodC() { } 50 | protected methodD() { } 51 | }`; 52 | const ast = new TypescriptAst(code); 53 | const myClass = find(ast.nodes, ts.isClassDeclaration, 'Foo'); 54 | const result = getClassMembers(myClass, ast) 55 | .map((x) => x.getMemberInfo(ast)) 56 | .filter((x) => x.type == MemberType.publicMethod) 57 | .map((x) => x.label); 58 | expect(result).toEqual(['methodB', 'methodC', 'methodD']); 59 | }); 60 | 61 | it('returns class properties that are arrow functions as methods', function () { 62 | const code = `class Foo { 63 | private methodA() { } 64 | private methodB = () => { }; 65 | public methodC() { } 66 | public methodD = () { }; 67 | }`; 68 | const ast = new TypescriptAst(code); 69 | const myClass = find(ast.nodes, ts.isClassDeclaration, 'Foo'); 70 | const result = getClassMembers(myClass, ast).map((x) => 71 | x.getMemberInfo(ast), 72 | ); 73 | expect(result).toEqual([ 74 | expect.objectContaining({ 75 | label: 'methodA', 76 | type: MemberType.privateMethod, 77 | }), 78 | expect.objectContaining({ 79 | label: 'methodB', 80 | type: MemberType.privateMethod, 81 | }), 82 | expect.objectContaining({ 83 | label: 'methodC', 84 | type: MemberType.publicMethod, 85 | }), 86 | expect.objectContaining({ 87 | label: 'methodD', 88 | type: MemberType.publicMethod, 89 | }), 90 | ]); 91 | }); 92 | }); 93 | 94 | function find( 95 | list: ReadonlyArray, 96 | typeGuard: (n: ts.Node) => n is T, 97 | name: string, 98 | ): T { 99 | return list 100 | .filter(typeGuard) 101 | .find((x) => x.name && ts.isIdentifier(x.name) && x.name.text == name); 102 | } 103 | -------------------------------------------------------------------------------- /packages/source-viz-core/src/core/parsers/typescript/module-based/getTopLevelFunctionVariables.spec.ts: -------------------------------------------------------------------------------- 1 | import { getTopLevelFunctionVariables } from './getTopLevelFunctionVariables'; 2 | import { MemberType } from '../../../types'; 3 | import { TypescriptAst } from '../TypescriptAst'; 4 | 5 | describe('getTopLevelFunctionVariables', function () { 6 | it('returns exported arrow functions variables as public methods', function () { 7 | const code = `export const foo = () => {}`; 8 | const ast = new TypescriptAst(code); 9 | const result = getTopLevelFunctionVariables(ast).map((x) => 10 | x.getMemberInfo(ast), 11 | ); 12 | expect(result).toEqual([ 13 | expect.objectContaining({ 14 | label: 'foo', 15 | type: MemberType.publicMethod, 16 | }), 17 | ]); 18 | }); 19 | 20 | it('returns exported anonymous functions variables as public methods', function () { 21 | const code = `export const foo = function() {}`; 22 | const ast = new TypescriptAst(code); 23 | const result = getTopLevelFunctionVariables(ast).map((x) => 24 | x.getMemberInfo(ast), 25 | ); 26 | expect(result).toEqual([ 27 | expect.objectContaining({ 28 | label: 'foo', 29 | type: MemberType.publicMethod, 30 | }), 31 | ]); 32 | }); 33 | 34 | it('returns exported named functions variables as public methods', function () { 35 | const code = `export const foo = function bar() {}`; 36 | const ast = new TypescriptAst(code); 37 | const result = getTopLevelFunctionVariables(ast).map((x) => 38 | x.getMemberInfo(ast), 39 | ); 40 | expect(result).toEqual([ 41 | expect.objectContaining({ 42 | label: 'foo', 43 | type: MemberType.publicMethod, 44 | }), 45 | ]); 46 | }); 47 | 48 | it('returns non-exported arrow functions variables as private methods', function () { 49 | const code = `const foo = () => {}`; 50 | const ast = new TypescriptAst(code); 51 | const result = getTopLevelFunctionVariables(ast).map((x) => 52 | x.getMemberInfo(ast), 53 | ); 54 | expect(result).toEqual([ 55 | expect.objectContaining({ 56 | label: 'foo', 57 | type: MemberType.privateMethod, 58 | }), 59 | ]); 60 | }); 61 | 62 | it('returns non-exported anonymous functions variables as private methods', function () { 63 | const code = `const foo = function() {}`; 64 | const ast = new TypescriptAst(code); 65 | const result = getTopLevelFunctionVariables(ast).map((x) => 66 | x.getMemberInfo(ast), 67 | ); 68 | expect(result).toEqual([ 69 | expect.objectContaining({ 70 | label: 'foo', 71 | type: MemberType.privateMethod, 72 | }), 73 | ]); 74 | }); 75 | 76 | it('returns non-exported named functions variables as private methods', function () { 77 | const code = `const foo = function bar() {}`; 78 | const ast = new TypescriptAst(code); 79 | const result = getTopLevelFunctionVariables(ast).map((x) => 80 | x.getMemberInfo(ast), 81 | ); 82 | expect(result).toEqual([ 83 | expect.objectContaining({ 84 | label: 'foo', 85 | type: MemberType.privateMethod, 86 | }), 87 | ]); 88 | }); 89 | 90 | it('returns exported default arrow functions variables as public methods', function () { 91 | const code = `export default () => {}`; 92 | const ast = new TypescriptAst(code); 93 | const result = getTopLevelFunctionVariables(ast).map((x) => 94 | x.getMemberInfo(ast), 95 | ); 96 | expect(result).toEqual([ 97 | expect.objectContaining({ 98 | label: 'default', 99 | type: MemberType.publicMethod, 100 | }), 101 | ]); 102 | }); 103 | 104 | it('does not return non-top-level arrow function variables as methods', function () { 105 | const code = `const foo => () => { const bar = () => { } }`; 106 | const ast = new TypescriptAst(code); 107 | const result = getTopLevelFunctionVariables(ast).map((x) => 108 | x.getMemberInfo(ast), 109 | ); 110 | expect(result).not.toContainEqual( 111 | expect.objectContaining({ label: 'bar' }), 112 | ); 113 | }); 114 | 115 | it('does not return non function variables as methods', function () { 116 | const code = `const foo = 42`; 117 | const ast = new TypescriptAst(code); 118 | const result = getTopLevelFunctionVariables(ast).map((x) => 119 | x.getMemberInfo(ast), 120 | ); 121 | expect(result).not.toContainEqual( 122 | expect.objectContaining({ label: 'foo' }), 123 | ); 124 | }); 125 | }); 126 | --------------------------------------------------------------------------------