Skip to content

Merging branch #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sapling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"title": "Generate Tree"
}
],
"menus" : {
"menus": {
"commandPalette": [
{
"command": "sapling.generateTree",
Expand Down Expand Up @@ -138,4 +138,4 @@
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
}
131 changes: 65 additions & 66 deletions sapling/src/SaplingParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Tree } from './types/Tree';
import { ImportObj } from './types/ImportObj';
import { File } from '@babel/types';


export class SaplingParser {
entryFile: string;
tree: Tree | undefined;
Expand All @@ -17,7 +16,7 @@ export class SaplingParser {
if (process.platform === 'linux' && this.entryFile.includes('wsl$')) {
this.entryFile = path.resolve(filePath.split(path.win32.sep).join(path.posix.sep));
this.entryFile = '/' + this.entryFile.split('/').slice(3).join('/');
// Fix for when running wsl but selecting files held on windows file system
// Fix for when running wsl but selecting files held on windows file system
} else if (process.platform === 'linux' && (/[a-zA-Z]/).test(this.entryFile[0])) {
const root = `/mnt/${this.entryFile[0].toLowerCase()}`;
this.entryFile = path.join(root, filePath.split(path.win32.sep).slice(1).join(path.posix.sep));
Expand All @@ -28,13 +27,13 @@ export class SaplingParser {
}

// Public method to generate component tree based on current entryFile
public parse() : Tree {
public parse(): Tree {
// Create root Tree node
const root = {
id: getNonce(),
name: path.basename(this.entryFile).replace(/\.(t|j)sx?$/, ''),
fileName: path.basename(this.entryFile),
filePath : this.entryFile,
filePath: this.entryFile,
importPath: '/', // this.entryFile here breaks windows file path on root e.g. C:\\ is detected as third party
expanded: false,
depth: 0,
Expand All @@ -53,66 +52,66 @@ export class SaplingParser {
return this.tree;
}

public getTree() : Tree | undefined {
public getTree(): Tree | undefined {
return this.tree;
}

// Set Sapling Parser with a specific Data Tree (from workspace state)
public setTree(tree : Tree) : void {
public setTree(tree: Tree): void {
this.entryFile = tree.filePath;
this.tree = tree;
}

// Updates the tree when a file is saved in VS Code
public updateTree(filePath : string) : Tree | undefined {
if (this.tree === undefined) {
return this.tree;
}
public updateTree(filePath: string): Tree | undefined {
if (this.tree === undefined) {
return this.tree;
}

type ChildInfo = {
depth: number,
filePath: string,
expanded: boolean
};
type ChildInfo = {
depth: number,
filePath: string,
expanded: boolean
};

let children : Array<ChildInfo> = [];
let children: Array<ChildInfo> = [];

const getChildNodes = (node: Tree) : void => {
const { depth, filePath, expanded } = node;
children.push({ depth, filePath, expanded });
};
const getChildNodes = (node: Tree): void => {
const { depth, filePath, expanded } = node;
children.push({ depth, filePath, expanded });
};

const matchExpand = (node: Tree) : void => {
for (let i = 0 ; i < children.length ; i += 1) {
const oldNode = children[i];
if (oldNode.depth === node.depth && oldNode.filePath === node.filePath && oldNode.expanded) {
node.expanded = true;
}
const matchExpand = (node: Tree): void => {
for (let i = 0; i < children.length; i += 1) {
const oldNode = children[i];
if (oldNode.depth === node.depth && oldNode.filePath === node.filePath && oldNode.expanded) {
node.expanded = true;
}
};
}
};

const callback = (node: Tree) : void => {
if (node.filePath === filePath) {
node.children.forEach(child => {
this.#traverseTree(getChildNodes, child);
});
const callback = (node: Tree): void => {
if (node.filePath === filePath) {
node.children.forEach(child => {
this.#traverseTree(getChildNodes, child);
});

const newNode = this.parser(node);
const newNode = this.parser(node);

this.#traverseTree(matchExpand, newNode);
this.#traverseTree(matchExpand, newNode);

children = [];
}
};
children = [];
}
};

this.#traverseTree(callback, this.tree);
this.#traverseTree(callback, this.tree);

return this.tree;
}
return this.tree;
}

// Traverses the tree and changes expanded property of node whose id matches provided id
public toggleNode(id : string, expanded : boolean) : Tree | undefined {
const callback = (node : Tree) => {
public toggleNode(id: string, expanded: boolean): Tree | undefined {
const callback = (node: Tree) => {
if (node.id === id) {
node.expanded = expanded;
}
Expand All @@ -124,20 +123,20 @@ export class SaplingParser {
}

// Traverses all nodes of current component tree and applies callback to each node
#traverseTree(callback : Function, node : Tree | undefined = this.tree) : void {
#traverseTree(callback: Function, node: Tree | undefined = this.tree): void {
if (!node) {
return;
}

callback(node);

node.children.forEach( (childNode) => {
node.children.forEach((childNode) => {
this.#traverseTree(callback, childNode);
});
}

// Recursively builds the React component tree structure starting from root node
private parser(componentTree: Tree) : Tree {
private parser(componentTree: Tree): Tree {

// If import is a node module, do not parse any deeper
if (!['\\', '/', '.'].includes(componentTree.importPath[0])) {
Expand Down Expand Up @@ -196,7 +195,7 @@ export class SaplingParser {
}

// Finds files where import string does not include a file extension
private getFileName(componentTree: Tree) : string | null {
private getFileName(componentTree: Tree): string | null {
const ext = path.extname(componentTree.filePath);

if (!ext) {
Expand All @@ -213,16 +212,16 @@ export class SaplingParser {
// Extracts Imports from current file
// const Page1 = lazy(() => import('./page1')); -> is parsed as 'ImportDeclaration'
// import Page2 from './page2'; -> is parsed as 'VariableDeclaration'
private getImports(body : {[key : string]: any}[]) : ImportObj {
private getImports(body: { [key: string]: any }[]): ImportObj {
const bodyImports = body.filter(item => item.type === 'ImportDeclaration' || 'VariableDeclaration');
// console.log('bodyImports are: ', bodyImports);
return bodyImports.reduce((accum, curr) => {
// Import Declarations:
if (curr.type === 'ImportDeclaration') {
curr.specifiers.forEach((i : {[key : string]: any}) => {
curr.specifiers.forEach((i: { [key: string]: any }) => {
accum[i.local.name] = {
importPath: curr.source.value,
importName: (i.imported)? i.imported.name : i.local.name
importName: (i.imported) ? i.imported.name : i.local.name
};
});
}
Expand All @@ -242,7 +241,7 @@ export class SaplingParser {
}

// Recursive helper method to find import path in Variable Declaration
private findVarDecImports(ast: {[key: string]: any}) : string | boolean {
private findVarDecImports(ast: { [key: string]: any }): string | boolean {
// Base Case, find import path in variable declaration and return it,
if (ast.hasOwnProperty('callee') && ast.callee.type === 'Import') {
return ast.arguments[0].value;
Expand All @@ -262,24 +261,24 @@ export class SaplingParser {
}

// Finds JSX React Components in current file
private getJSXChildren(astTokens: any[], importsObj : ImportObj, parentNode: Tree) : Tree[] {
let childNodes: {[key : string]: Tree} = {};
let props : {[key : string]: boolean} = {};
let token : {[key: string]: any};
private getJSXChildren(astTokens: any[], importsObj: ImportObj, parentNode: Tree): Tree[] {
let childNodes: { [key: string]: Tree } = {};
let props: { [key: string]: boolean } = {};
let token: { [key: string]: any };

for (let i = 0; i < astTokens.length; i++) {
// Case for finding JSX tags eg <App .../>
if (astTokens[i].type.label === 'jsxTagStart'
&& astTokens[i + 1].type.label === 'jsxName'
&& importsObj[astTokens[i + 1].value]) {
&& astTokens[i + 1].type.label === 'jsxName'
&& importsObj[astTokens[i + 1].value]) {
token = astTokens[i + 1];
props = this.getJSXProps(astTokens, i + 2);
childNodes = this.getChildNodes(importsObj, token, props, parentNode, childNodes);

// Case for finding components passed in as props e.g. <Route component={App} />
} else if (astTokens[i].type.label === 'jsxName'
&& (astTokens[i].value === 'component' || astTokens[i].value === 'children')
&& importsObj[astTokens[i + 3].value]) {
&& (astTokens[i].value === 'component' || astTokens[i].value === 'children')
&& importsObj[astTokens[i + 3].value]) {
token = astTokens[i + 3];
childNodes = this.getChildNodes(importsObj, token, props, parentNode, childNodes);
}
Expand All @@ -288,13 +287,13 @@ export class SaplingParser {
return Object.values(childNodes);
}

private getChildNodes(imports : ImportObj,
astToken : {[key: string]: any}, props : {[key : string]: boolean},
parent : Tree, children : {[key : string] : Tree}) : {[key : string] : Tree} {
private getChildNodes(imports: ImportObj,
astToken: { [key: string]: any }, props: { [key: string]: boolean },
parent: Tree, children: { [key: string]: Tree }): { [key: string]: Tree } {

if (children[astToken.value]) {
children[astToken.value].count += 1;
children[astToken.value].props = {...children[astToken.value].props, ...props};
children[astToken.value].props = { ...children[astToken.value].props, ...props };
} else {
// Add tree node to childNodes if one does not exist
children[astToken.value] = {
Expand All @@ -320,8 +319,8 @@ export class SaplingParser {
}

// Extracts prop names from a JSX element
private getJSXProps(astTokens: {[key: string]: any}[], j : number) : {[key : string]: boolean} {
const props : {[key : string]: boolean} = {};
private getJSXProps(astTokens: { [key: string]: any }[], j: number): { [key: string]: boolean } {
const props: { [key: string]: boolean } = {};
while (astTokens[j].type.label !== "jsxTagEnd") {
if (astTokens[j].type.label === "jsxName" && astTokens[j + 1].value === "=") {
props[astTokens[j].value] = true;
Expand All @@ -332,11 +331,11 @@ export class SaplingParser {
}

// Checks if current Node is connected to React-Redux Store
private checkForRedux(astTokens: any[], importsObj : ImportObj) : boolean {
private checkForRedux(astTokens: any[], importsObj: ImportObj): boolean {
// Check that react-redux is imported in this file (and we have a connect method or otherwise)
let reduxImported = false;
let connectAlias;
Object.keys(importsObj).forEach( key => {
Object.keys(importsObj).forEach(key => {
if (importsObj[key].importPath === 'react-redux' && importsObj[key].importName === 'connect') {
reduxImported = true;
connectAlias = key;
Expand Down
12 changes: 6 additions & 6 deletions sapling/src/test/suite/parser.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SaplingParser } from '../../SaplingParser';
import { Tree } from '../../types/Tree';
import { describe, suite , test, before} from 'mocha';
import { describe, suite, test, before } from 'mocha';
import { expect } from 'chai';
import * as path from 'path';

Expand All @@ -10,11 +10,11 @@ import * as path from 'path';
// import * as myExtension from '../../extension';

suite('Parser Test Suite', () => {
let parser : SaplingParser, tree : Tree, file : string;
let parser: SaplingParser, tree: Tree, file: string;

// UNPARSED TREE TEST
describe('It initializes correctly', () => {
before( () => {
before(() => {
file = path.join(__dirname, '../../../src/test/test_apps/test_0/index.js');
parser = new SaplingParser(file);
});
Expand All @@ -31,7 +31,7 @@ suite('Parser Test Suite', () => {

// TEST 0: ONE CHILD
describe('It works for simple apps', () => {
before( () => {
before(() => {
file = path.join(__dirname, '../../../src/test/test_apps/test_0/index.js');
parser = new SaplingParser(file);
tree = parser.parse();
Expand Down Expand Up @@ -142,7 +142,7 @@ suite('Parser Test Suite', () => {

// TEST 5: MISSING EXTENSIONS AND UNUSED IMPORTS
describe('It works for extension-less imports', () => {
let names: string[], paths: string [], expectedNames : string[], expectedPaths : string[];
let names: string[], paths: string[], expectedNames: string[], expectedPaths: string[];
before(() => {
file = path.join(__dirname, '../../../src/test/test_apps/test_5/index.js');
parser = new SaplingParser(file);
Expand All @@ -157,7 +157,7 @@ suite('Parser Test Suite', () => {
'../../../src/test/test_apps/test_5/components/JSX.jsx',
'../../../src/test/test_apps/test_5/components/TS.ts',
'../../../src/test/test_apps/test_5/components/TSX.tsx'
].map( el => path.resolve(__dirname, el));
].map(el => path.resolve(__dirname, el));
});

test('Check children match expected children', () => {
Expand Down
2 changes: 1 addition & 1 deletion sapling/src/types/ImportObj.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ImportObj type, used by SaplingParser
// ImportObj holds data about imports in the current JS/TS file
export type ImportObj = {
[key : string]: {importPath: string, importName: string}
[key: string]: { importPath: string, importName: string }
};