diff --git a/package-lock.json b/package-lock.json index c4bce56..84d2c05 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", @@ -630,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": { @@ -659,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": { @@ -674,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": { @@ -697,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": { @@ -711,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": { @@ -735,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": { @@ -759,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": { @@ -779,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": { @@ -847,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": { @@ -1080,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": { @@ -1518,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": { @@ -2258,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": { @@ -2286,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": { @@ -2716,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": { @@ -3942,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", @@ -3965,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", @@ -4137,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": { @@ -4211,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": { @@ -4264,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": { @@ -4397,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": { @@ -4449,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": { @@ -4495,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": { @@ -4556,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": { @@ -4608,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" @@ -4626,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": { @@ -7070,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": { @@ -7998,6 +8019,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", @@ -8146,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": { @@ -8988,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, @@ -9090,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": { @@ -9198,6 +9225,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/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/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..86016e9 --- /dev/null +++ b/src/app/services/MissingClassImporter.ts @@ -0,0 +1,88 @@ +import { inject, injectable } from "tsyringe"; +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 +} + +@injectable() +export class MissingClassImporter { + 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) { + const directoryPath = this.workspacePathResolver.extractDirectoryFromPath(oldUri.fsPath); + const classes = await this.getClassesNamesInDirectory(directoryPath); + + if (classes.length < 1) { + return; + } + + try { + const { document, text } = await this.textDocumentOpener.execute({ uri: newUri }); + + const imports = await this.useStatementCreator.multiple({ + classesUsed: this.unusedImportDetector.execute({ + contentDocument: text, + classes, + }), + directoryPath, + }); + + if (!imports || (directoryPath === this.workspacePathResolver.extractDirectoryFromPath(newUri.fsPath))) { + return; + } + + const insertionIndex = this.useStatementLocator.execute({ document }); + if (insertionIndex === 0) { + return; + } + + const edit = new WorkspaceEdit(); + + for (const use of imports) { + await this.useStatementInjector.save({ + document, + workspaceEdit: edit, + uri: newUri, + lastUseEndIndex: insertionIndex, + useNamespace: use, + flush: false, + }); + } + + if (imports.length > 0) { + await this.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..ca6b8f6 --- /dev/null +++ b/src/app/services/NamespaceBatchUpdater.ts @@ -0,0 +1,52 @@ +import { inject, injectable } from "tsyringe"; +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, +} + +@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 { + namespace: newNamespace, + fullNamespace: useNewNamespace, + } = await this.namespaceCreator.execute({ + uri: newUri, + }); + + if (!newNamespace) { + return; + } + + const { fullNamespace: useOldNamespace } = await this.namespaceCreator.execute({ + uri: oldUri, + }); + + const updated = await this.movedFileNamespaceUpdater.execute({ + newNamespace, + newUri, + }); + + if (!updated) { + return; + } + + await this.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..29e74e2 --- /dev/null +++ b/src/app/services/TextDocumentOpener.ts @@ -0,0 +1,22 @@ +import { TextDocument, Uri, workspace } from 'vscode'; +import { injectable } from "tsyringe"; + +interface Props { + uri: Uri +} + +type OpenTextDocument = { + document: TextDocument + text: string +} + +@injectable() +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..22ee42b --- /dev/null +++ b/src/app/services/import/UnusedImportDetector.ts @@ -0,0 +1,39 @@ +import { injectable } from "tsyringe"; + +interface Props { + contentDocument: string, + classes: string[], +} + +@injectable() +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..ab64011 --- /dev/null +++ b/src/app/services/remove/ImportRemover.ts @@ -0,0 +1,105 @@ +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'; +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[] +} + +@injectable() +export class ImportRemover { + constructor( + @inject(FeatureFlagManager) private featureFlagManager: FeatureFlagManager, + @inject(NamespaceCreator) private namespaceCreator: NamespaceCreator, + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + @inject(TextDocumentOpener) private textDocumentOpener: TextDocumentOpener, + ) {} + + public async execute({ uri }: Props) { + if (!this.featureFlagManager.isActive({ key: ConfigKeys.REMOVE_UNUSED_IMPORTS })) { + return; + } + + const { className } = await this.namespaceCreator.execute({ uri }); + + 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 => this.workspacePathResolver.extractClassNameFromPath(uri.fsPath)) + .filter(Boolean) + .filter(name => name !== className); + + if (!fileNames.length) { + return; + } + + try { + const { document } = await this.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 this.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..0442975 --- /dev/null +++ b/src/app/services/update/MovedFileNamespaceUpdater.ts @@ -0,0 +1,43 @@ +import { inject, injectable } from "tsyringe"; +import { Range, Uri, workspace, WorkspaceEdit } from 'vscode'; +import { TextDocumentOpener } from '@app/services/TextDocumentOpener'; + +interface Props { + newNamespace: string, + newUri: Uri, +} + +@injectable() +export class MovedFileNamespaceUpdater { + constructor( + @inject(TextDocumentOpener) private textDocumentOpener: TextDocumentOpener + ) {} + + public async execute({ newNamespace, newUri }: Props) { + const { document, text } = await this.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..723eb1b --- /dev/null +++ b/src/app/services/update/MultiFileReferenceUpdater.ts @@ -0,0 +1,121 @@ +import { inject, injectable } from "tsyringe"; +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 +} + +@injectable() +export class MultiFileReferenceUpdater { + 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, + useNewNamespace, + newUri, + oldUri, + }: Props) { + const directoryPath = this.workspacePathResolver.extractDirectoryFromPath(oldUri.fsPath); + const className = this.workspacePathResolver.extractClassNameFromPath(oldUri.fsPath); + + const useImport = this.useStatementCreator.single({ fullNamespace: useNewNamespace }); + + const ignoreFile = newUri.fsPath; + + const files = await this.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 this.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 this.textDocumentOpener.execute({ uri: file }); + + if (!text.includes(className)) { + return; + } + + const insertionIndex = this.useStatementLocator.execute({ document }); + if (insertionIndex === 0) { + return; + } + + const edit = new WorkspaceEdit(); + + await this.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..25bdfe3 --- /dev/null +++ b/src/app/services/workspace/WorkspaceFileFinder.ts @@ -0,0 +1,56 @@ +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 cacheDuration: number = 0; + + constructor( + @inject(ConfigurationLocator) private configurationLocator: ConfigurationLocator, + ) { + } + + async execute(duration: number = 4): Promise { + const now = Date.now(); + if (this.cachedFiles && (now - this.cacheTimestamp) < this.cacheDuration) { + return this.cachedFiles; + } + + const extensions = this.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 = this.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; + this.cacheDuration = SECONDS_IN_AN_HOUR * duration; + + 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..cb18c17 --- /dev/null +++ b/src/domain/namespace/NamespaceCreator.ts @@ -0,0 +1,54 @@ +import { inject, injectable } from "tsyringe"; +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 +} + +@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 this.namespaceAutoloadMapper.execute({ + uri: uri.fsPath + }); + + const className = this.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..b1b3026 --- /dev/null +++ b/src/domain/namespace/UseStatementCreator.ts @@ -0,0 +1,41 @@ +import { inject, injectable } from "tsyringe"; +import { NamespaceCreator } from './NamespaceCreator'; +import { Uri } from 'vscode'; + +interface MultipleProps { + classesUsed: string[], + directoryPath: string +} + +interface SingleProps { + fullNamespace: string +} + +@injectable() +export class UseStatementCreator { + 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 this.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..ac40d90 --- /dev/null +++ b/src/domain/namespace/UseStatementInjector.ts @@ -0,0 +1,40 @@ +import { Range, TextDocument, Uri, workspace, WorkspaceEdit } from 'vscode'; +import { injectable } from "tsyringe"; + +interface Props { + document: TextDocument + workspaceEdit: WorkspaceEdit + uri: Uri + useNamespace: string + lastUseEndIndex: number + flush: boolean +} + +@injectable() +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..8e8d222 --- /dev/null +++ b/src/domain/namespace/UseStatementLocator.ts @@ -0,0 +1,41 @@ +import { injectable } from "tsyringe"; +import { TextDocument } from 'vscode'; + +interface Props { + document: TextDocument +} + +@injectable() +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..0ac011c --- /dev/null +++ b/src/domain/workspace/ConfigurationLocator.ts @@ -0,0 +1,27 @@ +import { workspace, WorkspaceConfiguration } from 'vscode'; +import { injectable } from "tsyringe"; + +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 +} + +@injectable() +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..71fe770 --- /dev/null +++ b/src/domain/workspace/FeatureFlagManager.ts @@ -0,0 +1,20 @@ +import { workspace, WorkspaceConfiguration } from 'vscode'; +import { injectable } from "tsyringe"; + +export type Props = { + key: string, + defaultValue?: boolean +} + +@injectable() +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..2f2f014 --- /dev/null +++ b/src/domain/workspace/WorkspacePathResolver.ts @@ -0,0 +1,28 @@ +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 + ?.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..88858d3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,41 +1,20 @@ +import "reflect-metadata"; 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 { workspace } from 'vscode'; +import { FileRenameEvent, workspace } from 'vscode'; +import { COMPOSER_FILE } from '@infra/utils/constants'; +import { container } from "tsyringe"; +import { FileRenameHandler } from '@app/events/FileRenameHandler'; +import { WorkspacePathResolver } from './domain/workspace/WorkspacePathResolver'; export function activate() { - const files: string[] = fs.readdirSync(WORKSPACE_ROOT); + const workspacePathResolver = container.resolve(WorkspacePathResolver); + const files = fs.readdirSync(workspacePathResolver.getRootPath()); if (!files.includes(COMPOSER_FILE)) { return; } - workspace.onDidRenameFiles(async (event) => { - for (const { oldUri, newUri } of event.files) { - if (!oldUri.fsPath.endsWith(PHP_EXTENSION) - || !newUri.fsPath.endsWith(PHP_EXTENSION)) { - continue; - } - - try { - await updateReferences({ newUri, oldUri }); - - if (isConfigEnabled({ key: ConfigKeys.AUTO_IMPORT_NAMESPACE })) { - await importMissingClasses({ - oldFileName: oldUri.fsPath, - newUri, - }); - } - - await removeUnusedImports({ 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); }); } diff --git a/src/infra/autoload/AutoloadPathResolver.ts b/src/infra/autoload/AutoloadPathResolver.ts new file mode 100644 index 0000000..fb4d060 --- /dev/null +++ b/src/infra/autoload/AutoloadPathResolver.ts @@ -0,0 +1,34 @@ +import { injectable } from "tsyringe"; + +type AutoloadType = { + [key: string]: string +} + +interface Props { + autoload: AutoloadType, + workspaceRoot: string, +} + +@injectable() +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..791f9f7 --- /dev/null +++ b/src/infra/autoload/ComposerAutoloadManager.ts @@ -0,0 +1,62 @@ +import { inject, injectable } from "tsyringe"; +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; + +@injectable() +export class ComposerAutoloadManager { + constructor( + @inject(WorkspacePathResolver) private workspacePathResolver: WorkspacePathResolver, + ) {} + + public async execute() { + const workspaceRoot = this.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..0d81d22 --- /dev/null +++ b/src/infra/autoload/NamespaceAutoloadMapper.ts @@ -0,0 +1,41 @@ +import { inject, injectable } from "tsyringe"; +import { AutoloadPathResolver } from './AutoloadPathResolver'; +import { ComposerAutoloadManager } from './ComposerAutoloadManager'; +import { WorkspacePathResolver } from '@domain/workspace/WorkspacePathResolver'; + +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 this.composerAutoloadManager.execute(); + + if (!autoload && !autoloadDev) { + return { + autoload: null, + autoloadDev: null, + }; + } + + const newDir = this.workspacePathResolver.removeWorkspaceRoot(uri); + + return { + autoload: await this.autoloadPathResolver.execute({ + autoload, + workspaceRoot: newDir, + }), + autoloadDev: await this.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); -}; 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 } }