fluidattacks_core_filesystem 12.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fluidattacks_core/filesystem/__init__.py +18 -0
- fluidattacks_core/filesystem/defaults.py +517 -0
- fluidattacks_core/filesystem/detection.py +114 -0
- fluidattacks_core/filesystem/language.py +27 -0
- fluidattacks_core/filesystem/partitioning.py +254 -0
- fluidattacks_core/filesystem/parts.py +178 -0
- fluidattacks_core/filesystem/projects.py +154 -0
- fluidattacks_core/filesystem/py.typed +0 -0
- fluidattacks_core_filesystem-12.0.0.dist-info/METADATA +13 -0
- fluidattacks_core_filesystem-12.0.0.dist-info/RECORD +11 -0
- fluidattacks_core_filesystem-12.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .detection import detect_languages_in_dir
|
|
2
|
+
from .language import Language
|
|
3
|
+
from .partitioning import build_part_tree, decide_part_tree
|
|
4
|
+
from .parts import Part, PartIdentity, PartTree
|
|
5
|
+
from .projects import detect_root_language, find_parent_project, find_projects
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Language",
|
|
9
|
+
"Part",
|
|
10
|
+
"PartIdentity",
|
|
11
|
+
"PartTree",
|
|
12
|
+
"build_part_tree",
|
|
13
|
+
"decide_part_tree",
|
|
14
|
+
"detect_languages_in_dir",
|
|
15
|
+
"detect_root_language",
|
|
16
|
+
"find_parent_project",
|
|
17
|
+
"find_projects",
|
|
18
|
+
]
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
3
|
+
from .language import Language
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CONFIG_MARKERS",
|
|
7
|
+
"EXCLUDE_DIRS",
|
|
8
|
+
"EXCLUDE_FILES",
|
|
9
|
+
"EXTENDED_EXCLUDE_DIRS",
|
|
10
|
+
"MANIFEST_MARKERS",
|
|
11
|
+
"SKIP_DIRS",
|
|
12
|
+
"TEST_DATA_DIRS",
|
|
13
|
+
"TEST_FILES",
|
|
14
|
+
"TEST_GLOBS",
|
|
15
|
+
"VCS_DIRS",
|
|
16
|
+
"Language",
|
|
17
|
+
"MarkerSet",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MarkerSet(TypedDict):
|
|
22
|
+
names: set[str]
|
|
23
|
+
globs: list[str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
CONFIG_MARKERS: dict[Language, MarkerSet] = {
|
|
27
|
+
Language.Java: {
|
|
28
|
+
"names": {
|
|
29
|
+
"pom.xml",
|
|
30
|
+
"settings.gradle",
|
|
31
|
+
"settings.gradle.kts",
|
|
32
|
+
"build.gradle",
|
|
33
|
+
"build.gradle.kts",
|
|
34
|
+
"gradle.properties",
|
|
35
|
+
"gradlew",
|
|
36
|
+
"gradlew.bat",
|
|
37
|
+
"mvnw",
|
|
38
|
+
"mvnw.cmd",
|
|
39
|
+
"application.properties",
|
|
40
|
+
"application.yml",
|
|
41
|
+
"application.yaml",
|
|
42
|
+
"log4j.properties",
|
|
43
|
+
"log4j2.xml",
|
|
44
|
+
"logback.xml",
|
|
45
|
+
"web.xml",
|
|
46
|
+
"hibernate.cfg.xml",
|
|
47
|
+
"persistence.xml",
|
|
48
|
+
"MANIFEST.MF",
|
|
49
|
+
"sonar-project.properties",
|
|
50
|
+
"checkstyle.xml",
|
|
51
|
+
"spotbugs.xml",
|
|
52
|
+
"pmd.xml",
|
|
53
|
+
"findbugs.xml",
|
|
54
|
+
"junit-platform.properties",
|
|
55
|
+
"testng.xml",
|
|
56
|
+
"spring.factories",
|
|
57
|
+
"spring.profiles",
|
|
58
|
+
"bootstrap.properties",
|
|
59
|
+
"bootstrap.yml",
|
|
60
|
+
},
|
|
61
|
+
"globs": [
|
|
62
|
+
"*.gradle",
|
|
63
|
+
"*.gradle.kts",
|
|
64
|
+
"application-*.properties",
|
|
65
|
+
"application-*.yml",
|
|
66
|
+
"application-*.yaml",
|
|
67
|
+
"logback-*.xml",
|
|
68
|
+
"log4j2-*.xml",
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
Language.Python: {
|
|
72
|
+
"names": {
|
|
73
|
+
"pyproject.toml",
|
|
74
|
+
"requirements.txt",
|
|
75
|
+
"requirements.in",
|
|
76
|
+
"requirements-dev.txt",
|
|
77
|
+
"requirements-test.txt",
|
|
78
|
+
"constraints.txt",
|
|
79
|
+
"setup.py",
|
|
80
|
+
"setup.cfg",
|
|
81
|
+
"Pipfile",
|
|
82
|
+
"Pipfile.lock",
|
|
83
|
+
"poetry.lock",
|
|
84
|
+
"poetry.toml",
|
|
85
|
+
"hatch.toml",
|
|
86
|
+
"uv.lock",
|
|
87
|
+
"uv.toml",
|
|
88
|
+
"tox.ini",
|
|
89
|
+
"pytest.ini",
|
|
90
|
+
"mypy.ini",
|
|
91
|
+
".pylintrc",
|
|
92
|
+
"pylintrc",
|
|
93
|
+
".flake8",
|
|
94
|
+
".isort.cfg",
|
|
95
|
+
"ruff.toml",
|
|
96
|
+
".ruff.toml",
|
|
97
|
+
".coveragerc",
|
|
98
|
+
"coverage.ini",
|
|
99
|
+
"noxfile.py",
|
|
100
|
+
"pyrightconfig.json",
|
|
101
|
+
"MANIFEST.in",
|
|
102
|
+
"py.typed",
|
|
103
|
+
"tox.toml",
|
|
104
|
+
},
|
|
105
|
+
"globs": [
|
|
106
|
+
"requirements*.txt",
|
|
107
|
+
"requirements*.in",
|
|
108
|
+
"constraints*.txt",
|
|
109
|
+
"setup.cfg",
|
|
110
|
+
"*.ini",
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
Language.JavaScript: {
|
|
114
|
+
"names": {
|
|
115
|
+
"package.json",
|
|
116
|
+
"package-lock.json",
|
|
117
|
+
"yarn.lock",
|
|
118
|
+
"pnpm-lock.yaml",
|
|
119
|
+
"bun.lockb",
|
|
120
|
+
"bunfig.toml",
|
|
121
|
+
"nx.json",
|
|
122
|
+
"lerna.json",
|
|
123
|
+
"turbo.json",
|
|
124
|
+
"angular.json",
|
|
125
|
+
"workspace.json",
|
|
126
|
+
"gulpfile.js",
|
|
127
|
+
"gulpfile.ts",
|
|
128
|
+
"gruntfile.js",
|
|
129
|
+
"gruntfile.coffee",
|
|
130
|
+
"karma.conf.js",
|
|
131
|
+
"karma.conf.coffee",
|
|
132
|
+
"protractor.conf.js",
|
|
133
|
+
"cypress.json",
|
|
134
|
+
"cypress.config.js",
|
|
135
|
+
"playwright.config.js",
|
|
136
|
+
"playwright.config.ts",
|
|
137
|
+
"vitest.config.js",
|
|
138
|
+
"vitest.config.ts",
|
|
139
|
+
"storybook",
|
|
140
|
+
".storybook",
|
|
141
|
+
},
|
|
142
|
+
"globs": [
|
|
143
|
+
"vite.config.*",
|
|
144
|
+
"next.config.*",
|
|
145
|
+
"nuxt.config.*",
|
|
146
|
+
"svelte.config.*",
|
|
147
|
+
"astro.config.*",
|
|
148
|
+
"rollup.config.*",
|
|
149
|
+
"webpack.config.*",
|
|
150
|
+
"babel.config.*",
|
|
151
|
+
".babelrc*",
|
|
152
|
+
"eslint.config.*",
|
|
153
|
+
".eslintrc*",
|
|
154
|
+
".prettierrc*",
|
|
155
|
+
"prettier.config.*",
|
|
156
|
+
"jest.config.*",
|
|
157
|
+
"vitest.config.*",
|
|
158
|
+
"cypress.config.*",
|
|
159
|
+
"playwright.config.*",
|
|
160
|
+
"karma.conf.*",
|
|
161
|
+
"protractor.conf.*",
|
|
162
|
+
"*.config.js",
|
|
163
|
+
"*.config.ts",
|
|
164
|
+
"*.config.mjs",
|
|
165
|
+
"*.config.cjs",
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
Language.TypeScript: {
|
|
169
|
+
"names": {
|
|
170
|
+
"tsconfig.json",
|
|
171
|
+
"tsconfig.base.json",
|
|
172
|
+
"tsconfig.app.json",
|
|
173
|
+
"tsconfig.build.json",
|
|
174
|
+
"tsconfig.node.json",
|
|
175
|
+
"tsconfig.lib.json",
|
|
176
|
+
"tsconfig.spec.json",
|
|
177
|
+
"tsconfig.test.json",
|
|
178
|
+
"deno.json",
|
|
179
|
+
"deno.jsonc",
|
|
180
|
+
"tsconfig.eslint.json",
|
|
181
|
+
},
|
|
182
|
+
"globs": [
|
|
183
|
+
"tsconfig.*.json",
|
|
184
|
+
"tsconfig-*.json",
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
Language.PHP: {
|
|
188
|
+
"names": {
|
|
189
|
+
"composer.json",
|
|
190
|
+
"composer.lock",
|
|
191
|
+
"php.ini",
|
|
192
|
+
"phpunit.xml",
|
|
193
|
+
"phpunit.xml.dist",
|
|
194
|
+
"phpcs.xml",
|
|
195
|
+
"phpcs.xml.dist",
|
|
196
|
+
"phpmd.xml",
|
|
197
|
+
"phpstan.neon",
|
|
198
|
+
"phpstan.neon.dist",
|
|
199
|
+
"psalm.xml",
|
|
200
|
+
"psalm.xml.dist",
|
|
201
|
+
".php-cs-fixer.php",
|
|
202
|
+
".php_cs",
|
|
203
|
+
".php_cs.dist",
|
|
204
|
+
"codeception.yml",
|
|
205
|
+
"symfony.lock",
|
|
206
|
+
"artisan",
|
|
207
|
+
"wp-config.php",
|
|
208
|
+
"config.php",
|
|
209
|
+
},
|
|
210
|
+
"globs": [
|
|
211
|
+
"phpspec.yml*",
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
Language.CSharp: {
|
|
215
|
+
"names": {
|
|
216
|
+
"app.config",
|
|
217
|
+
"web.config",
|
|
218
|
+
"packages.config",
|
|
219
|
+
"nuget.config",
|
|
220
|
+
"NuGet.Config",
|
|
221
|
+
"Directory.Build.props",
|
|
222
|
+
"Directory.Build.targets",
|
|
223
|
+
"Directory.Packages.props",
|
|
224
|
+
"global.json",
|
|
225
|
+
"appsettings.json",
|
|
226
|
+
},
|
|
227
|
+
"globs": [
|
|
228
|
+
"*.sln",
|
|
229
|
+
"*.csproj",
|
|
230
|
+
"*.fsproj",
|
|
231
|
+
"*.vbproj",
|
|
232
|
+
"appsettings.*.json",
|
|
233
|
+
],
|
|
234
|
+
},
|
|
235
|
+
Language.Go: {
|
|
236
|
+
"names": {
|
|
237
|
+
"go.mod",
|
|
238
|
+
"go.sum",
|
|
239
|
+
"go.work",
|
|
240
|
+
"go.work.sum",
|
|
241
|
+
".golangci.yml",
|
|
242
|
+
".golangci.yaml",
|
|
243
|
+
"Gopkg.toml",
|
|
244
|
+
"Gopkg.lock",
|
|
245
|
+
"glide.yaml",
|
|
246
|
+
"glide.lock",
|
|
247
|
+
".air.toml",
|
|
248
|
+
"air.toml",
|
|
249
|
+
"goreleaser.yml",
|
|
250
|
+
"goreleaser.yaml",
|
|
251
|
+
},
|
|
252
|
+
"globs": [
|
|
253
|
+
"*.mk",
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
Language.Ruby: {
|
|
257
|
+
"names": {
|
|
258
|
+
"Gemfile",
|
|
259
|
+
"Gemfile.lock",
|
|
260
|
+
".ruby-version",
|
|
261
|
+
".rubocop.yml",
|
|
262
|
+
".reek",
|
|
263
|
+
".rspec",
|
|
264
|
+
".overcommit.yml",
|
|
265
|
+
"Rakefile",
|
|
266
|
+
"Capfile",
|
|
267
|
+
"Guardfile",
|
|
268
|
+
"config.ru",
|
|
269
|
+
"database.yml",
|
|
270
|
+
"boot.rb",
|
|
271
|
+
},
|
|
272
|
+
"globs": [],
|
|
273
|
+
},
|
|
274
|
+
Language.Kotlin: {
|
|
275
|
+
"names": {
|
|
276
|
+
"build.gradle.kts",
|
|
277
|
+
"settings.gradle.kts",
|
|
278
|
+
},
|
|
279
|
+
"globs": [],
|
|
280
|
+
},
|
|
281
|
+
Language.Swift: {
|
|
282
|
+
"names": {
|
|
283
|
+
"Package.swift",
|
|
284
|
+
"Package.resolved",
|
|
285
|
+
"Config.xcconfig",
|
|
286
|
+
"Info.plist",
|
|
287
|
+
"project.pbxproj",
|
|
288
|
+
".swiftlint.yml",
|
|
289
|
+
".swiftformat",
|
|
290
|
+
"Cartfile",
|
|
291
|
+
"Cartfile.resolved",
|
|
292
|
+
"Podfile",
|
|
293
|
+
"Podfile.lock",
|
|
294
|
+
},
|
|
295
|
+
"globs": [],
|
|
296
|
+
},
|
|
297
|
+
Language.Scala: {
|
|
298
|
+
"names": {
|
|
299
|
+
"build.sbt",
|
|
300
|
+
"build.scala",
|
|
301
|
+
"project/build.properties",
|
|
302
|
+
"project/plugins.sbt",
|
|
303
|
+
"project/Dependencies.scala",
|
|
304
|
+
"project/Assembly.scala",
|
|
305
|
+
"mill",
|
|
306
|
+
"mill.bat",
|
|
307
|
+
"millw",
|
|
308
|
+
"millw.bat",
|
|
309
|
+
},
|
|
310
|
+
"globs": [
|
|
311
|
+
"*.sbt",
|
|
312
|
+
"project/*.scala",
|
|
313
|
+
"project/*.sbt",
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
Language.Rust: {
|
|
317
|
+
"names": {
|
|
318
|
+
"Cargo.toml",
|
|
319
|
+
"Cargo.lock",
|
|
320
|
+
"rust-toolchain",
|
|
321
|
+
"rust-toolchain.toml",
|
|
322
|
+
"rustfmt.toml",
|
|
323
|
+
"clippy.toml",
|
|
324
|
+
"Cargo.toml.orig",
|
|
325
|
+
},
|
|
326
|
+
"globs": [],
|
|
327
|
+
},
|
|
328
|
+
Language.Dart: {
|
|
329
|
+
"names": {
|
|
330
|
+
"pubspec.yaml",
|
|
331
|
+
"pubspec.lock",
|
|
332
|
+
"analysis_options.yaml",
|
|
333
|
+
"dart_test.yaml",
|
|
334
|
+
"build.yaml",
|
|
335
|
+
"melos.yaml",
|
|
336
|
+
"melos.yml",
|
|
337
|
+
},
|
|
338
|
+
"globs": [
|
|
339
|
+
"pubspec*.yaml",
|
|
340
|
+
"pubspec*.yml",
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
VCS_DIRS = [
|
|
346
|
+
".git",
|
|
347
|
+
".hg",
|
|
348
|
+
".svn",
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
SKIP_DIRS = {
|
|
353
|
+
*VCS_DIRS,
|
|
354
|
+
".idea",
|
|
355
|
+
".vscode",
|
|
356
|
+
"__pycache__",
|
|
357
|
+
".mypy_cache",
|
|
358
|
+
".pytest_cache",
|
|
359
|
+
"*cache*",
|
|
360
|
+
"node_modules",
|
|
361
|
+
".venv",
|
|
362
|
+
"venv",
|
|
363
|
+
"dist",
|
|
364
|
+
"build",
|
|
365
|
+
"target",
|
|
366
|
+
"*test*",
|
|
367
|
+
"out",
|
|
368
|
+
"bin",
|
|
369
|
+
"obj",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
TEST_GLOBS = {
|
|
374
|
+
Language.Python: [
|
|
375
|
+
"test*", # Python
|
|
376
|
+
"*test.py", # Python
|
|
377
|
+
],
|
|
378
|
+
Language.JavaScript: [
|
|
379
|
+
"*.spec.js", # JavaScript
|
|
380
|
+
"*.test.js", # JavaScript
|
|
381
|
+
],
|
|
382
|
+
Language.TypeScript: [
|
|
383
|
+
"*.spec.ts", # TypeScript
|
|
384
|
+
"*.test.ts", # TypeScript
|
|
385
|
+
],
|
|
386
|
+
Language.Java: [
|
|
387
|
+
"*Test.java", # Java
|
|
388
|
+
],
|
|
389
|
+
Language.Kotlin: [
|
|
390
|
+
"*Test.kt", # Kotlin
|
|
391
|
+
],
|
|
392
|
+
Language.Go: [
|
|
393
|
+
"*_test.go", # Go
|
|
394
|
+
],
|
|
395
|
+
Language.Ruby: [
|
|
396
|
+
"*_test.rb", # Ruby
|
|
397
|
+
],
|
|
398
|
+
Language.CSharp: [
|
|
399
|
+
"*.Tests.cs", # C#
|
|
400
|
+
],
|
|
401
|
+
Language.Swift: [
|
|
402
|
+
"*Test.swift", # Swift
|
|
403
|
+
],
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
EXCLUDE_DIRS = [
|
|
408
|
+
"assets",
|
|
409
|
+
"target",
|
|
410
|
+
"build",
|
|
411
|
+
".gradle",
|
|
412
|
+
".mvn",
|
|
413
|
+
".idea",
|
|
414
|
+
"out",
|
|
415
|
+
"tests",
|
|
416
|
+
"__tests__",
|
|
417
|
+
"spec",
|
|
418
|
+
".venv",
|
|
419
|
+
"__pycache__",
|
|
420
|
+
"dist",
|
|
421
|
+
".tox",
|
|
422
|
+
".pytest_cache",
|
|
423
|
+
".mypy_cache",
|
|
424
|
+
"node_modules",
|
|
425
|
+
".cache",
|
|
426
|
+
"vendor",
|
|
427
|
+
".composer",
|
|
428
|
+
"bin",
|
|
429
|
+
"obj",
|
|
430
|
+
".vs",
|
|
431
|
+
"packages",
|
|
432
|
+
".vscode",
|
|
433
|
+
"pkg",
|
|
434
|
+
".bundle",
|
|
435
|
+
"tmp",
|
|
436
|
+
".build",
|
|
437
|
+
"Carthage",
|
|
438
|
+
"Pods",
|
|
439
|
+
"DerivedData",
|
|
440
|
+
"cache",
|
|
441
|
+
".ruff_cache",
|
|
442
|
+
".metals",
|
|
443
|
+
".bloop",
|
|
444
|
+
"wwwroot",
|
|
445
|
+
"plugin",
|
|
446
|
+
"plugins",
|
|
447
|
+
"libraries",
|
|
448
|
+
".stack-work",
|
|
449
|
+
"dist-newstyle",
|
|
450
|
+
".dart_tool",
|
|
451
|
+
"deps",
|
|
452
|
+
"_build",
|
|
453
|
+
"checkouts",
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
EXCLUDE_FILES = [
|
|
458
|
+
"*.min.js",
|
|
459
|
+
"*.min.css",
|
|
460
|
+
"*.jar",
|
|
461
|
+
"*.war",
|
|
462
|
+
"*.ear",
|
|
463
|
+
"*.jmod",
|
|
464
|
+
"*.nupkg",
|
|
465
|
+
"*.gem",
|
|
466
|
+
"*.phar*",
|
|
467
|
+
"*.so",
|
|
468
|
+
"*.dll",
|
|
469
|
+
"*.dylib",
|
|
470
|
+
"*.a",
|
|
471
|
+
"*.lib",
|
|
472
|
+
"*.rlib",
|
|
473
|
+
"*.rs.bk",
|
|
474
|
+
"*mock*",
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
TEST_FILES = [x for item in TEST_GLOBS.values() for x in item]
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
MANIFEST_MARKERS: dict[Language, frozenset[str]] = {
|
|
482
|
+
Language.Java: frozenset({"pom.xml", "build.gradle", "settings.gradle"}),
|
|
483
|
+
Language.Python: frozenset({"pyproject.toml", "setup.py", "Pipfile", "requirements.txt"}),
|
|
484
|
+
Language.JavaScript: frozenset({"package.json"}),
|
|
485
|
+
Language.TypeScript: frozenset({"tsconfig.json"}),
|
|
486
|
+
Language.PHP: frozenset({"composer.json"}),
|
|
487
|
+
Language.CSharp: frozenset({"Directory.Build.props", "Directory.Build.targets", "global.json"}),
|
|
488
|
+
Language.Go: frozenset({"go.mod", "Gopkg.toml", "glide.yaml"}),
|
|
489
|
+
Language.Ruby: frozenset({"Gemfile"}),
|
|
490
|
+
Language.Kotlin: frozenset({"build.gradle.kts", "settings.gradle.kts"}),
|
|
491
|
+
Language.Swift: frozenset({"Package.swift", "Podfile", "Cartfile", "project.pbxproj"}),
|
|
492
|
+
Language.Rust: frozenset({"Cargo.toml"}),
|
|
493
|
+
Language.Dart: frozenset({"pubspec.yaml"}),
|
|
494
|
+
Language.Scala: frozenset({"build.sbt"}),
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
TEST_DATA_DIRS = [
|
|
499
|
+
"test-data",
|
|
500
|
+
"testdata",
|
|
501
|
+
"test_data",
|
|
502
|
+
"fixtures",
|
|
503
|
+
"__fixtures__",
|
|
504
|
+
"fixture",
|
|
505
|
+
"mocks",
|
|
506
|
+
"__mocks__",
|
|
507
|
+
"*mock*",
|
|
508
|
+
"testfiles",
|
|
509
|
+
"test_files",
|
|
510
|
+
"sample_data",
|
|
511
|
+
"samples",
|
|
512
|
+
"examples",
|
|
513
|
+
"test",
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
EXTENDED_EXCLUDE_DIRS = [*EXCLUDE_DIRS, *TEST_DATA_DIRS, *VCS_DIRS]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fnmatch import fnmatch
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .defaults import CONFIG_MARKERS, MANIFEST_MARKERS, MarkerSet
|
|
7
|
+
from .language import Language
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
__all__ = ["detect_anchor_language", "detect_languages_in_dir", "is_config_marker"]
|
|
13
|
+
|
|
14
|
+
_FOLDED_MARKER_NAMES = frozenset(
|
|
15
|
+
name.casefold() for markers in CONFIG_MARKERS.values() for name in markers["names"]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_MARKER_GLOBS = tuple(pat for markers in CONFIG_MARKERS.values() for pat in markers["globs"])
|
|
19
|
+
|
|
20
|
+
_FOLDED_MANIFESTS = {
|
|
21
|
+
language: frozenset(name.casefold() for name in names)
|
|
22
|
+
for language, names in MANIFEST_MARKERS.items()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_LANGUAGE_PRIORITIES = {
|
|
26
|
+
Language.TypeScript: 10,
|
|
27
|
+
Language.JavaScript: 5,
|
|
28
|
+
Language.Kotlin: 8,
|
|
29
|
+
Language.Java: 6,
|
|
30
|
+
Language.Rust: 9,
|
|
31
|
+
Language.C: 4,
|
|
32
|
+
Language.Scala: 7,
|
|
33
|
+
Language.Dart: 8,
|
|
34
|
+
Language.Python: 7,
|
|
35
|
+
Language.Go: 8,
|
|
36
|
+
Language.PHP: 6,
|
|
37
|
+
Language.Ruby: 6,
|
|
38
|
+
Language.CSharp: 7,
|
|
39
|
+
Language.Swift: 8,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_LANGUAGE_CONFLICTS = {
|
|
43
|
+
Language.TypeScript: {Language.JavaScript},
|
|
44
|
+
Language.JavaScript: {Language.TypeScript, Language.Dart},
|
|
45
|
+
Language.Kotlin: {Language.Java, Language.Scala},
|
|
46
|
+
Language.Java: {Language.Kotlin, Language.Scala},
|
|
47
|
+
Language.Scala: {Language.Java, Language.Kotlin},
|
|
48
|
+
Language.Rust: {Language.C},
|
|
49
|
+
Language.C: {Language.Rust},
|
|
50
|
+
Language.Dart: {Language.JavaScript},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_language_conflicts(lang: Language) -> set[Language]:
|
|
55
|
+
return _LANGUAGE_CONFLICTS.get(lang, set())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _calculate_language_score(names: set[str], markers: MarkerSet) -> int:
|
|
59
|
+
exacts = {n.casefold() for n in markers["names"]}
|
|
60
|
+
patterns = markers["globs"]
|
|
61
|
+
score = len([n for n in exacts if n in names]) * 10
|
|
62
|
+
glob_matches = sum(1 for f in names if any(fnmatch(f, pat) for pat in patterns))
|
|
63
|
+
return score + glob_matches * 5
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_language_conflicts(
|
|
67
|
+
hits: list[Language],
|
|
68
|
+
language_scores: dict[Language, int],
|
|
69
|
+
) -> list[Language]:
|
|
70
|
+
if len(hits) <= 1:
|
|
71
|
+
return hits
|
|
72
|
+
ordered = sorted(
|
|
73
|
+
hits,
|
|
74
|
+
key=lambda lang: (_LANGUAGE_PRIORITIES.get(lang, 0), language_scores.get(lang, 0)),
|
|
75
|
+
reverse=True,
|
|
76
|
+
)
|
|
77
|
+
resolved_hits: list[Language] = []
|
|
78
|
+
for lang in ordered:
|
|
79
|
+
conflicts = _get_language_conflicts(lang)
|
|
80
|
+
if not any(conflict in resolved_hits for conflict in conflicts):
|
|
81
|
+
resolved_hits.append(lang)
|
|
82
|
+
return resolved_hits
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def detect_languages_in_dir(files: Iterable[str]) -> list[Language]:
|
|
86
|
+
names = {f.casefold() for f in files}
|
|
87
|
+
hits: list[Language] = []
|
|
88
|
+
language_scores: dict[Language, int] = {}
|
|
89
|
+
|
|
90
|
+
for lang, markers in CONFIG_MARKERS.items():
|
|
91
|
+
score = _calculate_language_score(names, markers)
|
|
92
|
+
if score > 0:
|
|
93
|
+
language_scores[lang] = score
|
|
94
|
+
hits.append(lang)
|
|
95
|
+
|
|
96
|
+
return _resolve_language_conflicts(hits, language_scores)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def is_config_marker(name: str) -> bool:
|
|
100
|
+
return name.casefold() in _FOLDED_MARKER_NAMES or any(
|
|
101
|
+
fnmatch(name, pat) for pat in _MARKER_GLOBS
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def detect_anchor_language(files: Iterable[str]) -> Language | None:
|
|
106
|
+
names = tuple(files)
|
|
107
|
+
folded = {name.casefold() for name in names}
|
|
108
|
+
anchors = {language for language, manifests in _FOLDED_MANIFESTS.items() if manifests & folded}
|
|
109
|
+
if not anchors:
|
|
110
|
+
return None
|
|
111
|
+
ranked = tuple(lang for lang in detect_languages_in_dir(names) if lang in anchors) or tuple(
|
|
112
|
+
sorted(anchors, key=lambda language: language.value)
|
|
113
|
+
)
|
|
114
|
+
return ranked[0] if len(ranked) == 1 else Language.Unknown
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Language(Enum):
|
|
5
|
+
Swift = "swift"
|
|
6
|
+
CSharp = "csharp"
|
|
7
|
+
Go = "golang"
|
|
8
|
+
Rust = "rust"
|
|
9
|
+
Dart = "dart"
|
|
10
|
+
FuzzyTestLang = "fuzzy_test_lang"
|
|
11
|
+
Java = "Java"
|
|
12
|
+
Python = "Python"
|
|
13
|
+
PHP = "PHP"
|
|
14
|
+
Ruby = "Ruby"
|
|
15
|
+
C = "C"
|
|
16
|
+
Kotlin = "Kotlin"
|
|
17
|
+
Ghidra = "Ghidra"
|
|
18
|
+
JavaScript = "JavaScript"
|
|
19
|
+
TypeScript = "TypeScript"
|
|
20
|
+
LLVM = "LLVM"
|
|
21
|
+
NewC = "newc"
|
|
22
|
+
Scala = "Scala"
|
|
23
|
+
Unknown = "unknown"
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
"""Render as the bare value, as sifts and explores consumers expect."""
|
|
27
|
+
return str(self.value)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from fnmatch import fnmatch
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from .defaults import EXTENDED_EXCLUDE_DIRS
|
|
13
|
+
from .detection import detect_anchor_language, detect_languages_in_dir, is_config_marker
|
|
14
|
+
from .parts import Part, PartIdentity, PartTree
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Collection, Mapping
|
|
18
|
+
|
|
19
|
+
__all__ = ["DirListing", "build_part_tree", "decide_part_tree"]
|
|
20
|
+
|
|
21
|
+
DirListing = Sequence[tuple[Path, tuple[str, ...]]]
|
|
22
|
+
|
|
23
|
+
PotentialPart = tuple[Path, frozenset[Path], str | None]
|
|
24
|
+
|
|
25
|
+
_ROOT_PATH_MARKER = Path()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _generate_part_name(path: Path, root_path: Path) -> str:
|
|
29
|
+
if path == root_path:
|
|
30
|
+
return "."
|
|
31
|
+
try:
|
|
32
|
+
relative = path.relative_to(root_path)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return path.name
|
|
35
|
+
return "/".join(relative.parts)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _relativize(path: Path, root_path: Path) -> Path:
|
|
39
|
+
try:
|
|
40
|
+
return path.relative_to(root_path)
|
|
41
|
+
except ValueError:
|
|
42
|
+
return path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _find_config_files(dir_path: Path, files: Sequence[str]) -> frozenset[Path]:
|
|
46
|
+
return frozenset(dir_path / name for name in files if is_config_marker(name))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _owning_part(dir_path: Path, part_paths: frozenset[Path], root_path: Path) -> Path | None:
|
|
50
|
+
current = dir_path
|
|
51
|
+
while True:
|
|
52
|
+
if current in part_paths:
|
|
53
|
+
return current
|
|
54
|
+
if current in (root_path, current.parent):
|
|
55
|
+
return None
|
|
56
|
+
current = current.parent
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _root_filenames(listing: DirListing, root_path: Path) -> tuple[str, ...]:
|
|
60
|
+
for dir_path, filenames in listing:
|
|
61
|
+
if dir_path == root_path:
|
|
62
|
+
return filenames
|
|
63
|
+
return ()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _discover_potential_parts(listing: DirListing) -> tuple[PotentialPart, ...]:
|
|
67
|
+
return tuple(
|
|
68
|
+
(dir_path, _find_config_files(dir_path, filenames), language.value)
|
|
69
|
+
for dir_path, filenames in listing
|
|
70
|
+
if (language := detect_anchor_language(filenames)) is not None
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_hierarchy(
|
|
75
|
+
potential_parts: Sequence[PotentialPart],
|
|
76
|
+
root_path: Path,
|
|
77
|
+
) -> Mapping[Path, tuple[Path, ...]]:
|
|
78
|
+
sorted_parts = sorted(potential_parts, key=lambda part: len(part[0].parts))
|
|
79
|
+
children: dict[Path, list[Path]] = {part[0]: [] for part in sorted_parts}
|
|
80
|
+
|
|
81
|
+
for part_path, _, _ in sorted_parts:
|
|
82
|
+
if part_path == root_path:
|
|
83
|
+
continue
|
|
84
|
+
current = part_path.parent
|
|
85
|
+
while True:
|
|
86
|
+
if current in children:
|
|
87
|
+
children[current].append(part_path)
|
|
88
|
+
break
|
|
89
|
+
if current == current.parent:
|
|
90
|
+
break
|
|
91
|
+
current = current.parent
|
|
92
|
+
|
|
93
|
+
return {path: tuple(child_paths) for path, child_paths in children.items()}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _assign_files_to_parts(
|
|
97
|
+
part_paths: frozenset[Path],
|
|
98
|
+
listing: DirListing,
|
|
99
|
+
root_path: Path,
|
|
100
|
+
) -> Mapping[Path, frozenset[Path]]:
|
|
101
|
+
owned: dict[Path, set[Path]] = {part_path: set() for part_path in part_paths}
|
|
102
|
+
|
|
103
|
+
for dir_path, filenames in listing:
|
|
104
|
+
owner = _owning_part(dir_path, part_paths, root_path)
|
|
105
|
+
if owner is None:
|
|
106
|
+
continue
|
|
107
|
+
owned[owner].update(_relativize(dir_path / name, root_path) for name in filenames)
|
|
108
|
+
|
|
109
|
+
return {part_path: frozenset(files) for part_path, files in owned.items()}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _collect_files_for_all(listing: DirListing, root_path: Path) -> frozenset[Path]:
|
|
113
|
+
return frozenset(
|
|
114
|
+
_relativize(dir_path / name, root_path)
|
|
115
|
+
for dir_path, filenames in listing
|
|
116
|
+
for name in filenames
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _find_all_config_files(listing: DirListing) -> frozenset[Path]:
|
|
121
|
+
return frozenset(
|
|
122
|
+
path for dir_path, filenames in listing for path in _find_config_files(dir_path, filenames)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class _BuildContext:
|
|
128
|
+
parent_map: Mapping[Path, tuple[Path, ...]]
|
|
129
|
+
parts_info: Mapping[Path, tuple[frozenset[Path], str | None]]
|
|
130
|
+
root_path: Path
|
|
131
|
+
files_by_part: Mapping[Path, frozenset[Path]]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_flat_parts(
|
|
135
|
+
ctx: _BuildContext,
|
|
136
|
+
) -> tuple[Mapping[str, Part], Mapping[str, tuple[str, ...]]]:
|
|
137
|
+
parts: dict[str, Part] = {}
|
|
138
|
+
children: dict[str, tuple[str, ...]] = {}
|
|
139
|
+
|
|
140
|
+
for part_path, child_paths in ctx.parent_map.items():
|
|
141
|
+
config_files, language = ctx.parts_info.get(part_path, (frozenset(), None))
|
|
142
|
+
name = _generate_part_name(part_path, ctx.root_path)
|
|
143
|
+
parts[name] = Part(
|
|
144
|
+
identity=PartIdentity(name=name, path=_relativize(part_path, ctx.root_path)),
|
|
145
|
+
files=ctx.files_by_part.get(part_path, frozenset()),
|
|
146
|
+
config_files=frozenset(_relativize(path, ctx.root_path) for path in config_files),
|
|
147
|
+
detected_language=language,
|
|
148
|
+
)
|
|
149
|
+
children[name] = tuple(
|
|
150
|
+
_generate_part_name(child, ctx.root_path) for child in sorted(child_paths)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return parts, children
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validate_tree(tree: PartTree) -> None:
|
|
157
|
+
all_parts = tree.get_all_parts()
|
|
158
|
+
|
|
159
|
+
names = Counter(part.name for part in all_parts)
|
|
160
|
+
repeated = sorted(name for name, count in names.items() if count > 1)
|
|
161
|
+
if repeated:
|
|
162
|
+
msg = f"Duplicate part names found: {repeated}"
|
|
163
|
+
raise ValueError(msg)
|
|
164
|
+
|
|
165
|
+
files = Counter(path for part in all_parts for path in part.files)
|
|
166
|
+
shared = sorted(str(path) for path, count in files.items() if count > 1)
|
|
167
|
+
if shared:
|
|
168
|
+
msg = f"Files assigned to multiple parts: {shared[:5]}"
|
|
169
|
+
raise ValueError(msg)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _build_single_root_tree(root_path: Path, listing: DirListing) -> PartTree:
|
|
173
|
+
config_files = _find_all_config_files(listing)
|
|
174
|
+
languages = detect_languages_in_dir(path.name for path in config_files)
|
|
175
|
+
root_part = Part(
|
|
176
|
+
identity=PartIdentity(name=".", path=_ROOT_PATH_MARKER),
|
|
177
|
+
files=_collect_files_for_all(listing, root_path),
|
|
178
|
+
config_files=frozenset(_relativize(path, root_path) for path in config_files),
|
|
179
|
+
detected_language=languages[0].value if languages else None,
|
|
180
|
+
)
|
|
181
|
+
return PartTree(
|
|
182
|
+
root_path=root_path,
|
|
183
|
+
parts={".": root_part},
|
|
184
|
+
children_map={".": ()},
|
|
185
|
+
root_name=".",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def decide_part_tree(root_path: Path, listing: DirListing) -> PartTree:
|
|
190
|
+
potential_parts = _discover_potential_parts(listing)
|
|
191
|
+
if not potential_parts:
|
|
192
|
+
return _build_single_root_tree(root_path, listing)
|
|
193
|
+
|
|
194
|
+
if all(path != root_path for path, _, _ in potential_parts):
|
|
195
|
+
root_files = _root_filenames(listing, root_path)
|
|
196
|
+
anchor = detect_anchor_language(root_files)
|
|
197
|
+
potential_parts = (
|
|
198
|
+
(
|
|
199
|
+
root_path,
|
|
200
|
+
_find_config_files(root_path, root_files),
|
|
201
|
+
anchor.value if anchor else None,
|
|
202
|
+
),
|
|
203
|
+
*potential_parts,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
parent_map = _build_hierarchy(potential_parts, root_path)
|
|
207
|
+
context = _BuildContext(
|
|
208
|
+
parent_map=parent_map,
|
|
209
|
+
parts_info={path: (configs, lang) for path, configs, lang in potential_parts},
|
|
210
|
+
root_path=root_path,
|
|
211
|
+
files_by_part=_assign_files_to_parts(frozenset(parent_map), listing, root_path),
|
|
212
|
+
)
|
|
213
|
+
parts, children_map = _build_flat_parts(context)
|
|
214
|
+
|
|
215
|
+
tree = PartTree(
|
|
216
|
+
root_path=root_path,
|
|
217
|
+
parts=parts,
|
|
218
|
+
children_map=children_map,
|
|
219
|
+
root_name=".",
|
|
220
|
+
)
|
|
221
|
+
_validate_tree(tree)
|
|
222
|
+
return tree
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _is_member_file(path: Path) -> bool:
|
|
226
|
+
try:
|
|
227
|
+
return stat.S_ISREG(path.lstat().st_mode)
|
|
228
|
+
except OSError:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _walk_listing(root_path: Path, exclude_dirs: Collection[str]) -> DirListing:
|
|
233
|
+
listing: list[tuple[Path, tuple[str, ...]]] = []
|
|
234
|
+
for current, dirs, filenames in os.walk(root_path):
|
|
235
|
+
dirs[:] = sorted(d for d in dirs if not any(fnmatch(d, pat) for pat in exclude_dirs))
|
|
236
|
+
directory = Path(current)
|
|
237
|
+
kept = (name for name in filenames if _is_member_file(directory / name))
|
|
238
|
+
listing.append((directory, tuple(sorted(kept))))
|
|
239
|
+
return tuple(listing)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def build_part_tree(root_path: Path, extra_excludes: Sequence[str] | None = None) -> PartTree:
|
|
243
|
+
resolved = Path(root_path).resolve()
|
|
244
|
+
if not resolved.is_dir():
|
|
245
|
+
msg = f"Root path must be a directory: {root_path}"
|
|
246
|
+
raise ValueError(msg)
|
|
247
|
+
|
|
248
|
+
exclusions = (*EXTENDED_EXCLUDE_DIRS, *(extra_excludes or ()))
|
|
249
|
+
listing = _walk_listing(resolved, exclusions)
|
|
250
|
+
if not listing:
|
|
251
|
+
msg = f"Root path could not be read: {root_path}"
|
|
252
|
+
raise OSError(msg)
|
|
253
|
+
|
|
254
|
+
return decide_part_tree(resolved, listing)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
__all__ = ["Part", "PartIdentity", "PartTree"]
|
|
6
|
+
|
|
7
|
+
_SAMPLE_FILE_LIMIT = 10
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class PartIdentity:
|
|
12
|
+
name: str
|
|
13
|
+
path: Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Part:
|
|
18
|
+
identity: PartIdentity
|
|
19
|
+
files: frozenset[Path] = field(default_factory=frozenset)
|
|
20
|
+
config_files: frozenset[Path] = field(default_factory=frozenset)
|
|
21
|
+
detected_language: str | None = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
return self.identity.name
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def path(self) -> Path:
|
|
29
|
+
return self.identity.path
|
|
30
|
+
|
|
31
|
+
def file_count(self) -> int:
|
|
32
|
+
return len(self.files)
|
|
33
|
+
|
|
34
|
+
def all_files(self) -> frozenset[Path]:
|
|
35
|
+
return self.files
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class PartTree:
|
|
40
|
+
root_path: Path
|
|
41
|
+
parts: Mapping[str, Part] = field(default_factory=dict)
|
|
42
|
+
children_map: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
|
|
43
|
+
root_name: str = "."
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def root(self) -> Part | None:
|
|
47
|
+
return self.parts.get(self.root_name)
|
|
48
|
+
|
|
49
|
+
def __len__(self) -> int:
|
|
50
|
+
"""Count every part in the tree, not just the root's children."""
|
|
51
|
+
return len(self.parts)
|
|
52
|
+
|
|
53
|
+
def get_part(self, name: str) -> Part | None:
|
|
54
|
+
return self.parts.get(name)
|
|
55
|
+
|
|
56
|
+
def get_all_parts(self) -> tuple[Part, ...]:
|
|
57
|
+
return self._collect_parts_dfs(self.root_name)
|
|
58
|
+
|
|
59
|
+
def get_part_by_path(self, path: Path) -> Part | None:
|
|
60
|
+
for part in self.parts.values():
|
|
61
|
+
if part.path == path:
|
|
62
|
+
return part
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def get_children(self, part_name: str) -> tuple[Part, ...]:
|
|
66
|
+
child_names = self.children_map.get(part_name, ())
|
|
67
|
+
return tuple(self.parts[name] for name in child_names if name in self.parts)
|
|
68
|
+
|
|
69
|
+
def get_parent(self, part_name: str) -> Part | None:
|
|
70
|
+
for parent_name, child_names in self.children_map.items():
|
|
71
|
+
if part_name in child_names:
|
|
72
|
+
return self.parts.get(parent_name)
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def recursive_file_count(self, part_name: str) -> int:
|
|
76
|
+
part = self.parts.get(part_name)
|
|
77
|
+
if part is None:
|
|
78
|
+
return 0
|
|
79
|
+
count = len(part.files)
|
|
80
|
+
for child_name in self.children_map.get(part_name, ()):
|
|
81
|
+
count += self.recursive_file_count(child_name)
|
|
82
|
+
return count
|
|
83
|
+
|
|
84
|
+
def recursive_all_files(self, part_name: str) -> frozenset[Path]:
|
|
85
|
+
part = self.parts.get(part_name)
|
|
86
|
+
if part is None:
|
|
87
|
+
return frozenset()
|
|
88
|
+
all_files: set[Path] = set(part.files)
|
|
89
|
+
for child_name in self.children_map.get(part_name, ()):
|
|
90
|
+
all_files.update(self.recursive_all_files(child_name))
|
|
91
|
+
return frozenset(all_files)
|
|
92
|
+
|
|
93
|
+
def get_part_context(self, part_name: str) -> str:
|
|
94
|
+
part = self.parts.get(part_name)
|
|
95
|
+
if part is None:
|
|
96
|
+
return ""
|
|
97
|
+
lines = [
|
|
98
|
+
f"# Part: {part.name}",
|
|
99
|
+
f"**Path**: `{part.path}`",
|
|
100
|
+
"",
|
|
101
|
+
]
|
|
102
|
+
if part.detected_language:
|
|
103
|
+
lines.extend([f"**Language**: {part.detected_language}", ""])
|
|
104
|
+
|
|
105
|
+
if part.config_files:
|
|
106
|
+
lines.append("**Config files**:")
|
|
107
|
+
lines.extend(f"- `{config_file}`" for config_file in sorted(part.config_files))
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
children = self.get_children(part_name)
|
|
111
|
+
total_file_count = self.recursive_file_count(part_name)
|
|
112
|
+
|
|
113
|
+
lines.extend(
|
|
114
|
+
[
|
|
115
|
+
f"**Direct files**: {len(part.files)}",
|
|
116
|
+
f"**Total files** (including children): {total_file_count}",
|
|
117
|
+
"",
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if children:
|
|
122
|
+
lines.append("**Child parts**:")
|
|
123
|
+
lines.extend(
|
|
124
|
+
f"- `{child.name}` ({self.recursive_file_count(child.name)} files)"
|
|
125
|
+
for child in children
|
|
126
|
+
)
|
|
127
|
+
lines.append("")
|
|
128
|
+
|
|
129
|
+
sample_files = sorted(part.files)[:_SAMPLE_FILE_LIMIT]
|
|
130
|
+
if sample_files:
|
|
131
|
+
lines.append("**Sample files**:")
|
|
132
|
+
lines.extend(f"- `{f}`" for f in sample_files)
|
|
133
|
+
if len(part.files) > _SAMPLE_FILE_LIMIT:
|
|
134
|
+
lines.append(f"- ... and {len(part.files) - _SAMPLE_FILE_LIMIT} more")
|
|
135
|
+
|
|
136
|
+
return "\n".join(lines)
|
|
137
|
+
|
|
138
|
+
def to_tree_string(self) -> str:
|
|
139
|
+
rendered = self._format_part_recursive(self.root_name, prefix="", is_last=True)
|
|
140
|
+
return "\n".join((f"{self.root_path}", *rendered))
|
|
141
|
+
|
|
142
|
+
def _collect_parts_dfs(self, part_name: str) -> tuple[Part, ...]:
|
|
143
|
+
part = self.parts.get(part_name)
|
|
144
|
+
if part is None:
|
|
145
|
+
return ()
|
|
146
|
+
return (
|
|
147
|
+
part,
|
|
148
|
+
*(
|
|
149
|
+
descendant
|
|
150
|
+
for child_name in self.children_map.get(part_name, ())
|
|
151
|
+
for descendant in self._collect_parts_dfs(child_name)
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _format_part_recursive(
|
|
156
|
+
self, part_name: str, *, prefix: str, is_last: bool
|
|
157
|
+
) -> tuple[str, ...]:
|
|
158
|
+
part = self.parts.get(part_name)
|
|
159
|
+
if part is None:
|
|
160
|
+
return ()
|
|
161
|
+
connector = "└── " if is_last else "├── "
|
|
162
|
+
lang_info = f" [{part.detected_language}]" if part.detected_language else ""
|
|
163
|
+
file_info = f" ({self.recursive_file_count(part_name)} files)"
|
|
164
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
165
|
+
child_names = self.children_map.get(part_name, ())
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
f"{prefix}{connector}{part.name}{lang_info}{file_info}",
|
|
169
|
+
*(
|
|
170
|
+
line
|
|
171
|
+
for index, child_name in enumerate(child_names)
|
|
172
|
+
for line in self._format_part_recursive(
|
|
173
|
+
child_name,
|
|
174
|
+
prefix=child_prefix,
|
|
175
|
+
is_last=index == len(child_names) - 1,
|
|
176
|
+
)
|
|
177
|
+
),
|
|
178
|
+
)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from fnmatch import fnmatch
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .defaults import (
|
|
8
|
+
EXCLUDE_DIRS,
|
|
9
|
+
EXCLUDE_FILES,
|
|
10
|
+
SKIP_DIRS,
|
|
11
|
+
TEST_FILES,
|
|
12
|
+
)
|
|
13
|
+
from .detection import detect_anchor_language, detect_languages_in_dir
|
|
14
|
+
from .language import Language
|
|
15
|
+
|
|
16
|
+
__all__ = ["detect_root_language", "find_parent_project", "find_projects"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _file_names(directory: Path) -> list[str]:
|
|
20
|
+
try:
|
|
21
|
+
entries = list(os.scandir(directory))
|
|
22
|
+
except OSError:
|
|
23
|
+
return []
|
|
24
|
+
return [e.name for e in entries if e.is_file(follow_symlinks=False)]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _optimize_exclusions(exclusions: list[str]) -> list[str]:
|
|
28
|
+
if not exclusions:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
paths = sorted([Path(p) for p in set(exclusions)])
|
|
32
|
+
|
|
33
|
+
optimized: list[Path] = []
|
|
34
|
+
for p in paths:
|
|
35
|
+
if not any(p.is_relative_to(opt_p) for opt_p in optimized):
|
|
36
|
+
optimized.append(p)
|
|
37
|
+
|
|
38
|
+
return sorted([p.as_posix() for p in optimized])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _collect_files(
|
|
42
|
+
entries: list[os.DirEntry[str]],
|
|
43
|
+
root_dir: Path,
|
|
44
|
+
excluded: set[str],
|
|
45
|
+
) -> list[str]:
|
|
46
|
+
files: list[str] = []
|
|
47
|
+
for e in entries:
|
|
48
|
+
if not e.is_file(follow_symlinks=False):
|
|
49
|
+
continue
|
|
50
|
+
name = e.name
|
|
51
|
+
if any(fnmatch(name, pat) for pat in TEST_FILES) or any(
|
|
52
|
+
fnmatch(name, pat) for pat in EXCLUDE_FILES
|
|
53
|
+
):
|
|
54
|
+
excluded.add(Path(e.path).relative_to(root_dir).as_posix())
|
|
55
|
+
continue
|
|
56
|
+
files.append(name)
|
|
57
|
+
return files
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _scan_dir(
|
|
61
|
+
dir_path: Path,
|
|
62
|
+
root_dir: Path,
|
|
63
|
+
excluded_by_skip: set[str],
|
|
64
|
+
) -> list[tuple[Path, Language, list[str]]]:
|
|
65
|
+
try:
|
|
66
|
+
entries = list(os.scandir(dir_path))
|
|
67
|
+
except OSError:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
files = _collect_files(entries, root_dir, excluded_by_skip)
|
|
71
|
+
language = detect_anchor_language(files)
|
|
72
|
+
subprojects: list[tuple[Path, Language, list[str]]] = []
|
|
73
|
+
|
|
74
|
+
for sd in sorted(
|
|
75
|
+
(e for e in entries if e.is_dir(follow_symlinks=False)),
|
|
76
|
+
key=lambda e: e.name,
|
|
77
|
+
):
|
|
78
|
+
rel_sd = Path(sd.path).relative_to(root_dir).as_posix()
|
|
79
|
+
skip_dir = (
|
|
80
|
+
sd.name.startswith(".tox")
|
|
81
|
+
or any(fnmatch(sd.name, pat) for pat in SKIP_DIRS)
|
|
82
|
+
or any(fnmatch(rel_sd, pat) for pat in EXCLUDE_DIRS)
|
|
83
|
+
)
|
|
84
|
+
if skip_dir:
|
|
85
|
+
excluded_by_skip.add(rel_sd)
|
|
86
|
+
continue
|
|
87
|
+
subprojects.extend(_scan_dir(Path(sd.path), root_dir, excluded_by_skip))
|
|
88
|
+
|
|
89
|
+
if language is not None:
|
|
90
|
+
exclusions = _optimize_exclusions(
|
|
91
|
+
[sp[0].relative_to(dir_path).as_posix() for sp in subprojects]
|
|
92
|
+
)
|
|
93
|
+
return [(dir_path, language, exclusions), *subprojects]
|
|
94
|
+
|
|
95
|
+
return subprojects
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _enrich(
|
|
99
|
+
projects: list[tuple[Path, Language, list[str]]],
|
|
100
|
+
root_dir: Path,
|
|
101
|
+
excluded_by_skip: set[str],
|
|
102
|
+
) -> list[tuple[Path, str, list[str]]]:
|
|
103
|
+
enriched: list[tuple[Path, str, list[str]]] = []
|
|
104
|
+
for p_path, p_lang, p_excls in projects:
|
|
105
|
+
extra = [
|
|
106
|
+
(root_dir / rel).relative_to(p_path).as_posix()
|
|
107
|
+
for rel in excluded_by_skip
|
|
108
|
+
if (root_dir / rel).is_relative_to(p_path)
|
|
109
|
+
]
|
|
110
|
+
final_excls = _optimize_exclusions([*p_excls, *extra]) if extra else p_excls
|
|
111
|
+
enriched.append((p_path, p_lang.value, final_excls))
|
|
112
|
+
return enriched
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def find_projects(root_dir: str | Path) -> list[tuple[Path, str, list[str]]]:
|
|
116
|
+
root_dir = Path(root_dir).resolve()
|
|
117
|
+
|
|
118
|
+
excluded_by_skip: set[str] = set()
|
|
119
|
+
scanned = _scan_dir(root_dir, root_dir, excluded_by_skip)
|
|
120
|
+
contained = sorted((p for p in scanned if p[0].is_relative_to(root_dir)), key=lambda p: p[0])
|
|
121
|
+
enriched_projects = _enrich(contained, root_dir, excluded_by_skip)
|
|
122
|
+
|
|
123
|
+
root_exclusions = [p[0].relative_to(root_dir).as_posix() for p in enriched_projects]
|
|
124
|
+
root_exclusions.extend(excluded_by_skip)
|
|
125
|
+
|
|
126
|
+
return [
|
|
127
|
+
(root_dir, "root", _optimize_exclusions(root_exclusions)),
|
|
128
|
+
*enriched_projects,
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def detect_root_language(root_dir: str | Path) -> Language:
|
|
133
|
+
langs = detect_languages_in_dir(_file_names(Path(root_dir)))
|
|
134
|
+
return langs[0] if len(langs) == 1 else Language.Unknown
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def find_parent_project(
|
|
138
|
+
file_path: str | Path,
|
|
139
|
+
base_dir: Path,
|
|
140
|
+
) -> tuple[Path, str] | None:
|
|
141
|
+
base = Path(base_dir).resolve()
|
|
142
|
+
start = Path(file_path).resolve()
|
|
143
|
+
current = start if start.is_dir() else start.parent
|
|
144
|
+
|
|
145
|
+
if not current.is_relative_to(base):
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
while True:
|
|
149
|
+
language = detect_anchor_language(_file_names(current))
|
|
150
|
+
if language is not None:
|
|
151
|
+
return (current, language.value)
|
|
152
|
+
if current in (base, current.parent):
|
|
153
|
+
return None
|
|
154
|
+
current = current.parent
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fluidattacks_core_filesystem
|
|
3
|
+
Version: 12.0.0
|
|
4
|
+
Summary: Fluid Attacks Core Filesystem Library
|
|
5
|
+
Author-email: Development <development@fluidattacks.com>
|
|
6
|
+
License: MPL-2.0
|
|
7
|
+
Classifier: Development Status :: 1 - Planning
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Requires-Python: >=3.11
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
fluidattacks_core/filesystem/__init__.py,sha256=g7DDWi0WEYVmNFPu2ZSqNy2--gQzELpvwPdZH2_hG14,498
|
|
2
|
+
fluidattacks_core/filesystem/defaults.py,sha256=vu9XB4La9bjVRYaRyckmUh731DojJml1udvRu5oa0nU,11740
|
|
3
|
+
fluidattacks_core/filesystem/detection.py,sha256=ave7Y67w2FYTLzCEM5UcFzT2sdaqQ1vc3jc4A84zcoY,3607
|
|
4
|
+
fluidattacks_core/filesystem/language.py,sha256=B_ESvmw56rjQw4C-e3Xk4j-iT_eBI-89FyAtZV7ZOeQ,592
|
|
5
|
+
fluidattacks_core/filesystem/partitioning.py,sha256=Q0rHNO1miDj1u3yYnbFZ5-FHxCso5XTxCmdiAlQsWBk,8468
|
|
6
|
+
fluidattacks_core/filesystem/parts.py,sha256=meyUBR9HdGLr3rGihRcF7J8KcBslC2cuvTi1-3kV7Lk,5838
|
|
7
|
+
fluidattacks_core/filesystem/projects.py,sha256=VTbSZQzE7889_j6_JFomB-nk4ecLWWCOoMlq89NTx6Q,4671
|
|
8
|
+
fluidattacks_core/filesystem/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
fluidattacks_core_filesystem-12.0.0.dist-info/METADATA,sha256=Q6FkoLFxZgt9cHFYXTSGL8OBSk3MCXGl1FqEluALdTU,539
|
|
10
|
+
fluidattacks_core_filesystem-12.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
11
|
+
fluidattacks_core_filesystem-12.0.0.dist-info/RECORD,,
|