From 7c0cb1e740f37cbfe740c944288ce0ca23593261 Mon Sep 17 00:00:00 2001 From: Rejman Date: Sun, 12 Oct 2025 22:11:02 -0300 Subject: [PATCH 1/4] refactor: from function to object --- src/app/namespace/openTextDocument.ts | 21 ---- src/app/namespace/remove/removeImports.ts | 43 ------- .../namespace/remove/removeUnusedImports.ts | 58 --------- .../update/import/findUnimportedClasses.ts | 37 ------ .../import/getClassesNamesInDirectory.ts | 18 --- .../update/import/importMissingClasses.ts | 67 ---------- .../namespace/update/updateInCurrentFile.ts | 39 ------ src/app/namespace/update/updateInFile.ts | 50 -------- src/app/namespace/update/updateReferences.ts | 45 ------- .../update/updateReferencesInFiles.ts | 67 ---------- src/app/services/MissingClassImporter.ts | 83 +++++++++++++ src/app/services/NamespaceBatchUpdater.ts | 45 +++++++ src/app/services/TextDocumentOpener.ts | 20 +++ .../services/import/UnusedImportDetector.ts | 36 ++++++ src/app/services/remove/ImportRemover.ts | 101 +++++++++++++++ .../update/MovedFileNamespaceUpdater.ts | 37 ++++++ .../update/MultiFileReferenceUpdater.ts | 115 ++++++++++++++++++ .../services/workspace/WorkspaceFileFinder.ts | 52 ++++++++ src/app/workespace/findPhpFilesInWorkspace.ts | 41 ------- src/domain/namespace/NamespaceCreator.ts | 46 +++++++ src/domain/namespace/UseStatementCreator.ts | 37 ++++++ src/domain/namespace/UseStatementInjector.ts | 38 ++++++ src/domain/namespace/UseStatementLocator.ts | 39 ++++++ src/domain/namespace/createNamespace.ts | 21 ---- src/domain/namespace/findLastUseEndIndex.ts | 23 ---- src/domain/namespace/findNamespaceEndIndex.ts | 20 --- src/domain/namespace/findUseInsertionIndex.ts | 19 --- src/domain/namespace/generateNamespace.ts | 30 ----- src/domain/namespace/generateUseStatement.ts | 11 -- .../generateUseStatementsForClasses.ts | 24 ---- .../namespace/import/insertUseStatement.ts | 30 ----- src/domain/workspace/ConfigurationLocator.ts | 25 ++++ src/domain/workspace/FeatureFlagManager.ts | 18 +++ src/domain/workspace/WorkspacePathResolver.ts | 26 ++++ src/extension.ts | 30 +++-- src/infra/autoload/AutoloadPathResolver.ts | 31 +++++ src/infra/autoload/ComposerAutoloadManager.ts | 56 +++++++++ src/infra/autoload/NamespaceAutoloadMapper.ts | 35 ++++++ src/infra/autoload/fetchComposerAutoload.ts | 57 --------- src/infra/autoload/mapAutoloadNamespaces.ts | 37 ------ src/infra/autoload/resolvePathFromPrefix.ts | 37 ------ src/infra/utils/constants.ts | 6 - src/infra/utils/filePathUtils.ts | 15 --- src/infra/workspace/configTypes.ts | 16 --- src/infra/workspace/vscodeConfig.ts | 18 --- 45 files changed, 858 insertions(+), 862 deletions(-) delete mode 100644 src/app/namespace/openTextDocument.ts delete mode 100644 src/app/namespace/remove/removeImports.ts delete mode 100644 src/app/namespace/remove/removeUnusedImports.ts delete mode 100644 src/app/namespace/update/import/findUnimportedClasses.ts delete mode 100644 src/app/namespace/update/import/getClassesNamesInDirectory.ts delete mode 100644 src/app/namespace/update/import/importMissingClasses.ts delete mode 100644 src/app/namespace/update/updateInCurrentFile.ts delete mode 100644 src/app/namespace/update/updateInFile.ts delete mode 100644 src/app/namespace/update/updateReferences.ts delete mode 100644 src/app/namespace/update/updateReferencesInFiles.ts create mode 100644 src/app/services/MissingClassImporter.ts create mode 100644 src/app/services/NamespaceBatchUpdater.ts create mode 100644 src/app/services/TextDocumentOpener.ts create mode 100644 src/app/services/import/UnusedImportDetector.ts create mode 100644 src/app/services/remove/ImportRemover.ts create mode 100644 src/app/services/update/MovedFileNamespaceUpdater.ts create mode 100644 src/app/services/update/MultiFileReferenceUpdater.ts create mode 100644 src/app/services/workspace/WorkspaceFileFinder.ts delete mode 100644 src/app/workespace/findPhpFilesInWorkspace.ts create mode 100644 src/domain/namespace/NamespaceCreator.ts create mode 100644 src/domain/namespace/UseStatementCreator.ts create mode 100644 src/domain/namespace/UseStatementInjector.ts create mode 100644 src/domain/namespace/UseStatementLocator.ts delete mode 100644 src/domain/namespace/createNamespace.ts delete mode 100644 src/domain/namespace/findLastUseEndIndex.ts delete mode 100644 src/domain/namespace/findNamespaceEndIndex.ts delete mode 100644 src/domain/namespace/findUseInsertionIndex.ts delete mode 100644 src/domain/namespace/generateNamespace.ts delete mode 100644 src/domain/namespace/generateUseStatement.ts delete mode 100644 src/domain/namespace/generateUseStatementsForClasses.ts delete mode 100644 src/domain/namespace/import/insertUseStatement.ts create mode 100644 src/domain/workspace/ConfigurationLocator.ts create mode 100644 src/domain/workspace/FeatureFlagManager.ts create mode 100644 src/domain/workspace/WorkspacePathResolver.ts create mode 100644 src/infra/autoload/AutoloadPathResolver.ts create mode 100644 src/infra/autoload/ComposerAutoloadManager.ts create mode 100644 src/infra/autoload/NamespaceAutoloadMapper.ts delete mode 100644 src/infra/autoload/fetchComposerAutoload.ts delete mode 100644 src/infra/autoload/mapAutoloadNamespaces.ts delete mode 100644 src/infra/autoload/resolvePathFromPrefix.ts delete mode 100644 src/infra/utils/filePathUtils.ts delete mode 100644 src/infra/workspace/configTypes.ts delete mode 100644 src/infra/workspace/vscodeConfig.ts diff --git a/src/app/namespace/openTextDocument.ts b/src/app/namespace/openTextDocument.ts deleted file mode 100644 index e6895ff..0000000 --- a/src/app/namespace/openTextDocument.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TextDocument, Uri, workspace } from 'vscode'; - -interface Props { - uri: Uri -} - -type OpenTextDocument = { - document: TextDocument - text: string -} - -export async function openTextDocument({ - uri, -}: Props): Promise { - const document = await workspace.openTextDocument(uri.fsPath); - - return { - document, - text: document.getText(), - }; -} diff --git a/src/app/namespace/remove/removeImports.ts b/src/app/namespace/remove/removeImports.ts deleted file mode 100644 index 7a3f3c2..0000000 --- a/src/app/namespace/remove/removeImports.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Range, TextDocument, workspace, WorkspaceEdit } from 'vscode'; - -interface Props { - document: TextDocument - fileNames: string[] -} - -export async function removeImports({ - document, - fileNames, -}: Props) { - const text = document.getText(); - - const edit = new WorkspaceEdit(); - let isEdit = false; - - const importLines = text.split('\n').filter(line => line.startsWith('use ')); - for (const line of importLines) { - const parts = line.split(' '); - if (parts.length < 2) { - continue; - } - - const importedClass = parts[1].replace(';', '').split('\\').pop() || ''; - if (!fileNames.includes(importedClass)) { - continue; - } - - isEdit = true; - - const lineIndex = text.indexOf(line); - edit.delete(document.uri, new Range( - document.positionAt(lineIndex), - document.positionAt((lineIndex + line.length) + 1) - )); - } - - if (false === isEdit) { - return; - } - - await workspace.applyEdit(edit); -} diff --git a/src/app/namespace/remove/removeUnusedImports.ts b/src/app/namespace/remove/removeUnusedImports.ts deleted file mode 100644 index 5fc183f..0000000 --- a/src/app/namespace/remove/removeUnusedImports.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { extractClassNameFromPath, extractDirectoryFromPath } from '@infra/utils/filePathUtils'; -import { RelativePattern, Uri, workspace } from 'vscode'; -import { ConfigKeys } from '@infra/workspace/configTypes'; -import { generateNamespace } from '@domain/namespace/generateNamespace'; -import { isConfigEnabled } from '@infra/workspace/vscodeConfig'; -import { openTextDocument } from '../openTextDocument'; -import { removeImports } from './removeImports'; - -interface Props { - uri: Uri -} - -export async function removeUnusedImports({ uri }: Props) { - if (!isConfigEnabled({ key: ConfigKeys.REMOVE_UNUSED_IMPORTS })) { - return; - } - - const { className } = await generateNamespace({ - uri: uri.fsPath, - }); - - const directoryPath = extractDirectoryFromPath(uri.fsPath); - - const pattern = new RelativePattern(Uri.parse(`file://${directoryPath}`), '*.php'); - const phpFiles = await workspace.findFiles(pattern); - - const fileNames = phpFiles.map(uri => extractClassNameFromPath(uri.fsPath)) - .filter(Boolean) - .filter(name => name !== className); - - if (!fileNames.length) { - return; - } - - try { - const { document } = await openTextDocument({ uri }); - await removeImports({ - document, - fileNames, - }); - } catch (_) { - // Main file might not exist, skip processing - } - - const otherFiles = phpFiles.filter(file => file.fsPath !== uri.fsPath); - - await Promise.all(otherFiles.map(async (file) => { - try { - const { document } = await openTextDocument({ uri: file }); - await removeImports({ - document, - fileNames: [className], - }); - } catch (_) { - return; - } - })); -} diff --git a/src/app/namespace/update/import/findUnimportedClasses.ts b/src/app/namespace/update/import/findUnimportedClasses.ts deleted file mode 100644 index c7f9d35..0000000 --- a/src/app/namespace/update/import/findUnimportedClasses.ts +++ /dev/null @@ -1,37 +0,0 @@ -interface Props { - text: string, - classes: string[], -} - -export function findUnimportedClasses({ - text, - classes, -}: Props): string[] { - const classesUsed: string[] = []; - - classes.forEach(className => { - const regex = new RegExp(`\\b${className}\\b`, 'g'); - if (regex.test(text) && !classesUsed.includes(className)) { - classesUsed.push(className); - } - }); - - const existingImports: string[] = extractClassesExistingImports(text); - - return classesUsed.filter(className => !existingImports.includes(className)); -} - -function extractClassesExistingImports(text: string): string[] { - const regex = /use\s+([a-zA-Z0-9\\]+)/g; - const imports: string[] = []; - - let match; - while ((match = regex.exec(text)) !== null) { - imports.push(match[1]); - } - - return imports.map(namespace => { - const parts = namespace.split('\\'); - return parts[parts.length - 1]; - }); -} diff --git a/src/app/namespace/update/import/getClassesNamesInDirectory.ts b/src/app/namespace/update/import/getClassesNamesInDirectory.ts deleted file mode 100644 index 681c0e7..0000000 --- a/src/app/namespace/update/import/getClassesNamesInDirectory.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { extractClassNameFromPath } from '@infra/utils/filePathUtils'; -import { promises as fs } from 'fs'; -import { PHP_EXTENSION } from '@infra/utils/constants'; - -interface Props { - directory: string -} - -export async function getClassesNamesInDirectory({ directory }: Props) { - try { - const files = await fs.readdir(directory); - return files.filter(file => file.endsWith(PHP_EXTENSION)) - .map(file => extractClassNameFromPath(file)) - .filter(Boolean); - } catch (error) { - return []; - } -} diff --git a/src/app/namespace/update/import/importMissingClasses.ts b/src/app/namespace/update/import/importMissingClasses.ts deleted file mode 100644 index 9ae837a..0000000 --- a/src/app/namespace/update/import/importMissingClasses.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Uri, workspace, WorkspaceEdit } from 'vscode'; -import { extractDirectoryFromPath } from '@infra/utils/filePathUtils'; -import { findUnimportedClasses } from './findUnimportedClasses'; -import { findUseInsertionIndex } from '@domain/namespace/findUseInsertionIndex'; -import { generateUseStatementsForClasses } from '@domain/namespace/generateUseStatementsForClasses'; -import { getClassesNamesInDirectory } from './getClassesNamesInDirectory'; -import { insertUseStatement } from '@domain/namespace/import/insertUseStatement'; -import { openTextDocument } from '@app/namespace/openTextDocument'; - -interface Props { - oldFileName: string - newUri: Uri -} - -export async function importMissingClasses({ - oldFileName, - newUri, -}: Props) { - const directoryPath = extractDirectoryFromPath(oldFileName); - const classes: string[] = await getClassesNamesInDirectory({ - directory: directoryPath, - }); - - if (classes.length < 1) { - return; - } - - try { - const { document, text } = await openTextDocument({ uri: newUri }); - - const imports = await generateUseStatementsForClasses({ - classesUsed: findUnimportedClasses({ - text, - classes, - }), - directoryPath, - }); - - if (!imports || (directoryPath === extractDirectoryFromPath(newUri.fsPath))) { - return; - } - - const insertionIndex = findUseInsertionIndex({ document }); - if (insertionIndex === 0) { - return; - } - - const edit = new WorkspaceEdit(); - - for (const use of imports) { - await insertUseStatement({ - document, - workspaceEdit: edit, - uri: newUri, - lastUseEndIndex: insertionIndex, - useNamespace: use, - flush: false, - }); - } - - if (imports.length > 0) { - await workspace.applyEdit(edit); - } - } catch (_) { - return; - } -} diff --git a/src/app/namespace/update/updateInCurrentFile.ts b/src/app/namespace/update/updateInCurrentFile.ts deleted file mode 100644 index 718d15c..0000000 --- a/src/app/namespace/update/updateInCurrentFile.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Range, Uri, workspace, WorkspaceEdit } from 'vscode'; -import { openTextDocument } from '../openTextDocument'; - -interface Props { - newNamespace: string, - newUri: Uri, -} - -export async function updateInCurrentFile({ - newNamespace, - newUri, -}: Props) { - const { document, text } = await openTextDocument({ uri: newUri }); - - const namespaceRegex = /^\s*namespace\s+[\w\\]+;/m; - const match = text.match(namespaceRegex); - - if (!match) { - return false; - } - - const startIndex = match.index!; - const startPosition = document.positionAt(startIndex); - const endPosition = document.positionAt(startIndex + match[0].length); - - const namespaceReplace = `\nnamespace ${newNamespace};`; - - const edit = new WorkspaceEdit(); - - edit.replace( - newUri, - new Range(startPosition, endPosition), - namespaceReplace, - ); - - workspace.applyEdit(edit); - - return true; -} diff --git a/src/app/namespace/update/updateInFile.ts b/src/app/namespace/update/updateInFile.ts deleted file mode 100644 index 98db8c3..0000000 --- a/src/app/namespace/update/updateInFile.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Uri, WorkspaceEdit } from 'vscode'; -import { extractDirectoryFromPath } from '@infra/utils/filePathUtils'; -import { findUseInsertionIndex } from '@domain/namespace/findUseInsertionIndex'; -import { insertUseStatement } from '@domain/namespace/import/insertUseStatement'; -import { openTextDocument } from '../openTextDocument'; - -interface Props { - file: Uri - oldDirectoryPath: string - useImport: string - className: string -} - -export async function updateInFile({ - file, - oldDirectoryPath, - useImport, - className, -}: Props) { - const currentDir = extractDirectoryFromPath(file.fsPath); - if (oldDirectoryPath !== currentDir) { - return; - } - - try { - const { document, text } = await openTextDocument({ uri: file }); - - if (!text.includes(className)) { - return; - } - - const insertionIndex = findUseInsertionIndex({ document }); - if (insertionIndex === 0) { - return; - } - - const edit = new WorkspaceEdit(); - - await insertUseStatement({ - document, - workspaceEdit: edit, - uri: file, - lastUseEndIndex: insertionIndex, - useNamespace: useImport, - flush: true, - }); - } catch (_) { - return; - } -} diff --git a/src/app/namespace/update/updateReferences.ts b/src/app/namespace/update/updateReferences.ts deleted file mode 100644 index 6e7572a..0000000 --- a/src/app/namespace/update/updateReferences.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { generateNamespace } from '@domain/namespace/generateNamespace'; -import { updateInCurrentFile } from './updateInCurrentFile'; -import { updateReferencesInFiles } from './updateReferencesInFiles'; -import { Uri } from 'vscode'; - -interface Props { - newUri: Uri, - oldUri: Uri, -} - -export async function updateReferences({ - newUri, - oldUri, -}: Props) { - const { - namespace: newNamespace, - fullNamespace: useNewNamespace, - } = await generateNamespace({ - uri: newUri.fsPath, - }); - - if (!newNamespace) { - return; - } - - const { fullNamespace: useOldNamespace } = await generateNamespace({ - uri: oldUri.fsPath, - }); - - const updated = await updateInCurrentFile({ - newNamespace, - newUri, - }); - - if (!updated) { - return; - } - - await updateReferencesInFiles({ - useOldNamespace, - useNewNamespace, - newUri, - oldUri, - }); -} diff --git a/src/app/namespace/update/updateReferencesInFiles.ts b/src/app/namespace/update/updateReferencesInFiles.ts deleted file mode 100644 index 4879fc2..0000000 --- a/src/app/namespace/update/updateReferencesInFiles.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { extractClassNameFromPath, extractDirectoryFromPath } from '@infra/utils/filePathUtils'; -import { Uri, workspace } from 'vscode'; -import { findPhpFilesInWorkspace } from '../../workespace/findPhpFilesInWorkspace'; -import { generateUseStatement } from '@domain/namespace/generateUseStatement'; -import { removeUnusedImports } from '../remove/removeUnusedImports'; -import { updateInFile } from './updateInFile'; - -interface Props { - useOldNamespace: string - useNewNamespace: string - newUri: Uri - oldUri: Uri -} - -export async function updateReferencesInFiles({ - useOldNamespace, - useNewNamespace, - newUri, - oldUri, -}: Props) { - const directoryPath = extractDirectoryFromPath(oldUri.fsPath); - const className = extractClassNameFromPath(oldUri.fsPath); - - const useImport = generateUseStatement({ fullNamespace: useNewNamespace }); - - const ignoreFile = newUri.fsPath; - - const files = await findPhpFilesInWorkspace(); - - const filesToProcess = files.filter(file => ignoreFile !== file.fsPath); - - await Promise.all(filesToProcess.map(async (file) => { - try { - const fileStream = workspace.fs; - - await fileStream.stat(file); - - const fileContent = await fileStream.readFile(file); - let text = Buffer.from(fileContent).toString(); - - if (!text.includes(useOldNamespace)) { - await updateInFile({ - file, - oldDirectoryPath: directoryPath, - useImport, - className, - }); - - return; - } - - text = text.replace(useOldNamespace, useNewNamespace); - await fileStream.writeFile(file, Buffer.from(text)); - - await updateInFile({ - file, - oldDirectoryPath: directoryPath, - useImport, - className, - }); - } catch (_) { - return; - } - })); - - await removeUnusedImports({ uri: newUri }); -} diff --git a/src/app/services/MissingClassImporter.ts b/src/app/services/MissingClassImporter.ts new file mode 100644 index 0000000..2569f51 --- /dev/null +++ b/src/app/services/MissingClassImporter.ts @@ -0,0 +1,83 @@ +import { Uri, WorkspaceEdit } from 'vscode'; +import { promises as fs } from 'fs'; +import { PHP_EXTENSION } from '@infra/utils/constants'; +import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; +import { UnusedImportDetector } from './import/UnusedImportDetector'; +import { UseStatementCreator } from '@domain/namespace/UseStatementCreator'; +import { UseStatementInjector } from '@domain/namespace/UseStatementInjector'; +import { UseStatementLocator } from '@domain/namespace/UseStatementLocator'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +interface Props { + oldUri: Uri + newUri: Uri +} + +export class MissingClassImporter { + private workspacePathResolver: WorkspacePathResolver; + + constructor() { + this.workspacePathResolver = new WorkspacePathResolver(); + } + + public async execute({ oldUri, newUri }: Props) { + const directoryPath = this.workspacePathResolver.extractDirectoryFromPath(oldUri.fsPath); + const classes = await this.getClassesNamesInDirectory(directoryPath); + + if (classes.length < 1) { + return; + } + + try { + const { document, text } = await new TextDocumentOpener().execute({ uri: newUri }); + + const imports = await new UseStatementCreator().multiple({ + classesUsed: new UnusedImportDetector().execute({ + contentDocument: text, + classes, + }), + directoryPath, + }); + + if (!imports || (directoryPath === this.workspacePathResolver.extractDirectoryFromPath(newUri.fsPath))) { + return; + } + + const insertionIndex = new UseStatementLocator().execute({ document }); + if (insertionIndex === 0) { + return; + } + + const edit = new WorkspaceEdit(); + + const useStatementInjector = new UseStatementInjector(); + for (const use of imports) { + await useStatementInjector.save({ + document, + workspaceEdit: edit, + uri: newUri, + lastUseEndIndex: insertionIndex, + useNamespace: use, + flush: false, + }); + } + + if (imports.length > 0) { + await useStatementInjector.flush(edit); + } + } catch (_) { + return; + } + } + + private async getClassesNamesInDirectory(directory: string): Promise { + try { + const files = await fs.readdir(directory); + return files.filter(file => file.endsWith(PHP_EXTENSION)) + .map(file => this.workspacePathResolver.extractClassNameFromPath(file)) + .filter(Boolean); + } catch (_) { + return []; + } + } +} diff --git a/src/app/services/NamespaceBatchUpdater.ts b/src/app/services/NamespaceBatchUpdater.ts new file mode 100644 index 0000000..e0168bd --- /dev/null +++ b/src/app/services/NamespaceBatchUpdater.ts @@ -0,0 +1,45 @@ +import { MovedFileNamespaceUpdater } from './update/MovedFileNamespaceUpdater'; +import { MultiFileReferenceUpdater } from './update/MultiFileReferenceUpdater'; +import { NamespaceCreator } from '@domain/namespace/NamespaceCreator'; +import { Uri } from 'vscode'; + +interface Props { + newUri: Uri, + oldUri: Uri, +} + +export class NamespaceBatchUpdater { + public async execute({ newUri, oldUri }: Props) { + const namespaceCreator = new NamespaceCreator(); + const { + namespace: newNamespace, + fullNamespace: useNewNamespace, + } = await namespaceCreator.execute({ + uri: newUri, + }); + + if (!newNamespace) { + return; + } + + const { fullNamespace: useOldNamespace } = await namespaceCreator.execute({ + uri: oldUri, + }); + + const updated = await new MovedFileNamespaceUpdater().execute({ + newNamespace, + newUri, + }); + + if (!updated) { + return; + } + + await new MultiFileReferenceUpdater().execute({ + useOldNamespace, + useNewNamespace, + newUri, + oldUri, + }); + } +} diff --git a/src/app/services/TextDocumentOpener.ts b/src/app/services/TextDocumentOpener.ts new file mode 100644 index 0000000..971e555 --- /dev/null +++ b/src/app/services/TextDocumentOpener.ts @@ -0,0 +1,20 @@ +import { TextDocument, Uri, workspace } from 'vscode'; + +interface Props { + uri: Uri +} + +type OpenTextDocument = { + document: TextDocument + text: string +} + +export class TextDocumentOpener { + public async execute({ uri }: Props): Promise { + const document = await workspace.openTextDocument(uri.fsPath); + return { + document, + text: document.getText(), + }; + } +} diff --git a/src/app/services/import/UnusedImportDetector.ts b/src/app/services/import/UnusedImportDetector.ts new file mode 100644 index 0000000..e40fa1d --- /dev/null +++ b/src/app/services/import/UnusedImportDetector.ts @@ -0,0 +1,36 @@ +interface Props { + contentDocument: string, + classes: string[], +} + +export class UnusedImportDetector { + public execute({ contentDocument, classes }: Props): string[] { + const classesUsed: string[] = []; + + classes.forEach(className => { + const regex = new RegExp(`\\b${className}\\b`, 'g'); + if (regex.test(contentDocument) && !classesUsed.includes(className)) { + classesUsed.push(className); + } + }); + + const existingImports: string[] = this.extractClassesExistingImports(contentDocument); + + return classesUsed.filter(className => !existingImports.includes(className)); + } + + private extractClassesExistingImports(text: string): string[] { + const regex = /use\s+([a-zA-Z0-9\\]+)/g; + const imports: string[] = []; + + let match; + while ((match = regex.exec(text)) !== null) { + imports.push(match[1]); + } + + return imports.map(namespace => { + const parts = namespace.split('\\'); + return parts[parts.length - 1]; + }); + } +} diff --git a/src/app/services/remove/ImportRemover.ts b/src/app/services/remove/ImportRemover.ts new file mode 100644 index 0000000..85607f2 --- /dev/null +++ b/src/app/services/remove/ImportRemover.ts @@ -0,0 +1,101 @@ +import { Range, RelativePattern, TextDocument, Uri, workspace, WorkspaceEdit } from 'vscode'; +import { ConfigKeys } from '@domain/workspace/ConfigurationLocator'; +import { FeatureFlagManager } from '@domain/workspace/FeatureFlagManager'; +import { NamespaceCreator } from '@domain/namespace/NamespaceCreator'; +import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +interface Props { + uri: Uri +} + +interface RemoveImportsProps { + document: TextDocument + fileNames: string[] +} + +export class ImportRemover { + public async execute({ uri }: Props) { + const featureFlagManager = new FeatureFlagManager(); + + if (!featureFlagManager.isActive({ key: ConfigKeys.REMOVE_UNUSED_IMPORTS })) { + return; + } + + const { className } = await new NamespaceCreator().execute({ uri }); + + const workspacePathService = new WorkspacePathResolver(); + const directoryPath = workspacePathService.extractDirectoryFromPath(uri.fsPath); + + const pattern = new RelativePattern(Uri.parse(`file://${directoryPath}`), '*.php'); + const phpFiles = await workspace.findFiles(pattern); + + const fileNames = phpFiles.map(uri => workspacePathService.extractClassNameFromPath(uri.fsPath)) + .filter(Boolean) + .filter(name => name !== className); + + if (!fileNames.length) { + return; + } + + const textDocumentOpener = new TextDocumentOpener(); + + try { + const { document } = await textDocumentOpener.execute({ uri }); + await this.removeImports({ + document, + fileNames, + }); + } catch (_) { + // Main file might not exist, skip processing + } + + const otherFiles = phpFiles.filter(file => file.fsPath !== uri.fsPath); + + await Promise.all(otherFiles.map(async (file) => { + try { + const { document } = await textDocumentOpener.execute({ uri: file }); + await this.removeImports({ + document, + fileNames: [className], + }); + } catch (_) { + return; + } + })); + } + + private async removeImports({ document, fileNames }: RemoveImportsProps) { + const text = document.getText(); + + const edit = new WorkspaceEdit(); + let isEdit = false; + + const importLines = text.split('\n').filter(line => line.startsWith('use ')); + for (const line of importLines) { + const parts = line.split(' '); + if (parts.length < 2) { + continue; + } + + const importedClass = parts[1].replace(';', '').split('\\').pop() || ''; + if (!fileNames.includes(importedClass)) { + continue; + } + + isEdit = true; + + const lineIndex = text.indexOf(line); + edit.delete(document.uri, new Range( + document.positionAt(lineIndex), + document.positionAt((lineIndex + line.length) + 1) + )); + } + + if (false === isEdit) { + return; + } + + await workspace.applyEdit(edit); + } +} diff --git a/src/app/services/update/MovedFileNamespaceUpdater.ts b/src/app/services/update/MovedFileNamespaceUpdater.ts new file mode 100644 index 0000000..d56d8ee --- /dev/null +++ b/src/app/services/update/MovedFileNamespaceUpdater.ts @@ -0,0 +1,37 @@ +import { Range, Uri, workspace, WorkspaceEdit } from 'vscode'; +import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; + +interface Props { + newNamespace: string, + newUri: Uri, +} + +export class MovedFileNamespaceUpdater { + public async execute({ newNamespace, newUri }: Props) { + const { document, text } = await new TextDocumentOpener().execute({ uri: newUri }); + + const namespaceRegex = /^\s*namespace\s+[\w\\]+;/m; + const match = text.match(namespaceRegex); + + if (!match) { + return false; + } + + const startIndex = match.index!; + const startPosition = document.positionAt(startIndex); + const endPosition = document.positionAt(startIndex + match[0].length); + + const namespaceReplace = `\nnamespace ${newNamespace};`; + + const edit = new WorkspaceEdit(); + edit.replace( + newUri, + new Range(startPosition, endPosition), + namespaceReplace, + ); + + workspace.applyEdit(edit); + + return true; + } +} diff --git a/src/app/services/update/MultiFileReferenceUpdater.ts b/src/app/services/update/MultiFileReferenceUpdater.ts new file mode 100644 index 0000000..537ed86 --- /dev/null +++ b/src/app/services/update/MultiFileReferenceUpdater.ts @@ -0,0 +1,115 @@ +import { Uri, workspace, WorkspaceEdit } from 'vscode'; +import { ImportRemover } from '@app/services/remove/ImportRemover'; +import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; +import { UseStatementCreator } from '@domain/namespace/UseStatementCreator'; +import { UseStatementInjector } from '@domain/namespace/UseStatementInjector'; +import { UseStatementLocator } from '@domain/namespace/UseStatementLocator'; +import { WorkspaceFileFinder } from '@app/services/workspace/WorkspaceFileFinder'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +interface Props { + useOldNamespace: string + useNewNamespace: string + newUri: Uri + oldUri: Uri +} + +export class MultiFileReferenceUpdater { + private workspacePathResolver: WorkspacePathResolver; + + constructor() { + this.workspacePathResolver = new WorkspacePathResolver(); + } + + public async execute({ + useOldNamespace, + useNewNamespace, + newUri, + oldUri, + }: Props) { + const directoryPath = this.workspacePathResolver.extractDirectoryFromPath(oldUri.fsPath); + const className = this.workspacePathResolver.extractClassNameFromPath(oldUri.fsPath); + + const useImport = new UseStatementCreator().single({ fullNamespace: useNewNamespace }); + + const ignoreFile = newUri.fsPath; + + const files = await new WorkspaceFileFinder().execute(); + + const filesToProcess = files.filter(file => ignoreFile !== file.fsPath); + + await Promise.all(filesToProcess.map(async (file) => { + try { + const fileStream = workspace.fs; + + await fileStream.stat(file); + + const fileContent = await fileStream.readFile(file); + let text = Buffer.from(fileContent).toString(); + + if (!text.includes(useOldNamespace)) { + await this.updateInFile( + file, + directoryPath, + useImport, + className, + ); + + return; + } + + text = text.replace(useOldNamespace, useNewNamespace); + await fileStream.writeFile(file, Buffer.from(text)); + + await this.updateInFile( + file, + directoryPath, + useImport, + className, + ); + } catch (_) { + return; + } + })); + + await new ImportRemover().execute({ uri: newUri }); + } + + private async updateInFile( + file: Uri, + oldDirectoryPath: string, + useImport: string, + className: string, + ): Promise { + const currentDir = this.workspacePathResolver.extractDirectoryFromPath(file.fsPath); + if (oldDirectoryPath !== currentDir) { + return; + } + + try { + const { document, text } = await new TextDocumentOpener().execute({ uri: file }); + + if (!text.includes(className)) { + return; + } + + const insertionIndex = new UseStatementLocator().execute({ document }); + if (insertionIndex === 0) { + return; + } + + const edit = new WorkspaceEdit(); + + await new UseStatementInjector().save({ + document, + workspaceEdit: edit, + uri: file, + lastUseEndIndex: insertionIndex, + useNamespace: useImport, + flush: true, + }); + } catch (_) { + return; + } + } +} diff --git a/src/app/services/workspace/WorkspaceFileFinder.ts b/src/app/services/workspace/WorkspaceFileFinder.ts new file mode 100644 index 0000000..4e2f75f --- /dev/null +++ b/src/app/services/workspace/WorkspaceFileFinder.ts @@ -0,0 +1,52 @@ +import { ConfigKeys, ConfigurationLocator } from '@domain/workspace/ConfigurationLocator'; +import { Uri, workspace } from 'vscode'; + +const DEFAULT_DIRECTORIES = ['/vendor/', '/var/', '/cache/']; +const DEFAULT_EXTENSION_PHP = 'php'; + +export class WorkspaceFileFinder { + private cachedFiles: Uri[] | null = null; + private cacheTimestamp: number = 0; + private readonly cacheDuration: number; + + constructor(cacheDuration: number = 4) { + this.cacheDuration = 60 * 60 * cacheDuration; + } + + async execute(): Promise { + const now = Date.now(); + if (this.cachedFiles && (now - this.cacheTimestamp) < this.cacheDuration) { + return this.cachedFiles; + } + + const configurationLocator = new ConfigurationLocator(); + + const extensions = configurationLocator.get({ + key: ConfigKeys.ADDITIONAL_EXTENSIONS, + defaultValue: [DEFAULT_EXTENSION_PHP], + }); + + const pattern = `**/*.{${[DEFAULT_EXTENSION_PHP, ...extensions].join(',')}}`; + const files = await workspace.findFiles(pattern); + + const ignoredDirectories = configurationLocator.get({ + key: ConfigKeys.IGNORED_DIRECTORIES, + defaultValue: DEFAULT_DIRECTORIES, + }); + + const filteredFiles = files.filter(file => ![ + ...DEFAULT_DIRECTORIES, + ...ignoredDirectories, + ].some(dir => file.fsPath.includes(dir))); + + this.cachedFiles = filteredFiles; + this.cacheTimestamp = now; + + return filteredFiles; + } + + clearCache() { + this.cachedFiles = null; + this.cacheTimestamp = 0; + } +} diff --git a/src/app/workespace/findPhpFilesInWorkspace.ts b/src/app/workespace/findPhpFilesInWorkspace.ts deleted file mode 100644 index 05817ce..0000000 --- a/src/app/workespace/findPhpFilesInWorkspace.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Uri, workspace } from 'vscode'; -import { ConfigKeys } from '@infra/workspace/configTypes'; -import { getWorkspaceConfig } from '@infra/workspace/vscodeConfig'; - -const DEFAULT_DIRECTORIES = ['/vendor/', '/var/', '/cache/']; -const DEFAULT_EXTENSION_PHP = 'php'; - -let cachedPhpFiles: Uri[] | null = null; -let cacheTimestamp: number = 0; -const CACHE_DURATION = 30000; - -export async function findPhpFilesInWorkspace(): Promise { - const now = Date.now(); - - if (cachedPhpFiles && (now - cacheTimestamp) < CACHE_DURATION) { - return cachedPhpFiles; - } - - const extensions = getWorkspaceConfig({ - key: ConfigKeys.ADDITIONAL_EXTENSIONS, - defaultValue: [DEFAULT_EXTENSION_PHP], - }); - - const pattern = `**/*.{${[DEFAULT_EXTENSION_PHP, ...extensions].join(',')}}`; - const phpFiles: Uri[] = await workspace.findFiles(pattern); - - const ignoredDirectories = getWorkspaceConfig({ - key: ConfigKeys.IGNORED_DIRECTORIES, - defaultValue: DEFAULT_DIRECTORIES, - }); - - const filteredFiles = phpFiles.filter(file => ![ - ...DEFAULT_DIRECTORIES, - ...ignoredDirectories, - ].some(dir => file.fsPath.includes(dir))); - - cachedPhpFiles = filteredFiles; - cacheTimestamp = now; - - return filteredFiles; -} diff --git a/src/domain/namespace/NamespaceCreator.ts b/src/domain/namespace/NamespaceCreator.ts new file mode 100644 index 0000000..8fb807b --- /dev/null +++ b/src/domain/namespace/NamespaceCreator.ts @@ -0,0 +1,46 @@ +import { NamespaceAutoloadMapper } from '@infra/autoload/NamespaceAutoloadMapper'; +import { Uri } from 'vscode'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +interface Props { + uri: Uri +} + +export interface Namespace { + namespace?: string + className: string + fullNamespace: string +} + +export class NamespaceCreator { + public async execute({ uri }: Props): Promise { + const { autoload, autoloadDev } = await new NamespaceAutoloadMapper().execute({ + uri: uri.fsPath + }); + + const className = new WorkspacePathResolver().extractClassNameFromPath(uri.fsPath); + + for (const currentAutoload of [autoload, autoloadDev]) { + if (null === currentAutoload) { + continue; + } + + return this.create( + className, + currentAutoload + ); + } + + return this.create(className); + } + + private create(className: string, namespace?: string): Namespace { + return { + namespace, + className, + fullNamespace: namespace + ? `${namespace}\\${className}` + : className, + }; + } +} diff --git a/src/domain/namespace/UseStatementCreator.ts b/src/domain/namespace/UseStatementCreator.ts new file mode 100644 index 0000000..8b25a92 --- /dev/null +++ b/src/domain/namespace/UseStatementCreator.ts @@ -0,0 +1,37 @@ +import { NamespaceCreator } from './NamespaceCreator'; +import { Uri } from 'vscode'; + +interface MultipleProps { + classesUsed: string[], + directoryPath: string +} + +interface SingleProps { + fullNamespace: string +} + +export class UseStatementCreator { + public async multiple({ classesUsed, directoryPath }: MultipleProps): Promise { + const namespaceCreator = new NamespaceCreator(); + + const useStatements = await Promise.all( + classesUsed.map(async (className) => { + const { fullNamespace } = await namespaceCreator.execute({ + uri: Uri.file(`${directoryPath}/${className}.php`) + }); + + return this.single({ fullNamespace}); + }) + ); + + return useStatements.join(''); + } + + public single({ fullNamespace }: SingleProps): string { + if (fullNamespace.length > 0) { + return `\nuse ${fullNamespace};`; + } + + throw new Error('O parâmetro "fullNamespace" deve ser uma string válida.'); + } +} diff --git a/src/domain/namespace/UseStatementInjector.ts b/src/domain/namespace/UseStatementInjector.ts new file mode 100644 index 0000000..035aa50 --- /dev/null +++ b/src/domain/namespace/UseStatementInjector.ts @@ -0,0 +1,38 @@ +import { Range, TextDocument, Uri, workspace, WorkspaceEdit } from 'vscode'; + +interface Props { + document: TextDocument + workspaceEdit: WorkspaceEdit + uri: Uri + useNamespace: string + lastUseEndIndex: number + flush: boolean +} + +export class UseStatementInjector { + public async save({ + document, + workspaceEdit, + lastUseEndIndex, + uri, + useNamespace, + flush = false, + }: Props ) { + const endPosition = document.positionAt(lastUseEndIndex); + workspaceEdit.replace( + uri, + new Range(endPosition, endPosition), + useNamespace, + ); + + if (!flush) { + return; + } + + await this.flush(workspaceEdit); + } + + public async flush(edit: WorkspaceEdit): Promise { + await workspace.applyEdit(edit); + } +} diff --git a/src/domain/namespace/UseStatementLocator.ts b/src/domain/namespace/UseStatementLocator.ts new file mode 100644 index 0000000..a7a38a0 --- /dev/null +++ b/src/domain/namespace/UseStatementLocator.ts @@ -0,0 +1,39 @@ +import { TextDocument } from 'vscode'; + +interface Props { + document: TextDocument +} + +export class UseStatementLocator { + public execute({ document }: Props) { + const text = document.getText(); + const lastUseEndIndex = this.findLastUseEndIndex(text); + + if (lastUseEndIndex > 0) { + return lastUseEndIndex; + } + + return this.findNamespaceEndIndex(text); + } + + private findLastUseEndIndex(contentDocument: string): number { + const useMatches = [...contentDocument.matchAll(/^use\s+[^\n]+;/gm)]; + + const lastUseMatch = useMatches[useMatches.length - 1]; + + if (!lastUseMatch) { + return 0; + } + + return lastUseMatch.index + lastUseMatch[0].length; + } + + private findNamespaceEndIndex(contentDocument: string): number { + const match = contentDocument.match(/^\s*namespace\s+[\w\\]+;/m); + if (!match) { + return 0; + } + + return match.index! + match[0].length; + } +} diff --git a/src/domain/namespace/createNamespace.ts b/src/domain/namespace/createNamespace.ts deleted file mode 100644 index e1f5bb2..0000000 --- a/src/domain/namespace/createNamespace.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface Props { - namespace?: string - className: string -} - -export interface Namespace { - namespace?: string - className: string - fullNamespace: string -} - -export function createNamespace({ - namespace, - className, -}: Props): Namespace { - return { - namespace, - className, - fullNamespace: namespace ? `${namespace}\\${className}` : className, - }; -} diff --git a/src/domain/namespace/findLastUseEndIndex.ts b/src/domain/namespace/findLastUseEndIndex.ts deleted file mode 100644 index 2fcdd3e..0000000 --- a/src/domain/namespace/findLastUseEndIndex.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TextDocument } from 'vscode'; - -interface Props { - document: TextDocument -} - -const REGEX = /^use\s+[^\n]+;/gm; - -export function findLastUseEndIndex({ - document, -}: Props): number { - const text = document.getText(); - - const useMatches = [...text.matchAll(REGEX)]; - - const lastUseMatch = useMatches[useMatches.length - 1]; - - if (!lastUseMatch) { - return 0; - } - - return lastUseMatch.index + lastUseMatch[0].length; -} diff --git a/src/domain/namespace/findNamespaceEndIndex.ts b/src/domain/namespace/findNamespaceEndIndex.ts deleted file mode 100644 index c671a06..0000000 --- a/src/domain/namespace/findNamespaceEndIndex.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TextDocument } from 'vscode'; - -interface Props { - document: TextDocument -} - -export function findNamespaceEndIndex({ - document, -}: Props): number { - const text = document.getText(); - - const namespaceRegex = /^\s*namespace\s+[\w\\]+;/m; - const match = text.match(namespaceRegex); - - if (!match) { - return 0; - } - - return match.index! + match[0].length; -} diff --git a/src/domain/namespace/findUseInsertionIndex.ts b/src/domain/namespace/findUseInsertionIndex.ts deleted file mode 100644 index 3ca80b8..0000000 --- a/src/domain/namespace/findUseInsertionIndex.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { findLastUseEndIndex } from './findLastUseEndIndex'; -import { findNamespaceEndIndex } from './findNamespaceEndIndex'; -import { TextDocument } from 'vscode'; - -interface Props { - document: TextDocument -} - -export function findUseInsertionIndex({ - document, -}: Props): number { - const lastUseEndIndex = findLastUseEndIndex({ document }); - - if (lastUseEndIndex > 0) { - return lastUseEndIndex; - } - - return findNamespaceEndIndex({ document }); -} diff --git a/src/domain/namespace/generateNamespace.ts b/src/domain/namespace/generateNamespace.ts deleted file mode 100644 index eed4c20..0000000 --- a/src/domain/namespace/generateNamespace.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { createNamespace, Namespace } from './createNamespace'; -import { extractClassNameFromPath } from '@infra/utils/filePathUtils'; -import { mapAutoloadNamespaces } from '@infra/autoload/mapAutoloadNamespaces'; - -interface Props { - uri: string -} - -export async function generateNamespace({ - uri, -}: Props): Promise { - const { autoload, autoloadDev } = await mapAutoloadNamespaces({ - uri - }); - - const className = extractClassNameFromPath(uri); - - for (const currentAutoload of [autoload, autoloadDev]) { - if (null === currentAutoload) { - continue; - } - - return createNamespace({ - namespace: currentAutoload, - className - }); - } - - return createNamespace({ className }); -} diff --git a/src/domain/namespace/generateUseStatement.ts b/src/domain/namespace/generateUseStatement.ts deleted file mode 100644 index 370c7ef..0000000 --- a/src/domain/namespace/generateUseStatement.ts +++ /dev/null @@ -1,11 +0,0 @@ -interface Props { - fullNamespace: string -} - -export function generateUseStatement({ fullNamespace }: Props) { - if (!fullNamespace || typeof fullNamespace !== 'string') { - throw new Error('O parâmetro "fullNamespace" deve ser uma string válida.'); - } - - return `\nuse ${fullNamespace};`; -} diff --git a/src/domain/namespace/generateUseStatementsForClasses.ts b/src/domain/namespace/generateUseStatementsForClasses.ts deleted file mode 100644 index f33862f..0000000 --- a/src/domain/namespace/generateUseStatementsForClasses.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { generateNamespace } from './generateNamespace'; -import { generateUseStatement } from './generateUseStatement'; - -interface Props { - classesUsed: string[] - directoryPath: string -} - -export async function generateUseStatementsForClasses({ - classesUsed, - directoryPath, -}: Props): Promise { - const useStatements = await Promise.all( - classesUsed.map(async (className) => { - const uri = `${directoryPath}/${className}.php`; - - const { fullNamespace } = await generateNamespace({ uri }); - - return generateUseStatement({ fullNamespace }); - }) - ); - - return useStatements.join(''); -} diff --git a/src/domain/namespace/import/insertUseStatement.ts b/src/domain/namespace/import/insertUseStatement.ts deleted file mode 100644 index 825eb52..0000000 --- a/src/domain/namespace/import/insertUseStatement.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Range, TextDocument, Uri, workspace, WorkspaceEdit } from 'vscode'; - -interface Props { - document: TextDocument - workspaceEdit: WorkspaceEdit - uri: Uri - useNamespace: string - lastUseEndIndex: number - flush: boolean -} - -export async function insertUseStatement({ - document, - workspaceEdit, - lastUseEndIndex, - uri, - useNamespace, - flush = false, -}: Props) { - const endPosition = document.positionAt(lastUseEndIndex); - workspaceEdit.replace( - uri, - new Range(endPosition, endPosition), - useNamespace, - ); - - if (flush) { - await workspace.applyEdit(workspaceEdit); - } -} diff --git a/src/domain/workspace/ConfigurationLocator.ts b/src/domain/workspace/ConfigurationLocator.ts new file mode 100644 index 0000000..8dd87e5 --- /dev/null +++ b/src/domain/workspace/ConfigurationLocator.ts @@ -0,0 +1,25 @@ +import { workspace, WorkspaceConfiguration } from 'vscode'; + +export const ConfigKeys = { + AUTO_IMPORT_NAMESPACE: 'autoImportNamespace', + REMOVE_UNUSED_IMPORTS: 'removeUnusedImports', + IGNORED_DIRECTORIES: 'ignoredDirectories', + ADDITIONAL_EXTENSIONS: 'additionalExtensions', +} as const; + +export type Props = { + key: string, + defaultValue?: T +} + +export class ConfigurationLocator { + private config: WorkspaceConfiguration; + + constructor() { + this.config = workspace.getConfiguration('phpNamespaceRefactor'); + } + + public get({ key, defaultValue }: Props): T { + return this.config.get(key, defaultValue as T); + } +} diff --git a/src/domain/workspace/FeatureFlagManager.ts b/src/domain/workspace/FeatureFlagManager.ts new file mode 100644 index 0000000..f3ac516 --- /dev/null +++ b/src/domain/workspace/FeatureFlagManager.ts @@ -0,0 +1,18 @@ +import { workspace, WorkspaceConfiguration } from 'vscode'; + +export type Props = { + key: string, + defaultValue?: boolean +} + +export class FeatureFlagManager { + private config: WorkspaceConfiguration; + + constructor() { + this.config = workspace.getConfiguration('phpNamespaceRefactor'); + } + + public isActive({ key, defaultValue = true }: Props): boolean { + return this.config.get(key, defaultValue); + } +} diff --git a/src/domain/workspace/WorkspacePathResolver.ts b/src/domain/workspace/WorkspacePathResolver.ts new file mode 100644 index 0000000..cdf7d8a --- /dev/null +++ b/src/domain/workspace/WorkspacePathResolver.ts @@ -0,0 +1,26 @@ +import { basename, dirname } from 'path'; +import { workspace } from 'vscode'; + +type AbsolutePath = string | null | undefined + +export class WorkspacePathResolver { + public removeWorkspaceRoot(filePath: AbsolutePath) { + return filePath + ?.replace(this.getRootPath(), '') + .replace(/^\/|\\/g, '') || ''; + } + + public extractDirectoryFromPath(filePath: AbsolutePath) { + return dirname(filePath || ''); + } + + public extractClassNameFromPath(filePath: AbsolutePath) { + return basename(filePath || '', '.php') || ''; + } + + public getRootPath() { + return workspace.workspaceFolders + ? workspace.workspaceFolders[0].uri.fsPath + : ''; + } +} diff --git a/src/extension.ts b/src/extension.ts index ac57711..0d9635f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,19 +1,25 @@ import * as fs from 'fs'; -import { COMPOSER_FILE, PHP_EXTENSION, WORKSPACE_ROOT } from '@infra/utils/constants'; -import { ConfigKeys } from '@infra/workspace/configTypes'; -import { importMissingClasses } from '@app/namespace/update/import/importMissingClasses'; -import { isConfigEnabled } from '@infra/workspace/vscodeConfig'; -import { removeUnusedImports } from '@app/namespace/remove/removeUnusedImports'; -import { updateReferences } from '@app/namespace/update/updateReferences'; +import { COMPOSER_FILE, PHP_EXTENSION } from '@infra/utils/constants'; +import { ConfigKeys } from '@domain/workspace/ConfigurationLocator'; +import { FeatureFlagManager } from '@domain/workspace/FeatureFlagManager'; +import { ImportRemover } from '@app/services/remove/ImportRemover'; +import { MissingClassImporter } from '@app/services/MissingClassImporter'; +import { NamespaceBatchUpdater } from '@app/services/NamespaceBatchUpdater'; import { workspace } from 'vscode'; +import { WorkspacePathResolver } from './domain/workspace/WorkspacePathResolver'; export function activate() { - const files: string[] = fs.readdirSync(WORKSPACE_ROOT); + const files: string[] = fs.readdirSync(new WorkspacePathResolver().getRootPath()); if (!files.includes(COMPOSER_FILE)) { return; } workspace.onDidRenameFiles(async (event) => { + const importRemover = new ImportRemover(); + const missingClassImporter = new MissingClassImporter(); + const namespaceBatchUpdater = new NamespaceBatchUpdater(); + const featureFlagManager = new FeatureFlagManager(); + for (const { oldUri, newUri } of event.files) { if (!oldUri.fsPath.endsWith(PHP_EXTENSION) || !newUri.fsPath.endsWith(PHP_EXTENSION)) { @@ -21,16 +27,16 @@ export function activate() { } try { - await updateReferences({ newUri, oldUri }); + await namespaceBatchUpdater.execute({ newUri, oldUri }); - if (isConfigEnabled({ key: ConfigKeys.AUTO_IMPORT_NAMESPACE })) { - await importMissingClasses({ - oldFileName: oldUri.fsPath, + if (featureFlagManager.isActive({ key: ConfigKeys.AUTO_IMPORT_NAMESPACE })) { + await missingClassImporter.execute({ + oldUri, newUri, }); } - await removeUnusedImports({ uri: newUri }); + await importRemover.execute({ uri: newUri }); } catch (error) { // eslint-disable-next-line no-undef console.error('Error processing file rename:', error); diff --git a/src/infra/autoload/AutoloadPathResolver.ts b/src/infra/autoload/AutoloadPathResolver.ts new file mode 100644 index 0000000..61e44bf --- /dev/null +++ b/src/infra/autoload/AutoloadPathResolver.ts @@ -0,0 +1,31 @@ +type AutoloadType = { + [key: string]: string +} + +interface Props { + autoload: AutoloadType, + workspaceRoot: string, +} + +export class AutoloadPathResolver { + public async execute({ autoload, workspaceRoot }: Props) { + for (const prefix in autoload) { + const src = autoload[prefix].replace(/\\/g, '/'); + + if (!workspaceRoot.startsWith(src)) { + continue; + } + + const prefixBase = prefix.split('\\":').at(0)?.replace(/\\+$/, '') || ''; + + const srcReplace = src.endsWith('/') ? prefixBase + '\\' : prefixBase; + + return workspaceRoot + .replace(src, srcReplace) + .replace(/\//g, '\\') + .replace(/\\[^\\]+$/, ''); + } + + return ''; + } +} diff --git a/src/infra/autoload/ComposerAutoloadManager.ts b/src/infra/autoload/ComposerAutoloadManager.ts new file mode 100644 index 0000000..2fd942f --- /dev/null +++ b/src/infra/autoload/ComposerAutoloadManager.ts @@ -0,0 +1,56 @@ +import { COMPOSER_FILE } from '@infra/utils/constants'; +import { promises as fs } from 'fs'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +interface ComposerAutoload { + autoload: Record; + autoloadDev: Record; +} + +const DEFAULT = { + autoload: {}, + autoloadDev: {} +}; + +let composerCache: ComposerAutoload | null = null; +let cacheWorkspaceRoot: string | null = null; +let cacheModifiedTime: number | null = null; + +export class ComposerAutoloadManager { + public async execute() { + const workspaceRoot = new WorkspacePathResolver().getRootPath(); + + if (!workspaceRoot) { + return DEFAULT; + } + + const composerPath = `${workspaceRoot}/${COMPOSER_FILE}`; + + try { + const stats = await fs.stat(composerPath); + const currentModifiedTime = stats.mtimeMs; + + if (composerCache && + cacheWorkspaceRoot === workspaceRoot && + cacheModifiedTime === currentModifiedTime) { + return composerCache; + } + + const composerJson = await fs.readFile(composerPath, 'utf8'); + const composerConfig = JSON.parse(composerJson); + + const result = { + autoload: composerConfig.autoload?.['psr-4'] || {}, + autoloadDev: composerConfig['autoload-dev']?.['psr-4'] || {}, + }; + + composerCache = result; + cacheWorkspaceRoot = workspaceRoot; + cacheModifiedTime = currentModifiedTime; + + return result; + } catch (error) { + return DEFAULT; + } + } +} diff --git a/src/infra/autoload/NamespaceAutoloadMapper.ts b/src/infra/autoload/NamespaceAutoloadMapper.ts new file mode 100644 index 0000000..9f8ac77 --- /dev/null +++ b/src/infra/autoload/NamespaceAutoloadMapper.ts @@ -0,0 +1,35 @@ +import { AutoloadPathResolver } from './AutoloadPathResolver'; +import { ComposerAutoloadManager } from './ComposerAutoloadManager'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +interface Props { + uri: string +} + +export class NamespaceAutoloadMapper { + public async execute({ uri }: Props) { + const { autoload, autoloadDev } = await new ComposerAutoloadManager().execute(); + + if (!autoload && !autoloadDev) { + return { + autoload: null, + autoloadDev: null, + }; + } + + const newDir = new WorkspacePathResolver().removeWorkspaceRoot(uri); + + const autoloadPathResolver = new AutoloadPathResolver(); + + return { + autoload: await autoloadPathResolver.execute({ + autoload, + workspaceRoot: newDir, + }), + autoloadDev: await autoloadPathResolver.execute({ + autoload: autoloadDev, + workspaceRoot: newDir, + }) + }; + } +} diff --git a/src/infra/autoload/fetchComposerAutoload.ts b/src/infra/autoload/fetchComposerAutoload.ts deleted file mode 100644 index 9241764..0000000 --- a/src/infra/autoload/fetchComposerAutoload.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { COMPOSER_FILE } from '@infra/utils/constants'; -import { promises as fs } from 'fs'; - -interface Props { - workspaceRoot?: string -} - -interface ComposerAutoload { - autoload: Record; - autoloadDev: Record; -} - -const DEFAULT = { - autoload: {}, - autoloadDev: {} -}; - -let composerCache: ComposerAutoload | null = null; -let cacheWorkspaceRoot: string | null = null; -let cacheModifiedTime: number | null = null; - -export async function fetchComposerAutoload({ - workspaceRoot, -}: Props): Promise { - if (!workspaceRoot) { - return DEFAULT; - } - - const composerPath = `${workspaceRoot}/${COMPOSER_FILE}`; - - try { - const stats = await fs.stat(composerPath); - const currentModifiedTime = stats.mtimeMs; - - if (composerCache && - cacheWorkspaceRoot === workspaceRoot && - cacheModifiedTime === currentModifiedTime) { - return composerCache; - } - - const composerJson = await fs.readFile(composerPath, 'utf8'); - const composerConfig = JSON.parse(composerJson); - - const result = { - autoload: composerConfig.autoload?.['psr-4'] || {}, - autoloadDev: composerConfig['autoload-dev']?.['psr-4'] || {}, - }; - - composerCache = result; - cacheWorkspaceRoot = workspaceRoot; - cacheModifiedTime = currentModifiedTime; - - return result; - } catch (error) { - return DEFAULT; - } -} diff --git a/src/infra/autoload/mapAutoloadNamespaces.ts b/src/infra/autoload/mapAutoloadNamespaces.ts deleted file mode 100644 index f3ef023..0000000 --- a/src/infra/autoload/mapAutoloadNamespaces.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { fetchComposerAutoload } from './fetchComposerAutoload'; -import { removeWorkspaceRoot } from '@infra/utils/filePathUtils'; -import { resolvePathFromPrefix } from './resolvePathFromPrefix'; -import { WORKSPACE_ROOT } from '@infra/utils/constants'; - -interface Props { - uri: string -} - -export async function mapAutoloadNamespaces({ - uri, -}: Props) { - const { autoload, autoloadDev } = await fetchComposerAutoload({ - workspaceRoot: WORKSPACE_ROOT, - }); - - if (!autoload && !autoloadDev) { - return { - autoload: null, - autoloadDev: null, - }; - } - - const newDir = removeWorkspaceRoot(uri); - - return { - autoload: resolvePathFromPrefix({ - autoload, - workspaceRoot: newDir, - }), - autoloadDev: resolvePathFromPrefix({ - autoload: autoloadDev, - workspaceRoot: newDir, - }) - }; -} - diff --git a/src/infra/autoload/resolvePathFromPrefix.ts b/src/infra/autoload/resolvePathFromPrefix.ts deleted file mode 100644 index 5d89363..0000000 --- a/src/infra/autoload/resolvePathFromPrefix.ts +++ /dev/null @@ -1,37 +0,0 @@ -type AutoloadType = { - [key: string]: string -} - -interface Props { - autoload: AutoloadType, - workspaceRoot: string, -} - -const NAMESPACE_DIVIDER = '\\":'; - -const REGEX_FORWARD_SLASH_PATTERN = /\//g; -const REGEX_FINAL_BACKSLASH_SEGMENT = /\\[^\\]+$/; - -export function resolvePathFromPrefix({ - autoload, - workspaceRoot, -}: Props) { - for (const prefix in autoload) { - const src = autoload[prefix].replace(/\\/g, '/'); - - if (!workspaceRoot.startsWith(src)) { - continue; - } - - const prefixBase = prefix.split(NAMESPACE_DIVIDER).at(0)?.replace(/\\+$/, '') || ''; - - const srcReplace = src.endsWith('/') ? prefixBase + '\\' : prefixBase; - - return workspaceRoot - .replace(src, srcReplace) - .replace(REGEX_FORWARD_SLASH_PATTERN, '\\') - .replace(REGEX_FINAL_BACKSLASH_SEGMENT, ''); - } - - return ''; -} diff --git a/src/infra/utils/constants.ts b/src/infra/utils/constants.ts index 9bf2e20..b6b0f20 100644 --- a/src/infra/utils/constants.ts +++ b/src/infra/utils/constants.ts @@ -1,9 +1,3 @@ -import { workspace } from 'vscode'; - export const COMPOSER_FILE = 'composer.json'; -export const WORKSPACE_ROOT = workspace.workspaceFolders - ? workspace.workspaceFolders[0].uri.fsPath - : ''; - export const PHP_EXTENSION = '.php'; diff --git a/src/infra/utils/filePathUtils.ts b/src/infra/utils/filePathUtils.ts deleted file mode 100644 index 5a7c230..0000000 --- a/src/infra/utils/filePathUtils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { basename, dirname } from 'path'; -import { WORKSPACE_ROOT } from './constants'; - -type AbsolutePath = string | null | undefined - -export const removeWorkspaceRoot = (filePath: AbsolutePath) => - filePath - ?.replace(WORKSPACE_ROOT, '') - .replace(/^\/|\\/g, '') || ''; - -export const extractDirectoryFromPath = (filePath: AbsolutePath) => - dirname(filePath || ''); - -export const extractClassNameFromPath = (filePath: AbsolutePath) => - basename(filePath || '', '.php') || ''; diff --git a/src/infra/workspace/configTypes.ts b/src/infra/workspace/configTypes.ts deleted file mode 100644 index 50a6ebf..0000000 --- a/src/infra/workspace/configTypes.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const ConfigKeys = { - AUTO_IMPORT_NAMESPACE: 'autoImportNamespace', - REMOVE_UNUSED_IMPORTS: 'removeUnusedImports', - IGNORED_DIRECTORIES: 'ignoredDirectories', - ADDITIONAL_EXTENSIONS: 'additionalExtensions', -} as const; - -export type IsConfigEnabledProps = { - key: string, - defaultValue?: boolean -} - -export type GetWorkspaceConfigProps = { - key: string, - defaultValue?: T -} diff --git a/src/infra/workspace/vscodeConfig.ts b/src/infra/workspace/vscodeConfig.ts deleted file mode 100644 index 58bf86a..0000000 --- a/src/infra/workspace/vscodeConfig.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { GetWorkspaceConfigProps, IsConfigEnabledProps } from './configTypes'; -import { workspace } from 'vscode'; - -const userConfig = workspace.getConfiguration('phpNamespaceRefactor'); - -export const isConfigEnabled = ({ - key, - defaultValue = true, -}: IsConfigEnabledProps): boolean => { - return userConfig.get(key, defaultValue); -}; - -export const getWorkspaceConfig = ({ - key, - defaultValue, -}: GetWorkspaceConfigProps) => { - return userConfig.get(key, defaultValue as T); -}; From f26727e1ecc2a77029eb5260fa3b94b46b57202f Mon Sep 17 00:00:00 2001 From: Rejman Date: Mon, 13 Oct 2025 19:13:41 -0300 Subject: [PATCH 2/4] feat: dependecy injection --- package-lock.json | 28 +++++++++++++++++++ package.json | 4 +++ src/app/services/MissingClassImporter.ts | 27 ++++++++++-------- src/app/services/NamespaceBatchUpdater.ts | 17 +++++++---- src/app/services/TextDocumentOpener.ts | 2 ++ .../services/import/UnusedImportDetector.ts | 3 ++ src/app/services/remove/ImportRemover.ts | 26 +++++++++-------- .../update/MovedFileNamespaceUpdater.ts | 8 +++++- .../update/MultiFileReferenceUpdater.ts | 28 +++++++++++-------- .../services/workspace/WorkspaceFileFinder.ts | 20 +++++++------ src/domain/namespace/NamespaceCreator.ts | 12 ++++++-- src/domain/namespace/UseStatementCreator.ts | 10 +++++-- src/domain/namespace/UseStatementInjector.ts | 2 ++ src/domain/namespace/UseStatementLocator.ts | 2 ++ src/domain/workspace/ConfigurationLocator.ts | 2 ++ src/domain/workspace/FeatureFlagManager.ts | 2 ++ src/domain/workspace/WorkspacePathResolver.ts | 2 ++ src/extension.ts | 10 ++++--- src/infra/autoload/AutoloadPathResolver.ts | 3 ++ src/infra/autoload/ComposerAutoloadManager.ts | 8 +++++- src/infra/autoload/NamespaceAutoloadMapper.ts | 18 ++++++++---- tsconfig.json | 16 ++++++++--- 22 files changed, 183 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4bce56..7ad54e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "php-namespace-refactor", "version": "1.3.2", + "dependencies": { + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0" + }, "devDependencies": { "@rocketseat/eslint-config": "^2.2.2", "@types/mocha": "^10.0.10", @@ -7998,6 +8002,12 @@ "node": ">=8.10.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9198,6 +9208,24 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index 5f7d71d..bb996b7 100644 --- a/package.json +++ b/package.json @@ -120,5 +120,9 @@ "eslint": "^9.16.0", "npm-run-all": "^4.1.5", "typescript": "^5.7.2" + }, + "dependencies": { + "reflect-metadata": "^0.2.2", + "tsyringe": "^4.10.0" } } diff --git a/src/app/services/MissingClassImporter.ts b/src/app/services/MissingClassImporter.ts index 2569f51..86016e9 100644 --- a/src/app/services/MissingClassImporter.ts +++ b/src/app/services/MissingClassImporter.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { Uri, WorkspaceEdit } from 'vscode'; import { promises as fs } from 'fs'; import { PHP_EXTENSION } from '@infra/utils/constants'; @@ -13,11 +14,16 @@ interface Props { newUri: Uri } +@injectable() export class MissingClassImporter { - private workspacePathResolver: WorkspacePathResolver; - - constructor() { - this.workspacePathResolver = new WorkspacePathResolver(); + constructor( + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + @inject(TextDocumentOpener) private textDocumentOpener: TextDocumentOpener, + @inject(UseStatementCreator) private useStatementCreator: UseStatementCreator, + @inject(UnusedImportDetector) private unusedImportDetector: UnusedImportDetector, + @inject(UseStatementLocator) private useStatementLocator: UseStatementLocator, + @inject(UseStatementInjector) private useStatementInjector: UseStatementInjector, + ) { } public async execute({ oldUri, newUri }: Props) { @@ -29,10 +35,10 @@ export class MissingClassImporter { } try { - const { document, text } = await new TextDocumentOpener().execute({ uri: newUri }); + const { document, text } = await this.textDocumentOpener.execute({ uri: newUri }); - const imports = await new UseStatementCreator().multiple({ - classesUsed: new UnusedImportDetector().execute({ + const imports = await this.useStatementCreator.multiple({ + classesUsed: this.unusedImportDetector.execute({ contentDocument: text, classes, }), @@ -43,16 +49,15 @@ export class MissingClassImporter { return; } - const insertionIndex = new UseStatementLocator().execute({ document }); + const insertionIndex = this.useStatementLocator.execute({ document }); if (insertionIndex === 0) { return; } const edit = new WorkspaceEdit(); - const useStatementInjector = new UseStatementInjector(); for (const use of imports) { - await useStatementInjector.save({ + await this.useStatementInjector.save({ document, workspaceEdit: edit, uri: newUri, @@ -63,7 +68,7 @@ export class MissingClassImporter { } if (imports.length > 0) { - await useStatementInjector.flush(edit); + await this.useStatementInjector.flush(edit); } } catch (_) { return; diff --git a/src/app/services/NamespaceBatchUpdater.ts b/src/app/services/NamespaceBatchUpdater.ts index e0168bd..ca6b8f6 100644 --- a/src/app/services/NamespaceBatchUpdater.ts +++ b/src/app/services/NamespaceBatchUpdater.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { MovedFileNamespaceUpdater } from './update/MovedFileNamespaceUpdater'; import { MultiFileReferenceUpdater } from './update/MultiFileReferenceUpdater'; import { NamespaceCreator } from '@domain/namespace/NamespaceCreator'; @@ -8,13 +9,19 @@ interface Props { oldUri: Uri, } +@injectable() export class NamespaceBatchUpdater { + constructor( + @inject(MovedFileNamespaceUpdater) private movedFileNamespaceUpdater: MovedFileNamespaceUpdater, + @inject(MultiFileReferenceUpdater) private multiFileReferenceUpdater: MultiFileReferenceUpdater, + @inject(NamespaceCreator) private namespaceCreator: NamespaceCreator, + ) {} + public async execute({ newUri, oldUri }: Props) { - const namespaceCreator = new NamespaceCreator(); const { namespace: newNamespace, fullNamespace: useNewNamespace, - } = await namespaceCreator.execute({ + } = await this.namespaceCreator.execute({ uri: newUri, }); @@ -22,11 +29,11 @@ export class NamespaceBatchUpdater { return; } - const { fullNamespace: useOldNamespace } = await namespaceCreator.execute({ + const { fullNamespace: useOldNamespace } = await this.namespaceCreator.execute({ uri: oldUri, }); - const updated = await new MovedFileNamespaceUpdater().execute({ + const updated = await this.movedFileNamespaceUpdater.execute({ newNamespace, newUri, }); @@ -35,7 +42,7 @@ export class NamespaceBatchUpdater { return; } - await new MultiFileReferenceUpdater().execute({ + await this.multiFileReferenceUpdater.execute({ useOldNamespace, useNewNamespace, newUri, diff --git a/src/app/services/TextDocumentOpener.ts b/src/app/services/TextDocumentOpener.ts index 971e555..29e74e2 100644 --- a/src/app/services/TextDocumentOpener.ts +++ b/src/app/services/TextDocumentOpener.ts @@ -1,4 +1,5 @@ import { TextDocument, Uri, workspace } from 'vscode'; +import { injectable } from "tsyringe"; interface Props { uri: Uri @@ -9,6 +10,7 @@ type OpenTextDocument = { text: string } +@injectable() export class TextDocumentOpener { public async execute({ uri }: Props): Promise { const document = await workspace.openTextDocument(uri.fsPath); diff --git a/src/app/services/import/UnusedImportDetector.ts b/src/app/services/import/UnusedImportDetector.ts index e40fa1d..22ee42b 100644 --- a/src/app/services/import/UnusedImportDetector.ts +++ b/src/app/services/import/UnusedImportDetector.ts @@ -1,8 +1,11 @@ +import { injectable } from "tsyringe"; + interface Props { contentDocument: string, classes: string[], } +@injectable() export class UnusedImportDetector { public execute({ contentDocument, classes }: Props): string[] { const classesUsed: string[] = []; diff --git a/src/app/services/remove/ImportRemover.ts b/src/app/services/remove/ImportRemover.ts index 85607f2..ab64011 100644 --- a/src/app/services/remove/ImportRemover.ts +++ b/src/app/services/remove/ImportRemover.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { Range, RelativePattern, TextDocument, Uri, workspace, WorkspaceEdit } from 'vscode'; import { ConfigKeys } from '@domain/workspace/ConfigurationLocator'; import { FeatureFlagManager } from '@domain/workspace/FeatureFlagManager'; @@ -14,23 +15,28 @@ interface RemoveImportsProps { fileNames: string[] } +@injectable() export class ImportRemover { - public async execute({ uri }: Props) { - const featureFlagManager = new FeatureFlagManager(); + constructor( + @inject(FeatureFlagManager) private featureFlagManager: FeatureFlagManager, + @inject(NamespaceCreator) private namespaceCreator: NamespaceCreator, + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + @inject(TextDocumentOpener) private textDocumentOpener: TextDocumentOpener, + ) {} - if (!featureFlagManager.isActive({ key: ConfigKeys.REMOVE_UNUSED_IMPORTS })) { + public async execute({ uri }: Props) { + if (!this.featureFlagManager.isActive({ key: ConfigKeys.REMOVE_UNUSED_IMPORTS })) { return; } - const { className } = await new NamespaceCreator().execute({ uri }); + const { className } = await this.namespaceCreator.execute({ uri }); - const workspacePathService = new WorkspacePathResolver(); - const directoryPath = workspacePathService.extractDirectoryFromPath(uri.fsPath); + const directoryPath = this.workspacePathResolver.extractDirectoryFromPath(uri.fsPath); const pattern = new RelativePattern(Uri.parse(`file://${directoryPath}`), '*.php'); const phpFiles = await workspace.findFiles(pattern); - const fileNames = phpFiles.map(uri => workspacePathService.extractClassNameFromPath(uri.fsPath)) + const fileNames = phpFiles.map(uri => this.workspacePathResolver.extractClassNameFromPath(uri.fsPath)) .filter(Boolean) .filter(name => name !== className); @@ -38,10 +44,8 @@ export class ImportRemover { return; } - const textDocumentOpener = new TextDocumentOpener(); - try { - const { document } = await textDocumentOpener.execute({ uri }); + const { document } = await this.textDocumentOpener.execute({ uri }); await this.removeImports({ document, fileNames, @@ -54,7 +58,7 @@ export class ImportRemover { await Promise.all(otherFiles.map(async (file) => { try { - const { document } = await textDocumentOpener.execute({ uri: file }); + const { document } = await this.textDocumentOpener.execute({ uri: file }); await this.removeImports({ document, fileNames: [className], diff --git a/src/app/services/update/MovedFileNamespaceUpdater.ts b/src/app/services/update/MovedFileNamespaceUpdater.ts index d56d8ee..0442975 100644 --- a/src/app/services/update/MovedFileNamespaceUpdater.ts +++ b/src/app/services/update/MovedFileNamespaceUpdater.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { Range, Uri, workspace, WorkspaceEdit } from 'vscode'; import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; @@ -6,9 +7,14 @@ interface Props { newUri: Uri, } +@injectable() export class MovedFileNamespaceUpdater { + constructor( + @inject(TextDocumentOpener) private textDocumentOpener: TextDocumentOpener + ) {} + public async execute({ newNamespace, newUri }: Props) { - const { document, text } = await new TextDocumentOpener().execute({ uri: newUri }); + const { document, text } = await this.textDocumentOpener.execute({ uri: newUri }); const namespaceRegex = /^\s*namespace\s+[\w\\]+;/m; const match = text.match(namespaceRegex); diff --git a/src/app/services/update/MultiFileReferenceUpdater.ts b/src/app/services/update/MultiFileReferenceUpdater.ts index 537ed86..723eb1b 100644 --- a/src/app/services/update/MultiFileReferenceUpdater.ts +++ b/src/app/services/update/MultiFileReferenceUpdater.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { Uri, workspace, WorkspaceEdit } from 'vscode'; import { ImportRemover } from '@app/services/remove/ImportRemover'; import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; @@ -14,12 +15,17 @@ interface Props { oldUri: Uri } +@injectable() export class MultiFileReferenceUpdater { - private workspacePathResolver: WorkspacePathResolver; - - constructor() { - this.workspacePathResolver = new WorkspacePathResolver(); - } + constructor( + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + @inject(ImportRemover) private importRemover: ImportRemover, + @inject(UseStatementCreator) private useStatementCreator: UseStatementCreator, + @inject(WorkspaceFileFinder) private workspaceFileFinder: WorkspaceFileFinder, + @inject(TextDocumentOpener) private textDocumentOpener: TextDocumentOpener, + @inject(UseStatementLocator) private useStatementLocator: UseStatementLocator, + @inject(UseStatementInjector) private useStatementInjector: UseStatementInjector, + ) {} public async execute({ useOldNamespace, @@ -30,11 +36,11 @@ export class MultiFileReferenceUpdater { const directoryPath = this.workspacePathResolver.extractDirectoryFromPath(oldUri.fsPath); const className = this.workspacePathResolver.extractClassNameFromPath(oldUri.fsPath); - const useImport = new UseStatementCreator().single({ fullNamespace: useNewNamespace }); + const useImport = this.useStatementCreator.single({ fullNamespace: useNewNamespace }); const ignoreFile = newUri.fsPath; - const files = await new WorkspaceFileFinder().execute(); + const files = await this.workspaceFileFinder.execute(); const filesToProcess = files.filter(file => ignoreFile !== file.fsPath); @@ -72,7 +78,7 @@ export class MultiFileReferenceUpdater { } })); - await new ImportRemover().execute({ uri: newUri }); + await this.importRemover.execute({ uri: newUri }); } private async updateInFile( @@ -87,20 +93,20 @@ export class MultiFileReferenceUpdater { } try { - const { document, text } = await new TextDocumentOpener().execute({ uri: file }); + const { document, text } = await this.textDocumentOpener.execute({ uri: file }); if (!text.includes(className)) { return; } - const insertionIndex = new UseStatementLocator().execute({ document }); + const insertionIndex = this.useStatementLocator.execute({ document }); if (insertionIndex === 0) { return; } const edit = new WorkspaceEdit(); - await new UseStatementInjector().save({ + await this.useStatementInjector.save({ document, workspaceEdit: edit, uri: file, diff --git a/src/app/services/workspace/WorkspaceFileFinder.ts b/src/app/services/workspace/WorkspaceFileFinder.ts index 4e2f75f..25bdfe3 100644 --- a/src/app/services/workspace/WorkspaceFileFinder.ts +++ b/src/app/services/workspace/WorkspaceFileFinder.ts @@ -1,27 +1,30 @@ import { ConfigKeys, ConfigurationLocator } from '@domain/workspace/ConfigurationLocator'; +import { inject, injectable } from "tsyringe"; import { Uri, workspace } from 'vscode'; const DEFAULT_DIRECTORIES = ['/vendor/', '/var/', '/cache/']; const DEFAULT_EXTENSION_PHP = 'php'; +const SECONDS_IN_AN_HOUR = 60 * 60; + +@injectable() export class WorkspaceFileFinder { private cachedFiles: Uri[] | null = null; private cacheTimestamp: number = 0; - private readonly cacheDuration: number; + private cacheDuration: number = 0; - constructor(cacheDuration: number = 4) { - this.cacheDuration = 60 * 60 * cacheDuration; + constructor( + @inject(ConfigurationLocator) private configurationLocator: ConfigurationLocator, + ) { } - async execute(): Promise { + async execute(duration: number = 4): Promise { const now = Date.now(); if (this.cachedFiles && (now - this.cacheTimestamp) < this.cacheDuration) { return this.cachedFiles; } - const configurationLocator = new ConfigurationLocator(); - - const extensions = configurationLocator.get({ + const extensions = this.configurationLocator.get({ key: ConfigKeys.ADDITIONAL_EXTENSIONS, defaultValue: [DEFAULT_EXTENSION_PHP], }); @@ -29,7 +32,7 @@ export class WorkspaceFileFinder { const pattern = `**/*.{${[DEFAULT_EXTENSION_PHP, ...extensions].join(',')}}`; const files = await workspace.findFiles(pattern); - const ignoredDirectories = configurationLocator.get({ + const ignoredDirectories = this.configurationLocator.get({ key: ConfigKeys.IGNORED_DIRECTORIES, defaultValue: DEFAULT_DIRECTORIES, }); @@ -41,6 +44,7 @@ export class WorkspaceFileFinder { this.cachedFiles = filteredFiles; this.cacheTimestamp = now; + this.cacheDuration = SECONDS_IN_AN_HOUR * duration; return filteredFiles; } diff --git a/src/domain/namespace/NamespaceCreator.ts b/src/domain/namespace/NamespaceCreator.ts index 8fb807b..cb18c17 100644 --- a/src/domain/namespace/NamespaceCreator.ts +++ b/src/domain/namespace/NamespaceCreator.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { NamespaceAutoloadMapper } from '@infra/autoload/NamespaceAutoloadMapper'; import { Uri } from 'vscode'; import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; @@ -12,13 +13,20 @@ export interface Namespace { fullNamespace: string } +@injectable() export class NamespaceCreator { + constructor( + @inject(NamespaceAutoloadMapper) private namespaceAutoloadMapper: NamespaceAutoloadMapper, + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + ) { + } + public async execute({ uri }: Props): Promise { - const { autoload, autoloadDev } = await new NamespaceAutoloadMapper().execute({ + const { autoload, autoloadDev } = await this.namespaceAutoloadMapper.execute({ uri: uri.fsPath }); - const className = new WorkspacePathResolver().extractClassNameFromPath(uri.fsPath); + const className = this.workspacePathResolver.extractClassNameFromPath(uri.fsPath); for (const currentAutoload of [autoload, autoloadDev]) { if (null === currentAutoload) { diff --git a/src/domain/namespace/UseStatementCreator.ts b/src/domain/namespace/UseStatementCreator.ts index 8b25a92..b1b3026 100644 --- a/src/domain/namespace/UseStatementCreator.ts +++ b/src/domain/namespace/UseStatementCreator.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { NamespaceCreator } from './NamespaceCreator'; import { Uri } from 'vscode'; @@ -10,13 +11,16 @@ interface SingleProps { fullNamespace: string } +@injectable() export class UseStatementCreator { - public async multiple({ classesUsed, directoryPath }: MultipleProps): Promise { - const namespaceCreator = new NamespaceCreator(); + constructor ( + @inject(NamespaceCreator) private namespaceCreator: NamespaceCreator + ) {} + public async multiple({ classesUsed, directoryPath }: MultipleProps): Promise { const useStatements = await Promise.all( classesUsed.map(async (className) => { - const { fullNamespace } = await namespaceCreator.execute({ + const { fullNamespace } = await this.namespaceCreator.execute({ uri: Uri.file(`${directoryPath}/${className}.php`) }); diff --git a/src/domain/namespace/UseStatementInjector.ts b/src/domain/namespace/UseStatementInjector.ts index 035aa50..ac40d90 100644 --- a/src/domain/namespace/UseStatementInjector.ts +++ b/src/domain/namespace/UseStatementInjector.ts @@ -1,4 +1,5 @@ import { Range, TextDocument, Uri, workspace, WorkspaceEdit } from 'vscode'; +import { injectable } from "tsyringe"; interface Props { document: TextDocument @@ -9,6 +10,7 @@ interface Props { flush: boolean } +@injectable() export class UseStatementInjector { public async save({ document, diff --git a/src/domain/namespace/UseStatementLocator.ts b/src/domain/namespace/UseStatementLocator.ts index a7a38a0..8e8d222 100644 --- a/src/domain/namespace/UseStatementLocator.ts +++ b/src/domain/namespace/UseStatementLocator.ts @@ -1,9 +1,11 @@ +import { injectable } from "tsyringe"; import { TextDocument } from 'vscode'; interface Props { document: TextDocument } +@injectable() export class UseStatementLocator { public execute({ document }: Props) { const text = document.getText(); diff --git a/src/domain/workspace/ConfigurationLocator.ts b/src/domain/workspace/ConfigurationLocator.ts index 8dd87e5..0ac011c 100644 --- a/src/domain/workspace/ConfigurationLocator.ts +++ b/src/domain/workspace/ConfigurationLocator.ts @@ -1,4 +1,5 @@ import { workspace, WorkspaceConfiguration } from 'vscode'; +import { injectable } from "tsyringe"; export const ConfigKeys = { AUTO_IMPORT_NAMESPACE: 'autoImportNamespace', @@ -12,6 +13,7 @@ export type Props = { defaultValue?: T } +@injectable() export class ConfigurationLocator { private config: WorkspaceConfiguration; diff --git a/src/domain/workspace/FeatureFlagManager.ts b/src/domain/workspace/FeatureFlagManager.ts index f3ac516..71fe770 100644 --- a/src/domain/workspace/FeatureFlagManager.ts +++ b/src/domain/workspace/FeatureFlagManager.ts @@ -1,10 +1,12 @@ import { workspace, WorkspaceConfiguration } from 'vscode'; +import { injectable } from "tsyringe"; export type Props = { key: string, defaultValue?: boolean } +@injectable() export class FeatureFlagManager { private config: WorkspaceConfiguration; diff --git a/src/domain/workspace/WorkspacePathResolver.ts b/src/domain/workspace/WorkspacePathResolver.ts index cdf7d8a..2f2f014 100644 --- a/src/domain/workspace/WorkspacePathResolver.ts +++ b/src/domain/workspace/WorkspacePathResolver.ts @@ -1,8 +1,10 @@ import { basename, dirname } from 'path'; +import { injectable } from "tsyringe"; import { workspace } from 'vscode'; type AbsolutePath = string | null | undefined +@injectable() export class WorkspacePathResolver { public removeWorkspaceRoot(filePath: AbsolutePath) { return filePath diff --git a/src/extension.ts b/src/extension.ts index 0d9635f..dd0ee77 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,8 @@ +import "reflect-metadata"; import * as fs from 'fs'; import { COMPOSER_FILE, PHP_EXTENSION } from '@infra/utils/constants'; import { ConfigKeys } from '@domain/workspace/ConfigurationLocator'; +import { container } from "tsyringe"; import { FeatureFlagManager } from '@domain/workspace/FeatureFlagManager'; import { ImportRemover } from '@app/services/remove/ImportRemover'; import { MissingClassImporter } from '@app/services/MissingClassImporter'; @@ -15,10 +17,10 @@ export function activate() { } workspace.onDidRenameFiles(async (event) => { - const importRemover = new ImportRemover(); - const missingClassImporter = new MissingClassImporter(); - const namespaceBatchUpdater = new NamespaceBatchUpdater(); - const featureFlagManager = new FeatureFlagManager(); + const importRemover = container.resolve(ImportRemover); + const missingClassImporter = container.resolve(MissingClassImporter); + const namespaceBatchUpdater = container.resolve(NamespaceBatchUpdater); + const featureFlagManager = container.resolve(FeatureFlagManager); for (const { oldUri, newUri } of event.files) { if (!oldUri.fsPath.endsWith(PHP_EXTENSION) diff --git a/src/infra/autoload/AutoloadPathResolver.ts b/src/infra/autoload/AutoloadPathResolver.ts index 61e44bf..fb4d060 100644 --- a/src/infra/autoload/AutoloadPathResolver.ts +++ b/src/infra/autoload/AutoloadPathResolver.ts @@ -1,3 +1,5 @@ +import { injectable } from "tsyringe"; + type AutoloadType = { [key: string]: string } @@ -7,6 +9,7 @@ interface Props { workspaceRoot: string, } +@injectable() export class AutoloadPathResolver { public async execute({ autoload, workspaceRoot }: Props) { for (const prefix in autoload) { diff --git a/src/infra/autoload/ComposerAutoloadManager.ts b/src/infra/autoload/ComposerAutoloadManager.ts index 2fd942f..791f9f7 100644 --- a/src/infra/autoload/ComposerAutoloadManager.ts +++ b/src/infra/autoload/ComposerAutoloadManager.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { COMPOSER_FILE } from '@infra/utils/constants'; import { promises as fs } from 'fs'; import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; @@ -16,9 +17,14 @@ let composerCache: ComposerAutoload | null = null; let cacheWorkspaceRoot: string | null = null; let cacheModifiedTime: number | null = null; +@injectable() export class ComposerAutoloadManager { + constructor( + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + ) {} + public async execute() { - const workspaceRoot = new WorkspacePathResolver().getRootPath(); + const workspaceRoot = this.workspacePathResolver.getRootPath(); if (!workspaceRoot) { return DEFAULT; diff --git a/src/infra/autoload/NamespaceAutoloadMapper.ts b/src/infra/autoload/NamespaceAutoloadMapper.ts index 9f8ac77..0d81d22 100644 --- a/src/infra/autoload/NamespaceAutoloadMapper.ts +++ b/src/infra/autoload/NamespaceAutoloadMapper.ts @@ -1,3 +1,4 @@ +import { inject, injectable } from "tsyringe"; import { AutoloadPathResolver } from './AutoloadPathResolver'; import { ComposerAutoloadManager } from './ComposerAutoloadManager'; import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; @@ -6,9 +7,16 @@ interface Props { uri: string } +@injectable() export class NamespaceAutoloadMapper { + constructor ( + @inject(ComposerAutoloadManager) private composerAutoloadManager: ComposerAutoloadManager, + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + @inject(AutoloadPathResolver) private autoloadPathResolver: AutoloadPathResolver, + ) {} + public async execute({ uri }: Props) { - const { autoload, autoloadDev } = await new ComposerAutoloadManager().execute(); + const { autoload, autoloadDev } = await this.composerAutoloadManager.execute(); if (!autoload && !autoloadDev) { return { @@ -17,16 +25,14 @@ export class NamespaceAutoloadMapper { }; } - const newDir = new WorkspacePathResolver().removeWorkspaceRoot(uri); - - const autoloadPathResolver = new AutoloadPathResolver(); + const newDir = this.workspacePathResolver.removeWorkspaceRoot(uri); return { - autoload: await autoloadPathResolver.execute({ + autoload: await this.autoloadPathResolver.execute({ autoload, workspaceRoot: newDir, }), - autoloadDev: await autoloadPathResolver.execute({ + autoloadDev: await this.autoloadPathResolver.execute({ autoload: autoloadDev, workspaceRoot: newDir, }) diff --git a/tsconfig.json b/tsconfig.json index 5ccd31a..7f12205 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,9 +10,17 @@ "strict": true, "baseUrl": ".", "paths": { - "@app/*": ["./src/app/*"], - "@domain/*": ["./src/domain/*"], - "@infra/*": ["./src/infra/*"], - } + "@app/*": [ + "./src/app/*" + ], + "@domain/*": [ + "./src/domain/*" + ], + "@infra/*": [ + "./src/infra/*" + ], + }, + "experimentalDecorators": true, + "emitDecoratorMetadata": true } } From 3e1e7162d772dbe2d4c4d82e0d6c5e97fe425dba Mon Sep 17 00:00:00 2001 From: Rejman Date: Mon, 13 Oct 2025 21:00:41 -0300 Subject: [PATCH 3/4] feat: dedicated event --- src/app/events/FileRenameHandler.ts | 14 ++++++++ src/app/features/FileRenameFeature.ts | 49 +++++++++++++++++++++++++++ src/extension.ts | 45 +++++------------------- 3 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 src/app/events/FileRenameHandler.ts create mode 100644 src/app/features/FileRenameFeature.ts diff --git a/src/app/events/FileRenameHandler.ts b/src/app/events/FileRenameHandler.ts new file mode 100644 index 0000000..317b6c1 --- /dev/null +++ b/src/app/events/FileRenameHandler.ts @@ -0,0 +1,14 @@ +import { inject, injectable } from 'tsyringe'; +import { FileRenameEvent } from 'vscode'; +import { FileRenameFeature } from '@app/features/FileRenameFeature'; + +@injectable() +export class FileRenameHandler { + constructor( + @inject(FileRenameFeature) private fileRenameFeature: FileRenameFeature, + ) {} + + public async handle(event: FileRenameEvent) { + await this.fileRenameFeature.execute(event.files); + } +} diff --git a/src/app/features/FileRenameFeature.ts b/src/app/features/FileRenameFeature.ts new file mode 100644 index 0000000..e41a023 --- /dev/null +++ b/src/app/features/FileRenameFeature.ts @@ -0,0 +1,49 @@ +import { inject, injectable } from 'tsyringe'; +import { ConfigKeys } from '@domain/workspace/ConfigurationLocator'; +import { FeatureFlagManager } from '@domain/workspace/FeatureFlagManager'; +import { ImportRemover } from '@app/services/remove/ImportRemover'; +import { MissingClassImporter } from '@app/services/MissingClassImporter'; +import { NamespaceBatchUpdater } from '@app/services/NamespaceBatchUpdater'; +import { PHP_EXTENSION } from '@infra/utils/constants'; +import { Uri } from 'vscode'; + +interface Props extends ReadonlyArray<{ + oldUri: Uri + newUri: Uri +}> {} + +@injectable() +export class FileRenameFeature { + constructor( + @inject(ImportRemover) private importRemover: ImportRemover, + @inject(MissingClassImporter) private missingClassImporter: MissingClassImporter, + @inject(NamespaceBatchUpdater) private namespaceBatchUpdater: NamespaceBatchUpdater, + @inject(FeatureFlagManager) private featureFlagManager: FeatureFlagManager, + ) {} + + public async execute(files: Props) { + for (const { oldUri, newUri } of files) { + if (!oldUri.fsPath.endsWith(PHP_EXTENSION) + || !newUri.fsPath.endsWith(PHP_EXTENSION)) { + continue; + } + + try { + await this.namespaceBatchUpdater.execute({ newUri, oldUri }); + + if (this.featureFlagManager.isActive({ key: ConfigKeys.AUTO_IMPORT_NAMESPACE })) { + await this.missingClassImporter.execute({ + oldUri, + newUri, + }); + } + + await this.importRemover.execute({ uri: newUri }); + } catch (error) { + // eslint-disable-next-line no-undef + console.error('Error processing file rename:', error); + throw error; + } + } + } +} diff --git a/src/extension.ts b/src/extension.ts index dd0ee77..88858d3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,49 +1,20 @@ import "reflect-metadata"; import * as fs from 'fs'; -import { COMPOSER_FILE, PHP_EXTENSION } from '@infra/utils/constants'; -import { ConfigKeys } from '@domain/workspace/ConfigurationLocator'; +import { FileRenameEvent, workspace } from 'vscode'; +import { COMPOSER_FILE } from '@infra/utils/constants'; import { container } from "tsyringe"; -import { FeatureFlagManager } from '@domain/workspace/FeatureFlagManager'; -import { ImportRemover } from '@app/services/remove/ImportRemover'; -import { MissingClassImporter } from '@app/services/MissingClassImporter'; -import { NamespaceBatchUpdater } from '@app/services/NamespaceBatchUpdater'; -import { workspace } from 'vscode'; +import { FileRenameHandler } from '@app/events/FileRenameHandler'; import { WorkspacePathResolver } from './domain/workspace/WorkspacePathResolver'; export function activate() { - const files: string[] = fs.readdirSync(new WorkspacePathResolver().getRootPath()); + const workspacePathResolver = container.resolve(WorkspacePathResolver); + const files = fs.readdirSync(workspacePathResolver.getRootPath()); if (!files.includes(COMPOSER_FILE)) { return; } - workspace.onDidRenameFiles(async (event) => { - const importRemover = container.resolve(ImportRemover); - const missingClassImporter = container.resolve(MissingClassImporter); - const namespaceBatchUpdater = container.resolve(NamespaceBatchUpdater); - const featureFlagManager = container.resolve(FeatureFlagManager); - - for (const { oldUri, newUri } of event.files) { - if (!oldUri.fsPath.endsWith(PHP_EXTENSION) - || !newUri.fsPath.endsWith(PHP_EXTENSION)) { - continue; - } - - try { - await namespaceBatchUpdater.execute({ newUri, oldUri }); - - if (featureFlagManager.isActive({ key: ConfigKeys.AUTO_IMPORT_NAMESPACE })) { - await missingClassImporter.execute({ - oldUri, - newUri, - }); - } - - await importRemover.execute({ uri: newUri }); - } catch (error) { - // eslint-disable-next-line no-undef - console.error('Error processing file rename:', error); - throw error; - } - } + const handler = container.resolve(FileRenameHandler); + workspace.onDidRenameFiles(async (event: FileRenameEvent) => { + await handler.handle(event); }); } From e959f255728c688a92f8b5abf4780aaab676edd7 Mon Sep 17 00:00:00 2001 From: Rejman Date: Tue, 14 Oct 2025 07:38:41 -0300 Subject: [PATCH 4/4] fix: security package --- package-lock.json | 209 +++++++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ad54e0..84d2c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -634,9 +634,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -663,9 +663,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -678,9 +678,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -701,10 +701,23 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -715,9 +728,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", - "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -739,9 +752,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -763,13 +776,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", - "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -783,13 +799,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -851,9 +867,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1084,9 +1100,9 @@ } }, "node_modules/@rocketseat/eslint-config/node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1522,9 +1538,9 @@ } }, "node_modules/@rocketseat/eslint-config/node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2262,9 +2278,9 @@ } }, "node_modules/@vscode/vsce/node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2290,9 +2306,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -2720,9 +2736,9 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3946,19 +3962,20 @@ } }, "node_modules/eslint": { - "version": "9.21.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", - "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.0", - "@eslint/js": "9.21.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3969,9 +3986,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4141,9 +4158,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4215,9 +4232,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4268,9 +4285,9 @@ } }, "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4401,9 +4418,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4453,9 +4470,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4499,9 +4516,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -4560,9 +4577,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4612,15 +4629,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4630,9 +4647,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7074,9 +7091,9 @@ } }, "node_modules/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -8156,9 +8173,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -8998,9 +9015,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "optional": true, @@ -9100,9 +9117,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": {