Skip to content

Commit

Permalink
feat: js-scope refactor (#195)
Browse files Browse the repository at this point in the history
* fix: js-scope refactor - various renames and structure changes

* fix: make jsxScope static property read-only

* fix: minor formatting changes
  • Loading branch information
francinelucca committed Mar 11, 2024
1 parent 6c2d122 commit dc4da39
Show file tree
Hide file tree
Showing 72 changed files with 869 additions and 601 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,35 @@ import path from 'node:path'

import * as ts from 'typescript'

import { type Logger } from '../../../core/log/logger.js'
import { TrackedFileEnumerator } from '../../../core/tracked-file-enumerator.js'
import { type Logger } from './log/logger.js'
import { TrackedFileEnumerator } from './tracked-file-enumerator.js'

/**
* Gets all tracked source files to consider for data collection.
* Gets all tracked source files to consider for data collection,
* filtered by supplied file extension array.
*
* @param root - Root directory in which to search for tracked source files. This is an absolute
* path.
* @param logger - Logger instance to use.
* @param fileExtensions - List of file extensions to filter files by.
* @returns An array of source file objects.
*/
export async function getTrackedSourceFiles(root: string, logger: Logger) {
logger.traceEnter('', 'getTrackedSourceFiles', [root])
export async function getTrackedSourceFiles(
root: string,
logger: Logger,
fileExtensions: string[]
) {
logger.traceEnter('', 'getTrackedSourceFiles', [root, fileExtensions])

const fileEnumerator = new TrackedFileEnumerator(logger)
const allowedExtensions = ['.js', '.mjs', '.cjs', '.jsx', '.tsx']
const files = []

// If a file is passed instead of a directory, avoid the `git ls-tree` call
if (allowedExtensions.includes(path.extname(root))) {
if (fileExtensions.includes(path.extname(root))) {
files.push(root)
} else {
files.push(
...(await fileEnumerator.find(root, (file) => allowedExtensions.includes(path.extname(file))))
...(await fileEnumerator.find(root, (file) => fileExtensions.includes(path.extname(file))))
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*/

/**
* Object representing a complex attribute.
* Object representing a complex value.
*/
export class ComplexAttribute {
export class ComplexValue {
constructor(public complexValue: unknown) {}
}
88 changes: 88 additions & 0 deletions src/main/scopes/js/find-relevant-source-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright IBM Corp. 2024, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/

import getPropertyByPath from 'lodash/get.js'
import { ObjectPath } from 'object-scan'
import path from 'path'

import { getTrackedSourceFiles } from '../../core/get-tracked-source-files.js'
import { Logger } from '../../core/log/logger.js'
import { NoInstallationFoundError } from '../../exceptions/no-installation-found-error.js'
import { getDependencyTree } from '../npm/get-dependency-tree.js'
import { getDirectoryPrefix } from '../npm/get-directory-prefix.js'
import { getInstalledVersionPaths } from '../npm/get-installed-version-paths.js'
import { getPackageData } from '../npm/get-package-data.js'
import { getPackageTrees } from '../npm/get-package-trees.js'
import { getTreePredecessor } from '../npm/get-tree-predecessor.js'
import { DependencyTree, PackageData } from '../npm/interfaces.js'

/**
* Finds tracked source files and then filters them based on ones that appear in a project which
* depends on the in-context instrumented package/version.
*
* @param instrumentedPackage - Data about the instrumented package to use during filtering.
* @param cwd - Current working directory. This must be inside of the root directory. This is an
* absolute path.
* @param root - Root-most directory. This is an absolute path.
* @param fileExtensions - List of file extensions to capture metrics for.
* @param logger - Logger instance.
* @returns A (possibly empty) array of source files.
*/
export async function findRelevantSourceFiles(
instrumentedPackage: PackageData,
cwd: string,
root: string,
fileExtensions: string[],
logger: Logger
) {
logger.traceEnter('', 'findRelevantSourceFiles', [instrumentedPackage, cwd, root, fileExtensions])
const sourceFiles = await getTrackedSourceFiles(root, logger, fileExtensions)

const dependencyTree = await getDependencyTree(cwd, root, logger)

const filterPromises = sourceFiles.map(async (f) => {
const prefix = await getDirectoryPrefix(path.dirname(f.fileName), logger)
const prefixPackageData = await getPackageData(prefix, root, logger)

let packageTrees = getPackageTrees(dependencyTree, prefixPackageData)

let instrumentedInstallVersions: string[] | undefined = undefined
let shortestPathLength: number | undefined = undefined
do {
for (const tree of packageTrees) {
const instrumentedInstallPaths = getInstalledVersionPaths(tree, instrumentedPackage.name)
if (instrumentedInstallPaths.length > 0) {
const pathsLength = instrumentedInstallPaths[0]?.length ?? 0
if (shortestPathLength === undefined || pathsLength < shortestPathLength) {
instrumentedInstallVersions = instrumentedInstallPaths.map(
(path) => getPropertyByPath(tree, path)['version']
)
shortestPathLength = pathsLength
}
}
}
// did not find, go up one level for all packages
packageTrees = packageTrees
.map((tree) => getTreePredecessor(dependencyTree, tree['path'] as ObjectPath))
.filter((tree) => tree !== undefined) as DependencyTree[]
} while (shortestPathLength === undefined && packageTrees.length > 0)

if (instrumentedInstallVersions === undefined) {
throw new NoInstallationFoundError(instrumentedPackage.name)
}

return instrumentedInstallVersions.some((version) => version === instrumentedPackage.version)
})
const filterData = await Promise.all(filterPromises)

const results = sourceFiles.filter((_, index) => {
return filterData[index]
})

logger.traceEnter('', 'findRelevantSourceFiles', results)
return results
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as ts from 'typescript'

import { type JsxImport } from '../interfaces.js'
import { JsImport } from '../interfaces.js'
import { ImportParser } from './import-parser.js'

/**
Expand All @@ -24,7 +24,7 @@ export class AllImportParser extends ImportParser {
* @returns Array of JsxImport.
*/
parse(importNode: ts.ImportClause, importPath: string) {
const allImports: JsxImport[] = []
const allImports: JsImport[] = []

if (importNode.namedBindings?.kind === ts.SyntaxKind.NamespaceImport) {
allImports.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import * as ts from 'typescript'

import { DEFAULT_ELEMENT_NAME, DEFAULT_IMPORT_KEY } from '../constants.js'
import { type JsxImport } from '../interfaces.js'
import { DEFAULT_ELEMENT_NAME, DEFAULT_IMPORT_KEY } from '../../jsx/constants.js'
import { JsImport } from '../interfaces.js'
import { ImportParser } from './import-parser.js'

/**
Expand All @@ -25,7 +25,7 @@ export class DefaultImportParser extends ImportParser {
* @returns Array of JsxImportElement.
*/
parse(importNode: ts.ImportClause, importPath: string) {
const defaultImports: JsxImport[] = []
const defaultImports: JsImport[] = []

if (importNode.namedBindings?.kind === ts.SyntaxKind.NamedImports) {
importNode.namedBindings.elements.forEach((element) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

import type * as ts from 'typescript'

import { type JsxImport } from '../interfaces.js'
import { JsImport } from '../interfaces.js'

/**
* Defines API to construct JsxImportElements from ImportClause nodes.
*/
export abstract class ImportParser {
abstract parse(importNode: ts.ImportClause, importPath: string): JsxImport[]
abstract parse(importNode: ts.ImportClause, importPath: string): JsImport[]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as ts from 'typescript'

import { type JsxImport } from '../interfaces.js'
import { JsImport } from '../interfaces.js'
import { ImportParser } from './import-parser.js'

/**
Expand All @@ -24,7 +24,7 @@ export class NamedImportParser extends ImportParser {
* @returns Array of JsxImportElement.
*/
parse(importNode: ts.ImportClause, importPath: string) {
const namedImports: JsxImport[] = []
const namedImports: JsImport[] = []

if (importNode.namedBindings?.kind === ts.SyntaxKind.NamedImports) {
importNode.namedBindings.elements.forEach((element) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as ts from 'typescript'

import { type JsxImport } from '../interfaces.js'
import { JsImport } from '../interfaces.js'
import { ImportParser } from './import-parser.js'

/**
Expand All @@ -24,7 +24,7 @@ export class RenamedImportParser extends ImportParser {
* @returns Array of JsxImportElement.
*/
parse(importNode: ts.ImportClause, importPath: string) {
const renamedImports: JsxImport[] = []
const renamedImports: JsImport[] = []

if (importNode.namedBindings?.kind === ts.SyntaxKind.NamedImports) {
importNode.namedBindings.elements.forEach((element) => {
Expand Down
37 changes: 37 additions & 0 deletions src/main/scopes/js/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright IBM Corp. 2024, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import type * as ts from 'typescript'

import { Logger } from '../../core/log/logger.js'
import { ComplexValue } from './complex-value.js'
import { JsNodeHandler } from './node-handlers/js-node-handler.js'
import { NodeValueHandler } from './node-handlers/value-handlers/node-value-handler.js'

export interface JsImport {
name: string
path: string
isDefault: boolean
isAll: boolean
rename?: string
}

type JsNodeHandlerClass<DataType> = new (
node: ts.SourceFile,
logger: Logger
) => JsNodeHandler<DataType>

export type JsNodeHandlerMap = Partial<Record<ts.SyntaxKind, JsNodeHandlerClass<unknown>>>

export type NodeValue = string | number | boolean | ComplexValue | null | undefined

type NodeValueHandlerProducer = new (node: ts.SourceFile, logger: Logger) => NodeValueHandler

export type NodeValueHandlerMap = Partial<Record<ts.SyntaxKind, NodeValueHandlerProducer>>

export interface JsImportMatcher<Element> {
findMatch: (element: Element, imports: JsImport[]) => JsImport | undefined
}
19 changes: 19 additions & 0 deletions src/main/scopes/js/js-accumulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright IBM Corp. 2023, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { JsImport } from '../js/interfaces.js'

/**
* Base class for all JS Accumulators.
* Responsible for maintaining an aggregated state of imports and other elements.
*/
export abstract class JsAccumulator {
public readonly imports: JsImport[]

constructor() {
this.imports = []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@
*/
import type * as ts from 'typescript'

import { AllImportParser } from '../../import-parsers/all-import-parser.js'
import { DefaultImportParser } from '../../import-parsers/default-import-parser.js'
import { NamedImportParser } from '../../import-parsers/named-import-parser.js'
import { RenamedImportParser } from '../../import-parsers/renamed-import-parser.js'
import { type JsxImport } from '../../interfaces.js'
import { type JsxElementAccumulator } from '../../jsx-element-accumulator.js'
import { ElementNodeHandler } from './element-node-handler.js'
import { type JsxElementAccumulator } from '../../jsx/jsx-element-accumulator.js'
import { AllImportParser } from '../import-parsers/all-import-parser.js'
import { DefaultImportParser } from '../import-parsers/default-import-parser.js'
import { NamedImportParser } from '../import-parsers/named-import-parser.js'
import { RenamedImportParser } from '../import-parsers/renamed-import-parser.js'
import { JsImport } from '../interfaces.js'
import { JsNodeHandler } from './js-node-handler.js'

/**
* Holds logic to construct a JsxImport object given an ImportDeclaration node.
*
*/
export class ImportNodeHandler extends ElementNodeHandler<JsxImport[]> {
export class ImportNodeHandler extends JsNodeHandler<JsImport[]> {
/**
* Processes an ImportDeclaration node data and adds it to the given accumulator.
*
Expand All @@ -35,15 +35,15 @@ export class ImportNodeHandler extends ElementNodeHandler<JsxImport[]> {
* @param node - Node element to process.
* @returns Constructed JsxImport object.
*/
getData(node: ts.ImportDeclaration): JsxImport[] {
getData(node: ts.ImportDeclaration): JsImport[] {
const importParsers = [
new AllImportParser(),
new DefaultImportParser(),
new NamedImportParser(),
new RenamedImportParser()
]

const results: JsxImport[] = []
const results: JsImport[] = []

const importClause = node.importClause
// This has quotes on it which need to be removed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@

import type * as ts from 'typescript'

import { Loggable } from '../../../../core/log/loggable.js'
import { type Logger } from '../../../../core/log/logger.js'
import { type JsxElementAccumulator } from '../../jsx-element-accumulator.js'
import { Loggable } from '../../../core/log/loggable.js'
import { type Logger } from '../../../core/log/logger.js'
import { JsAccumulator } from '../js-accumulator.js'

/**
* Defines API to process typescript AST nodes and capture elements and imports.
*
* @param node - Node element to process.
* @param accumulator - Keeps the state of the collected data (by the handlers).
*/
export abstract class ElementNodeHandler<DataType> extends Loggable {
export abstract class JsNodeHandler<DataType> extends Loggable {
protected readonly sourceFile: ts.SourceFile

constructor(sourceFile: ts.SourceFile, logger: Logger) {
super(logger)
this.sourceFile = sourceFile
}

abstract handle(node: ts.Node, accumulator: JsxElementAccumulator): void
abstract handle(node: ts.Node, accumulator: JsAccumulator): void

abstract getData(node: ts.Node): DataType
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
*/
import type * as ts from 'typescript'

import { ComplexAttribute } from '../../complex-attribute.js'
import { AttributeNodeHandler } from './attribute-node-handler.js'
import { ComplexValue } from '../../complex-value.js'
import { NodeValueHandler } from './node-value-handler.js'

/**
* Holds logic to extract raw data from an AST node.
*
*/
export class DefaultHandler extends AttributeNodeHandler {
export class DefaultHandler extends NodeValueHandler {
/**
* Extracts raw string representation of node.
*
* @param node - Node to extract data from.
* @returns Text value of node.
*/
public getData(node: ts.Node) {
return new ComplexAttribute(this.sourceFile.text.substring(node.pos, node.end).trim())
return new ComplexValue(this.sourceFile.text.substring(node.pos, node.end).trim())
}
}

0 comments on commit dc4da39

Please sign in to comment.