Skip to content

Commit 1f1ed88

Browse files
ctf0ctf0
authored andcommitted
040
1 parent acbcc64 commit 1f1ed88

File tree

7 files changed

+634
-273
lines changed

7 files changed

+634
-273
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,12 @@ All notable changes to the "php-namespace-resolver" extension will be documented
6565

6666
- fix not showing all available namespaces for selected class
6767
- use `fs.readFile` instead of `vscode.openDocument`
68+
69+
## 0.4.0
70+
71+
- add new configs
72+
- configs are now separated into categories, plz update your settings
73+
- try to load php builtin namespaces dynamically & fall back to hardcoded classes when not possible
74+
- if use statement already exists, no changes will be made
75+
- support importing/expanding class FQN when its called with a partial FQN ex.`Rules\Password` + `use Illuminate\Validation\Rules;`
76+
- you will get an error msg if a use statement already exists with a similar class name of what u r trying to import ex.`use Illuminate\Facade\Password;` + importing ex.`Rules\Password`

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
based on https://github.com/MehediDracula/PHP-Namespace-Resolver which seems abandoned
44

5-
# Changes
5+
## Changes
66

77
- apply pending PRs
88
- remove `Highlight` cmnds and settings as most of the time they aren't accurate
99
- generate name space should work correctly for both root & sub dirs, if u have issues plz open a ticket.
1010
- expose an API for other extensions to use
11+
- check for namespaces project wide
12+
13+
### Check for namespaces project wide
14+
15+
- make sure to run `composer dump` first & fix any reported issues.
16+
- run `PHP Namespace Resolver: Check for namespaces project wide`
17+
- note that commented out FQN will show up in the problems panel as well, the cmnd lists all the namespaces that are unknown regardless of its position

package.json

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "php-namespace-resolver",
33
"displayName": "PHP Namespace Resolver",
44
"description": "Import and expand php namespaces",
5-
"version": "0.3.1",
5+
"version": "0.4.0",
66
"publisher": "ctf0",
77
"author": "ctf0",
88
"repository": "https://github.com/ctf0/PHP-Namespace-Resolver",
@@ -25,35 +25,41 @@
2525
"namespaceResolver.importAll",
2626
"namespaceResolver.expand",
2727
"namespaceResolver.sort",
28-
"namespaceResolver.generateNamespace"
28+
"namespaceResolver.generateNamespace",
29+
"onStartupFinished"
2930
],
3031
"main": "./out/main",
3132
"icon": "images/icon.png",
3233
"contributes": {
3334
"commands": [
3435
{
35-
"title": "Import Class",
3636
"command": "namespaceResolver.import",
37+
"title": "Import Class",
3738
"category": "PHP Namespace Resolver"
3839
},
3940
{
40-
"title": "Import All Classes",
4141
"command": "namespaceResolver.importAll",
42+
"title": "Import All Classes",
4243
"category": "PHP Namespace Resolver"
4344
},
4445
{
45-
"title": "Expand Class",
4646
"command": "namespaceResolver.expand",
47+
"title": "Expand Class",
4748
"category": "PHP Namespace Resolver"
4849
},
4950
{
50-
"title": "Sort Imports",
5151
"command": "namespaceResolver.sort",
52+
"title": "Sort Imports",
5253
"category": "PHP Namespace Resolver"
5354
},
5455
{
55-
"title": "Generate namespace for this file",
5656
"command": "namespaceResolver.generateNamespace",
57+
"title": "Generate namespace for this file",
58+
"category": "PHP Namespace Resolver"
59+
},
60+
{
61+
"command": "namespaceResolver.checkForNamespaces",
62+
"title": "Check for namespaces project wide",
5763
"category": "PHP Namespace Resolver"
5864
}
5965
],
@@ -132,12 +138,14 @@
132138
{
133139
"command": "namespaceResolver.generateNamespace",
134140
"when": "editorLangId == php"
141+
},
142+
{
143+
"command": "namespaceResolver.checkForNamespaces"
135144
}
136145
]
137146
},
138147
"configuration": {
139-
"type": "object",
140-
"title": "PHP Namespace Resolver extension configuration",
148+
"title": "PHP Namespace Resolver",
141149
"properties": {
142150
"namespaceResolver.exclude": {
143151
"type": "string",
@@ -149,22 +157,22 @@
149157
"default": false,
150158
"description": "Show message on status bar instead of notification box"
151159
},
152-
"namespaceResolver.autoSort": {
160+
"namespaceResolver.sort.auto": {
153161
"type": "boolean",
154162
"default": true,
155163
"description": "Auto sort after imports"
156164
},
157-
"namespaceResolver.sortOnSave": {
165+
"namespaceResolver.sort.onSave": {
158166
"type": "boolean",
159167
"default": false,
160168
"description": "Auto sort when a file is saved"
161169
},
162-
"namespaceResolver.sortAlphabetically": {
170+
"namespaceResolver.sort.alphabetically": {
163171
"type": "boolean",
164172
"default": false,
165173
"description": "Sort imports in alphabetical order instead of line length"
166174
},
167-
"namespaceResolver.sortNatural": {
175+
"namespaceResolver.sort.natural": {
168176
"type": "boolean",
169177
"default": false,
170178
"description": "Sort imports using a 'natural order' algorithm"
@@ -173,6 +181,51 @@
173181
"type": "boolean",
174182
"default": true,
175183
"description": "Expand class with leading namespace separator"
184+
},
185+
"namespaceResolver.php.command": {
186+
"type": "string",
187+
"default": "php",
188+
"description": "php command path"
189+
},
190+
"namespaceResolver.php.builtIns": {
191+
"type": "array",
192+
"default": [
193+
"get_declared_classes()",
194+
"get_declared_interfaces()"
195+
],
196+
"items": {
197+
"type": "string"
198+
},
199+
"minItems": 1,
200+
"uniqueItems": true,
201+
"description": "php methods to get builtin/extension classes & interfaces"
202+
},
203+
"namespaceResolver.checkForNamespaces.classMapFileGlob": {
204+
"type": "string",
205+
"default": "vendor/**/composer/autoload_classmap.php",
206+
"description": "composer autoload_classmap file(s) path glob"
207+
},
208+
"namespaceResolver.checkForNamespaces.rg.command": {
209+
"type": "string",
210+
"default": "rg",
211+
"markdownDescription": "ripgrep command path (require [ripgrep](https://github.com/BurntSushi/ripgrep#installation))"
212+
},
213+
"namespaceResolver.checkForNamespaces.rg.excludeDirs": {
214+
"type": "array",
215+
"default": [
216+
"vendor",
217+
"stubs",
218+
"bootstrap",
219+
"resources",
220+
"storage",
221+
"lang"
222+
],
223+
"items": {
224+
"type": "string"
225+
},
226+
"minItems": 1,
227+
"uniqueItems": true,
228+
"markdownDescription": "exclude directories from namespace use call search\n\n(.gitignore contents are already excluded)"
176229
}
177230
}
178231
}
@@ -192,8 +245,11 @@
192245
"typescript": "^4.6.3"
193246
},
194247
"dependencies": {
248+
"escape-string-regexp": "^5.0.0",
249+
"execa": "^6.1.0",
195250
"find-up": "^6.3.0",
196251
"fs-extra": "^11.1.0",
252+
"lodash.groupby": "^4.6.0",
197253
"node-natural-sort": "^0.8.6",
198254
"php-parser": "^3.1.2"
199255
}

src/NamespaceCheck.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* copyright @ctf0 https://github.com/ctf0
3+
* ================================================
4+
* read the namespaces using `namespaceResolver.checkForNamespaces.classMapFileGlob`
5+
* read the php_built-in classes using `namespaceResolver.php.builtIns`
6+
* use regex to search for any class imports `FQN, partial FQN` project wide excluding folders we dont care about using `ripgrep`
7+
* filter the statements that we already know about (using the data we have from `autoload_classmap` & `php_built-in`)
8+
* add the unknown statements to the diagnose panel
9+
*/
10+
11+
import escapeStringRegexp from 'escape-string-regexp';
12+
import { execa } from 'execa';
13+
import * as vscode from 'vscode';
14+
import Resolver from './Resolver';
15+
const _groupBy = require('lodash.groupby');
16+
17+
export default async function checkForNamespaces(resolver: Resolver, createDiagnosticCollection: vscode.DiagnosticCollection) {
18+
if (resolver.CWD) {
19+
vscode.window.withProgress({
20+
location : vscode.ProgressLocation.Notification,
21+
cancellable : false,
22+
title : 'Please Wait',
23+
}, async () => {
24+
try {
25+
const autoloadFilesData = [...await getAutoloadFileData(resolver)]
26+
.map((item) => item.namespace)
27+
.concat(resolver.BUILT_IN_CLASSES)
28+
.flat();
29+
30+
const filesWithUseStatements: any = await searchAllFilesForImports(resolver);
31+
const nonFoundNamespaces = findNonFoundNamespaces(
32+
[...new Set(autoloadFilesData)],
33+
filesWithUseStatements,
34+
);
35+
36+
const list: any = [];
37+
38+
for (const item of nonFoundNamespaces) {
39+
list.push([
40+
vscode.Uri.file(item.file.replace(new RegExp('^\.', 'm'), resolver.CWD)),
41+
[createDiagnostic(item)],
42+
]);
43+
}
44+
45+
createDiagnosticCollection.set(list);
46+
47+
await vscode.commands.executeCommand('workbench.panel.markers.view.focus');
48+
} catch (error) {
49+
console.error(error);
50+
}
51+
});
52+
}
53+
}
54+
55+
async function getAutoloadFileData(resolver: Resolver): Promise<any> {
56+
const classMapFileGlob = resolver.config('checkForNamespaces.classMapFileGlob');
57+
58+
if (!classMapFileGlob) {
59+
throw new Error('config required : classMapFileGlob');
60+
}
61+
62+
const files = await vscode.workspace.findFiles(classMapFileGlob, null);
63+
64+
const _data: any = await Promise
65+
.all(
66+
files.map(async (file) => {
67+
const fPath = file.path.replace(`${resolver.CWD}/`, '');
68+
69+
return Object
70+
.entries(await resolver.runPhpCli(`include("${fPath}")`))
71+
.map(([key, value]) => {
72+
const file: any = value;
73+
74+
return {
75+
namespace: key,
76+
// file : file,
77+
// name : resolver.getFileNameFromPath(file),
78+
};
79+
});
80+
}),
81+
);
82+
83+
return _data.flat();
84+
}
85+
86+
async function searchAllFilesForImports(resolver: Resolver): Promise<any> {
87+
const rgCommand = resolver.config('checkForNamespaces.rg.command');
88+
const rgExcludeDirs = resolver.config('checkForNamespaces.rg.excludeDirs').map((item) => escapeStringRegexp(item)).join(',');
89+
90+
if (!rgCommand || !rgExcludeDirs) {
91+
throw new Error('config required : rgCommand,rgExcludeDirs');
92+
}
93+
94+
const { stdout } = await execa(rgCommand, [
95+
"'((namespace|use) )?\\\\?(\\w+\\\\)+\\w+(?=[ ;:\\(])'",
96+
'--mmap',
97+
'--pcre2',
98+
'--no-messages',
99+
'--line-number',
100+
'--only-matching',
101+
"--glob='!*blade.php'",
102+
`--glob='!{${rgExcludeDirs}}'`,
103+
"--glob='**/*.php'",
104+
"./",
105+
], {
106+
cwd : resolver.CWD,
107+
shell : vscode.env.shell,
108+
});
109+
110+
const uses: any = [];
111+
112+
return stdout
113+
.split('\n')
114+
.filter((line) => !line.includes(':namespace '))
115+
.map((item) => {
116+
const matches: any = item.split(':');
117+
const _file = matches[0];
118+
const _import = matches[2].replace(new RegExp('^\\\\', 'm'), ''); // \App > App
119+
120+
/* -------------------------------------------------------------------------- */
121+
if (_import.startsWith('use ') && !uses.includes(_import)) {
122+
uses.push({
123+
file : _file,
124+
import : _import,
125+
found : 0,
126+
});
127+
}
128+
129+
if (!_import.startsWith('use ')) {
130+
const classBaseName: any = new RegExp(/\w+(?=\\)/).exec(_import);
131+
132+
if (classBaseName !== null) {
133+
const found = uses.find((_use) => _use.import.endsWith(classBaseName[0]) && _use.file === _file);
134+
135+
if (found) {
136+
uses.find((_use) => {
137+
if (_use.file === _file) {
138+
_use.found++;
139+
140+
return true;
141+
}
142+
});
143+
144+
return;
145+
}
146+
}
147+
}
148+
/* -------------------------------------------------------------------------- */
149+
150+
return {
151+
file : _file,
152+
line : parseInt(matches[1]),
153+
import : _import.replace('use ', ''),
154+
};
155+
})
156+
.map((obj: any) => {
157+
if (obj && uses.find((_use) => _use.file === obj.file && _use.found > 0)) {
158+
return;
159+
}
160+
161+
return obj;
162+
})
163+
.filter((e) => e);
164+
}
165+
166+
function findNonFoundNamespaces(appNamespaces, importsFiles) {
167+
importsFiles = _groupBy(importsFiles, 'import');
168+
const list: any = [];
169+
170+
for (const namespace of Object.keys(importsFiles)) {
171+
if (!appNamespaces.includes(namespace)) {
172+
list.push(importsFiles[namespace]);
173+
}
174+
}
175+
176+
return list.flat();
177+
}
178+
179+
function createDiagnostic(item) {
180+
const line = item.line - 1;
181+
const diagnostic = new vscode.Diagnostic(
182+
new vscode.Range(line, 0, line, 0),
183+
`unknown namespace : ${item.import}`,
184+
vscode.DiagnosticSeverity.Warning,
185+
);
186+
187+
diagnostic.source = 'PHP Namespace Resolver';
188+
189+
return diagnostic;
190+
}

0 commit comments

Comments
 (0)