forceupdate 0.1.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.
- forceupdate/__init__.py +9 -0
- forceupdate/__main__.py +5 -0
- forceupdate/advisories/__init__.py +30 -0
- forceupdate/advisories/cvss.py +95 -0
- forceupdate/advisories/osv.py +584 -0
- forceupdate/checks/__init__.py +49 -0
- forceupdate/checks/base.py +98 -0
- forceupdate/checks/deprecated.py +115 -0
- forceupdate/checks/outdated.py +183 -0
- forceupdate/checks/priority.py +82 -0
- forceupdate/checks/vulns.py +201 -0
- forceupdate/cli.py +309 -0
- forceupdate/detect/__init__.py +5 -0
- forceupdate/detect/languages.py +266 -0
- forceupdate/detect/manifests.py +478 -0
- forceupdate/engine.py +313 -0
- forceupdate/models.py +510 -0
- forceupdate/net/__init__.py +20 -0
- forceupdate/net/cache.py +217 -0
- forceupdate/net/http.py +270 -0
- forceupdate/parsers/__init__.py +43 -0
- forceupdate/parsers/base.py +22 -0
- forceupdate/parsers/json_spans.py +123 -0
- forceupdate/parsers/lockfiles.py +231 -0
- forceupdate/parsers/npm.py +1257 -0
- forceupdate/parsers/python.py +1421 -0
- forceupdate/parsers/toml_spans.py +297 -0
- forceupdate/registries/__init__.py +52 -0
- forceupdate/registries/base.py +294 -0
- forceupdate/registries/npm.py +153 -0
- forceupdate/registries/pypi.py +242 -0
- forceupdate/report/__init__.py +359 -0
- forceupdate/report/json.py +34 -0
- forceupdate/report/markdown.py +169 -0
- forceupdate/report/terminal.py +376 -0
- forceupdate/sources/__init__.py +1 -0
- forceupdate/sources/base.py +45 -0
- forceupdate/sources/local.py +183 -0
- forceupdate/update/__init__.py +8 -0
- forceupdate/update/constraints.py +317 -0
- forceupdate/update/planner.py +531 -0
- forceupdate/versions/__init__.py +40 -0
- forceupdate/versions/base.py +229 -0
- forceupdate/versions/npm.py +432 -0
- forceupdate/versions/pypi.py +220 -0
- forceupdate-0.1.0.dist-info/METADATA +79 -0
- forceupdate-0.1.0.dist-info/RECORD +50 -0
- forceupdate-0.1.0.dist-info/WHEEL +4 -0
- forceupdate-0.1.0.dist-info/entry_points.txt +2 -0
- forceupdate-0.1.0.dist-info/licenses/LICENSE +21 -0
forceupdate/cli.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""Ligne de commande : ``forceupdate ui``, ``scan`` et ``update``.
|
|
2
|
+
|
|
3
|
+
Pas de logique métier ici : chaque commande appelle le moteur, et les rapports (``report/``)
|
|
4
|
+
mettent le résultat en forme. Codes de retour : 0 rien à signaler, 1 seuil ``--fail-on``
|
|
5
|
+
atteint, 2 erreur.
|
|
6
|
+
|
|
7
|
+
Le JSON et le Markdown sortent toujours en UTF-8, sur la sortie standard comme dans un fichier.
|
|
8
|
+
Sous Windows, une sortie redirigée serait sinon écrite dans le codage de la console (cp1252) :
|
|
9
|
+
la sortie standard redirigée est donc repassée en UTF-8 (``_utf8_stdout``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import enum
|
|
13
|
+
import io
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Annotated, NoReturn
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
from rich.progress import BarColumn, MofNCompleteColumn, Progress, SpinnerColumn, TextColumn
|
|
21
|
+
|
|
22
|
+
from forceupdate import __version__, checks, engine
|
|
23
|
+
from forceupdate.checks import FailOn
|
|
24
|
+
from forceupdate.models import Ecosystem, Event, EventKind, Gap, ScanOptions
|
|
25
|
+
from forceupdate.report import json as json_report
|
|
26
|
+
from forceupdate.report import markdown, terminal
|
|
27
|
+
from forceupdate.sources.base import SourceError
|
|
28
|
+
from forceupdate.sources.local import LocalSource
|
|
29
|
+
from forceupdate.update import planner
|
|
30
|
+
|
|
31
|
+
EXIT_FAIL_ON = 1
|
|
32
|
+
EXIT_ERROR = 2
|
|
33
|
+
|
|
34
|
+
FILE_WIDTH = 120
|
|
35
|
+
"""Largeur du rapport terminal écrit dans un fichier (``--output``)."""
|
|
36
|
+
|
|
37
|
+
app = typer.Typer(
|
|
38
|
+
name="forceupdate",
|
|
39
|
+
help="Trouve les dépendances à mettre à jour, et fait les mises à jour.",
|
|
40
|
+
no_args_is_help=True,
|
|
41
|
+
add_completion=False,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
PathArgument = Annotated[
|
|
45
|
+
Path,
|
|
46
|
+
typer.Argument(help="Dossier ou fichier à traiter (par défaut, le dossier courant)."),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class OutputFormat(enum.StrEnum):
|
|
51
|
+
TERMINAL = "terminal"
|
|
52
|
+
JSON = "json"
|
|
53
|
+
MD = "md"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _print_version(value: bool) -> None:
|
|
57
|
+
if value:
|
|
58
|
+
typer.echo(f"forceupdate {__version__}")
|
|
59
|
+
raise typer.Exit()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@app.callback()
|
|
63
|
+
def main(
|
|
64
|
+
version: Annotated[
|
|
65
|
+
bool,
|
|
66
|
+
typer.Option(
|
|
67
|
+
"--version",
|
|
68
|
+
callback=_print_version,
|
|
69
|
+
is_eager=True,
|
|
70
|
+
help="Affiche la version et quitte.",
|
|
71
|
+
),
|
|
72
|
+
] = False,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Trouve les dépendances à mettre à jour, et fait les mises à jour."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _fail(command: str, message: str) -> NoReturn:
|
|
78
|
+
"""Affiche l'erreur sur stderr, et sort avec le code d'erreur."""
|
|
79
|
+
typer.echo(f"forceupdate {command} : {message}", err=True)
|
|
80
|
+
raise typer.Exit(code=EXIT_ERROR)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _not_available(command: str) -> NoReturn:
|
|
84
|
+
"""Commande prévue au plan mais pas encore écrite : le dire, et sortir en erreur."""
|
|
85
|
+
_fail(command, "pas encore disponible.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command()
|
|
89
|
+
def ui() -> None:
|
|
90
|
+
"""Ouvre l'interface web dans le navigateur."""
|
|
91
|
+
_not_available("ui")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.command()
|
|
95
|
+
def scan(
|
|
96
|
+
path: PathArgument = Path("."),
|
|
97
|
+
output_format: Annotated[
|
|
98
|
+
OutputFormat,
|
|
99
|
+
typer.Option("--format", "-f", help="Forme du rapport : terminal, json ou md."),
|
|
100
|
+
] = OutputFormat.TERMINAL,
|
|
101
|
+
output: Annotated[
|
|
102
|
+
Path | None,
|
|
103
|
+
typer.Option(
|
|
104
|
+
"--output",
|
|
105
|
+
"-o",
|
|
106
|
+
dir_okay=False,
|
|
107
|
+
help="Écrit le rapport dans ce fichier (UTF-8) au lieu de la sortie standard.",
|
|
108
|
+
),
|
|
109
|
+
] = None,
|
|
110
|
+
dev: Annotated[
|
|
111
|
+
bool,
|
|
112
|
+
typer.Option(
|
|
113
|
+
"--dev/--no-dev", help="Vérifie aussi les dépendances de développement (par défaut)."
|
|
114
|
+
),
|
|
115
|
+
] = True,
|
|
116
|
+
transitive: Annotated[
|
|
117
|
+
bool,
|
|
118
|
+
typer.Option(
|
|
119
|
+
"--transitive",
|
|
120
|
+
help=(
|
|
121
|
+
"Cherche aussi les versions plus récentes, retirées ou dépréciées des "
|
|
122
|
+
"dépendances indirectes (leurs failles sont toujours cherchées)."
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
] = False,
|
|
126
|
+
offline: Annotated[
|
|
127
|
+
bool,
|
|
128
|
+
typer.Option(
|
|
129
|
+
"--offline",
|
|
130
|
+
help="N'interroge pas les registres : seulement le cache, le reste « non concluant ».",
|
|
131
|
+
),
|
|
132
|
+
] = False,
|
|
133
|
+
no_cache: Annotated[
|
|
134
|
+
bool,
|
|
135
|
+
typer.Option("--no-cache", help="Ne lit ni n'écrit le cache disque."),
|
|
136
|
+
] = False,
|
|
137
|
+
fail_on: Annotated[
|
|
138
|
+
FailOn | None,
|
|
139
|
+
typer.Option(
|
|
140
|
+
"--fail-on",
|
|
141
|
+
help=(
|
|
142
|
+
"Sort avec le code 1 si un constat atteint ce seuil : vuln (une faille), major "
|
|
143
|
+
"(faille, fin de vie, retirée, dépréciée ou majeure), any (tout ce qui est à "
|
|
144
|
+
"mettre à jour)."
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
] = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Analyse un dossier ou un fichier : langages, dépendances, ce qui est à mettre à jour."""
|
|
150
|
+
try:
|
|
151
|
+
source = LocalSource(path)
|
|
152
|
+
except SourceError as err:
|
|
153
|
+
_fail("scan", f"{err}.")
|
|
154
|
+
options = ScanOptions(dev=dev, transitive=transitive, offline=offline, cache=not no_cache)
|
|
155
|
+
with _ProgressBar(Console(stderr=True)) as progress:
|
|
156
|
+
report = engine.scan(source, options, on_event=progress)
|
|
157
|
+
|
|
158
|
+
if output_format is OutputFormat.JSON:
|
|
159
|
+
text = json_report.render(report)
|
|
160
|
+
elif output_format is OutputFormat.MD:
|
|
161
|
+
text = markdown.render(report)
|
|
162
|
+
elif output is not None:
|
|
163
|
+
buffer = io.StringIO()
|
|
164
|
+
terminal.render(report, Console(file=buffer, width=FILE_WIDTH, color_system=None))
|
|
165
|
+
text = buffer.getvalue()
|
|
166
|
+
else:
|
|
167
|
+
_utf8_stdout()
|
|
168
|
+
terminal.render(report, Console(highlight=False))
|
|
169
|
+
text = None
|
|
170
|
+
|
|
171
|
+
if text is not None:
|
|
172
|
+
if output is None:
|
|
173
|
+
_utf8_stdout()
|
|
174
|
+
sys.stdout.write(text)
|
|
175
|
+
sys.stdout.flush()
|
|
176
|
+
else:
|
|
177
|
+
try:
|
|
178
|
+
output.write_text(text, encoding="utf-8", newline="\n")
|
|
179
|
+
except OSError as err:
|
|
180
|
+
_fail("scan", f"impossible d'écrire « {output} » : {err.strerror or err}.")
|
|
181
|
+
|
|
182
|
+
if fail_on is not None and checks.fails(report.findings, fail_on):
|
|
183
|
+
raise typer.Exit(code=EXIT_FAIL_ON)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
def update(
|
|
188
|
+
path: PathArgument = Path("."),
|
|
189
|
+
level: Annotated[
|
|
190
|
+
Gap,
|
|
191
|
+
typer.Option(
|
|
192
|
+
"--level",
|
|
193
|
+
case_sensitive=False,
|
|
194
|
+
help=(
|
|
195
|
+
"Écart le plus grand choisi : patch (correctifs), minor (et mineures, par "
|
|
196
|
+
"défaut), major (et majeures, qui peuvent demander d'adapter le code)."
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
] = Gap.MINOR,
|
|
200
|
+
security_only: Annotated[
|
|
201
|
+
bool,
|
|
202
|
+
typer.Option("--security-only", help="Choisit seulement les corrections de failles."),
|
|
203
|
+
] = False,
|
|
204
|
+
include: Annotated[
|
|
205
|
+
list[str] | None,
|
|
206
|
+
typer.Option(
|
|
207
|
+
"--include",
|
|
208
|
+
help="Ne choisit que les paquets dont le nom correspond (« * » permis ; répétable).",
|
|
209
|
+
),
|
|
210
|
+
] = None,
|
|
211
|
+
exclude: Annotated[
|
|
212
|
+
list[str] | None,
|
|
213
|
+
typer.Option(
|
|
214
|
+
"--exclude",
|
|
215
|
+
help="Écarte les paquets dont le nom correspond (« * » permis ; répétable).",
|
|
216
|
+
),
|
|
217
|
+
] = None,
|
|
218
|
+
ecosystem: Annotated[
|
|
219
|
+
list[Ecosystem] | None,
|
|
220
|
+
typer.Option(
|
|
221
|
+
"--ecosystem",
|
|
222
|
+
case_sensitive=False,
|
|
223
|
+
help="Ne choisit que les paquets de cet écosystème (répétable).",
|
|
224
|
+
),
|
|
225
|
+
] = None,
|
|
226
|
+
dev: Annotated[
|
|
227
|
+
bool,
|
|
228
|
+
typer.Option(
|
|
229
|
+
"--dev/--no-dev",
|
|
230
|
+
help="Met à jour aussi les dépendances de développement (par défaut).",
|
|
231
|
+
),
|
|
232
|
+
] = True,
|
|
233
|
+
offline: Annotated[
|
|
234
|
+
bool,
|
|
235
|
+
typer.Option(
|
|
236
|
+
"--offline",
|
|
237
|
+
help="N'interroge pas les registres : seulement le cache, le reste « non concluant ».",
|
|
238
|
+
),
|
|
239
|
+
] = False,
|
|
240
|
+
no_cache: Annotated[
|
|
241
|
+
bool,
|
|
242
|
+
typer.Option("--no-cache", help="Ne lit ni n'écrit le cache disque."),
|
|
243
|
+
] = False,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Met à jour les dépendances d'un dossier ou d'un fichier.
|
|
246
|
+
|
|
247
|
+
Pour l'instant, montre les mises à jour choisies, sans rien écrire.
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
source = LocalSource(path)
|
|
251
|
+
except SourceError as err:
|
|
252
|
+
_fail("update", f"{err}.")
|
|
253
|
+
options = ScanOptions(dev=dev, offline=offline, cache=not no_cache)
|
|
254
|
+
with _ProgressBar(Console(stderr=True)) as progress:
|
|
255
|
+
report = engine.scan(source, options, on_event=progress)
|
|
256
|
+
rules = planner.Rules(
|
|
257
|
+
level=level,
|
|
258
|
+
security_only=security_only,
|
|
259
|
+
include=tuple(include or ()),
|
|
260
|
+
exclude=tuple(exclude or ()),
|
|
261
|
+
ecosystems=frozenset(ecosystem or ()),
|
|
262
|
+
)
|
|
263
|
+
_utf8_stdout()
|
|
264
|
+
terminal.render_plan(report, planner.select(report, rules), Console(highlight=False), rules)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _utf8_stdout() -> None:
|
|
268
|
+
"""Passe la sortie standard en UTF-8 si elle est redirigée (un fichier, un tube). Dans une
|
|
269
|
+
console, Python écrit déjà de l'Unicode, quel que soit le codage de la console."""
|
|
270
|
+
stdout = sys.stdout
|
|
271
|
+
if stdout.isatty() or not isinstance(stdout, io.TextIOWrapper):
|
|
272
|
+
return
|
|
273
|
+
if stdout.encoding.lower().replace("-", "") != "utf8":
|
|
274
|
+
stdout.reconfigure(encoding="utf-8")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class _ProgressBar:
|
|
278
|
+
"""Affiche les événements du moteur en barre de progression, sur la sortie d'erreur, et
|
|
279
|
+
seulement dans une console : rien ne s'écrit dans une sortie redirigée."""
|
|
280
|
+
|
|
281
|
+
def __init__(self, console: Console) -> None:
|
|
282
|
+
self._progress = Progress(
|
|
283
|
+
SpinnerColumn(),
|
|
284
|
+
TextColumn("{task.description}"),
|
|
285
|
+
BarColumn(),
|
|
286
|
+
MofNCompleteColumn(),
|
|
287
|
+
TextColumn("{task.fields[name]}", style="dim"),
|
|
288
|
+
console=console,
|
|
289
|
+
transient=True,
|
|
290
|
+
disable=not console.is_terminal,
|
|
291
|
+
)
|
|
292
|
+
self._task = self._progress.add_task("Lecture des projets", total=None, name="")
|
|
293
|
+
|
|
294
|
+
def __enter__(self) -> "_ProgressBar":
|
|
295
|
+
self._progress.start()
|
|
296
|
+
return self
|
|
297
|
+
|
|
298
|
+
def __exit__(self, *_: object) -> None:
|
|
299
|
+
self._progress.stop()
|
|
300
|
+
|
|
301
|
+
def __call__(self, event: Event) -> None:
|
|
302
|
+
if event.kind is EventKind.CHECKING:
|
|
303
|
+
self._progress.update(
|
|
304
|
+
self._task, description="Vérification des paquets", total=event.total, completed=0
|
|
305
|
+
)
|
|
306
|
+
elif event.kind is EventKind.PACKAGE_CHECKED:
|
|
307
|
+
self._progress.update(self._task, completed=event.done, name=event.name or "")
|
|
308
|
+
elif event.kind is EventKind.VULNERABILITIES_CHECKED:
|
|
309
|
+
self._progress.update(self._task, completed=event.done, name="failles (OSV)")
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Les langages d'une source, et leur poids en octets, comme GitHub Linguist.
|
|
2
|
+
|
|
3
|
+
Le détecteur travaille sur une liste de ``(chemin, taille)``, sans lire les
|
|
4
|
+
fichiers : il sert tel quel à un dossier local et à l'arborescence d'un dépôt sur
|
|
5
|
+
une forge (E17).
|
|
6
|
+
|
|
7
|
+
Comme Linguist :
|
|
8
|
+
|
|
9
|
+
- le langage d'un fichier vient de son extension, ou de son nom (``Dockerfile``) ;
|
|
10
|
+
- seuls comptent les langages de programmation et de balisage. JSON, YAML, TOML ou
|
|
11
|
+
Markdown sont des données ou de la prose : ils ne comptent pas ;
|
|
12
|
+
- les fichiers vendorisés (``vendor/``, ``*.min.js``), générés (``*.pb.go``) et la
|
|
13
|
+
documentation (``docs/``) sont écartés.
|
|
14
|
+
|
|
15
|
+
Linguist lit aussi le contenu des fichiers et ``.gitattributes`` pour lever les
|
|
16
|
+
ambiguïtés. Ici on s'en tient au chemin : un ``.h`` va au C, ou au C++ (à
|
|
17
|
+
l'Objective-C) quand la source n'a pas de C.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from collections import Counter
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
|
|
24
|
+
from forceupdate.models import Ecosystem, Language
|
|
25
|
+
|
|
26
|
+
_EXTENSIONS: dict[str, str | None] = {
|
|
27
|
+
".py": "Python",
|
|
28
|
+
".pyw": "Python",
|
|
29
|
+
".pyi": "Python",
|
|
30
|
+
".pyx": "Cython",
|
|
31
|
+
".ipynb": "Jupyter Notebook",
|
|
32
|
+
".js": "JavaScript",
|
|
33
|
+
".mjs": "JavaScript",
|
|
34
|
+
".cjs": "JavaScript",
|
|
35
|
+
".jsx": "JavaScript",
|
|
36
|
+
".ts": "TypeScript",
|
|
37
|
+
".mts": "TypeScript",
|
|
38
|
+
".cts": "TypeScript",
|
|
39
|
+
".tsx": "TypeScript",
|
|
40
|
+
".coffee": "CoffeeScript",
|
|
41
|
+
".vue": "Vue",
|
|
42
|
+
".svelte": "Svelte",
|
|
43
|
+
".astro": "Astro",
|
|
44
|
+
".html": "HTML",
|
|
45
|
+
".htm": "HTML",
|
|
46
|
+
".xhtml": "HTML",
|
|
47
|
+
".css": "CSS",
|
|
48
|
+
".scss": "SCSS",
|
|
49
|
+
".sass": "Sass",
|
|
50
|
+
".less": "Less",
|
|
51
|
+
".java": "Java",
|
|
52
|
+
".kt": "Kotlin",
|
|
53
|
+
".kts": "Kotlin",
|
|
54
|
+
".gradle": None,
|
|
55
|
+
".gradle.kts": None,
|
|
56
|
+
".scala": "Scala",
|
|
57
|
+
".sc": "Scala",
|
|
58
|
+
".groovy": "Groovy",
|
|
59
|
+
".clj": "Clojure",
|
|
60
|
+
".cljs": "Clojure",
|
|
61
|
+
".cljc": "Clojure",
|
|
62
|
+
".cs": "C#",
|
|
63
|
+
".csx": "C#",
|
|
64
|
+
".fs": "F#",
|
|
65
|
+
".fsx": "F#",
|
|
66
|
+
".fsi": "F#",
|
|
67
|
+
".vb": "Visual Basic .NET",
|
|
68
|
+
".go": "Go",
|
|
69
|
+
".rs": "Rust",
|
|
70
|
+
".c": "C",
|
|
71
|
+
".h": "C",
|
|
72
|
+
".cpp": "C++",
|
|
73
|
+
".cc": "C++",
|
|
74
|
+
".cxx": "C++",
|
|
75
|
+
".c++": "C++",
|
|
76
|
+
".hpp": "C++",
|
|
77
|
+
".hh": "C++",
|
|
78
|
+
".hxx": "C++",
|
|
79
|
+
".h++": "C++",
|
|
80
|
+
".ino": "C++",
|
|
81
|
+
".cu": "Cuda",
|
|
82
|
+
".cuh": "Cuda",
|
|
83
|
+
".m": "Objective-C",
|
|
84
|
+
".mm": "Objective-C++",
|
|
85
|
+
".swift": "Swift",
|
|
86
|
+
".rb": "Ruby",
|
|
87
|
+
".rake": "Ruby",
|
|
88
|
+
".gemspec": "Ruby",
|
|
89
|
+
".php": "PHP",
|
|
90
|
+
".phtml": "PHP",
|
|
91
|
+
".blade.php": "Blade",
|
|
92
|
+
".pl": "Perl",
|
|
93
|
+
".pm": "Perl",
|
|
94
|
+
".lua": "Lua",
|
|
95
|
+
".r": "R",
|
|
96
|
+
".dart": "Dart",
|
|
97
|
+
".ex": "Elixir",
|
|
98
|
+
".exs": "Elixir",
|
|
99
|
+
".erl": "Erlang",
|
|
100
|
+
".hrl": "Erlang",
|
|
101
|
+
".hs": "Haskell",
|
|
102
|
+
".lhs": "Haskell",
|
|
103
|
+
".ml": "OCaml",
|
|
104
|
+
".mli": "OCaml",
|
|
105
|
+
".jl": "Julia",
|
|
106
|
+
".zig": "Zig",
|
|
107
|
+
".nim": "Nim",
|
|
108
|
+
".elm": "Elm",
|
|
109
|
+
".cr": "Crystal",
|
|
110
|
+
".sol": "Solidity",
|
|
111
|
+
".asm": "Assembly",
|
|
112
|
+
".nasm": "Assembly",
|
|
113
|
+
".sh": "Shell",
|
|
114
|
+
".bash": "Shell",
|
|
115
|
+
".zsh": "Shell",
|
|
116
|
+
".ksh": "Shell",
|
|
117
|
+
".ps1": "PowerShell",
|
|
118
|
+
".psm1": "PowerShell",
|
|
119
|
+
".psd1": "PowerShell",
|
|
120
|
+
".bat": "Batchfile",
|
|
121
|
+
".cmd": "Batchfile",
|
|
122
|
+
".mk": "Makefile",
|
|
123
|
+
".mak": "Makefile",
|
|
124
|
+
".cmake": "CMake",
|
|
125
|
+
".dockerfile": "Dockerfile",
|
|
126
|
+
".containerfile": "Dockerfile",
|
|
127
|
+
".tf": "HCL",
|
|
128
|
+
".tfvars": "HCL",
|
|
129
|
+
".hcl": "HCL",
|
|
130
|
+
".nix": "Nix",
|
|
131
|
+
".tex": "TeX",
|
|
132
|
+
}
|
|
133
|
+
"""Extension (en minuscules) → langage. L'extension la plus longue l'emporte :
|
|
134
|
+
``.blade.php`` avant ``.php``. ``None`` : un format de données pour Linguist
|
|
135
|
+
(``build.gradle``), qui ne compte pas."""
|
|
136
|
+
|
|
137
|
+
_FILENAMES: dict[str, str] = {
|
|
138
|
+
"dockerfile": "Dockerfile",
|
|
139
|
+
"containerfile": "Dockerfile",
|
|
140
|
+
"makefile": "Makefile",
|
|
141
|
+
"gnumakefile": "Makefile",
|
|
142
|
+
"cmakelists.txt": "CMake",
|
|
143
|
+
"gemfile": "Ruby",
|
|
144
|
+
"rakefile": "Ruby",
|
|
145
|
+
"vagrantfile": "Ruby",
|
|
146
|
+
}
|
|
147
|
+
"""Nom de fichier (en minuscules) → langage, pour les fichiers sans extension parlante."""
|
|
148
|
+
|
|
149
|
+
_ECOSYSTEMS: dict[str, Ecosystem] = {
|
|
150
|
+
"Python": Ecosystem.PYPI,
|
|
151
|
+
"Cython": Ecosystem.PYPI,
|
|
152
|
+
"Jupyter Notebook": Ecosystem.PYPI,
|
|
153
|
+
"JavaScript": Ecosystem.NPM,
|
|
154
|
+
"TypeScript": Ecosystem.NPM,
|
|
155
|
+
"CoffeeScript": Ecosystem.NPM,
|
|
156
|
+
"Vue": Ecosystem.NPM,
|
|
157
|
+
"Svelte": Ecosystem.NPM,
|
|
158
|
+
"Astro": Ecosystem.NPM,
|
|
159
|
+
"Rust": Ecosystem.CRATES_IO,
|
|
160
|
+
"Go": Ecosystem.GO,
|
|
161
|
+
"PHP": Ecosystem.PACKAGIST,
|
|
162
|
+
"Blade": Ecosystem.PACKAGIST,
|
|
163
|
+
"Ruby": Ecosystem.RUBYGEMS,
|
|
164
|
+
"Java": Ecosystem.MAVEN,
|
|
165
|
+
"Kotlin": Ecosystem.MAVEN,
|
|
166
|
+
"Scala": Ecosystem.MAVEN,
|
|
167
|
+
"Groovy": Ecosystem.MAVEN,
|
|
168
|
+
"Clojure": Ecosystem.MAVEN,
|
|
169
|
+
"C#": Ecosystem.NUGET,
|
|
170
|
+
"F#": Ecosystem.NUGET,
|
|
171
|
+
"Visual Basic .NET": Ecosystem.NUGET,
|
|
172
|
+
}
|
|
173
|
+
"""Langage → écosystème de paquets. Un langage absent n'a pas de gestionnaire de
|
|
174
|
+
paquets que forceupdate sait lire (C, C++, Swift, Shell, HTML…)."""
|
|
175
|
+
|
|
176
|
+
_VENDORED = re.compile(
|
|
177
|
+
r"""
|
|
178
|
+
(^|/)(node_modules|bower_components|jspm_packages|vendors?|extern(als?)?
|
|
179
|
+
|third[-_]?party|3rd[-_]?party|Pods|Carthage|Godeps/_workspace|\.yarn)/
|
|
180
|
+
| (^|/)(dist|cache|\.github|gradle/wrapper|\.mvn/wrapper)/
|
|
181
|
+
| ^(deps|dependencies|debian)/
|
|
182
|
+
| (^|/)(gradlew|gradlew\.bat|mvnw|mvnw\.cmd|configure|config\.guess|config\.sub)$
|
|
183
|
+
| [.-]min\.(js|css)$
|
|
184
|
+
| (^|/)jquery[^/]*\.js$
|
|
185
|
+
| (^|/)bootstrap[^/]*\.(js|css)$
|
|
186
|
+
""",
|
|
187
|
+
re.VERBOSE | re.IGNORECASE,
|
|
188
|
+
)
|
|
189
|
+
"""Code de tiers recopié dans la source : les règles de ``vendor.yml`` de Linguist
|
|
190
|
+
qui se lisent sur le chemin."""
|
|
191
|
+
|
|
192
|
+
_GENERATED = re.compile(
|
|
193
|
+
r"""
|
|
194
|
+
(^|/)__generated__/
|
|
195
|
+
| \.pb\.(go|cc|h)$
|
|
196
|
+
| _pb2(_grpc)?\.pyi?$
|
|
197
|
+
| \.designer\.(cs|vb)$
|
|
198
|
+
| \.(g|freezed)\.dart$
|
|
199
|
+
""",
|
|
200
|
+
re.VERBOSE | re.IGNORECASE,
|
|
201
|
+
)
|
|
202
|
+
"""Code produit par un outil (protobuf, concepteurs .NET, build_runner Dart) : les
|
|
203
|
+
règles de ``generated.rb`` de Linguist qui se lisent sur le chemin."""
|
|
204
|
+
|
|
205
|
+
_DOCUMENTATION = re.compile(
|
|
206
|
+
r"^(docs?|examples|demos?|samples?|man)/|(^|/)(documentation|javadoc|groovydoc)/",
|
|
207
|
+
re.IGNORECASE,
|
|
208
|
+
)
|
|
209
|
+
"""Documentation et exemples, comme ``documentation.yml`` de Linguist : ``docs/``,
|
|
210
|
+
``examples/`` comptent à la racine seulement, ``documentation/`` partout."""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def language_for(path: str) -> str | None:
|
|
214
|
+
"""Le langage d'un fichier d'après son chemin POSIX ; ``None`` s'il ne compte pas."""
|
|
215
|
+
name = path.rsplit("/", 1)[-1].lower()
|
|
216
|
+
if name in _FILENAMES:
|
|
217
|
+
return _FILENAMES[name]
|
|
218
|
+
if name.startswith(("dockerfile.", "containerfile.")):
|
|
219
|
+
return "Dockerfile"
|
|
220
|
+
# L'extension la plus longue d'abord : « a.blade.php » essaie « .blade.php », puis « .php ».
|
|
221
|
+
for index, char in enumerate(name):
|
|
222
|
+
if char == "." and index > 0 and name[index:] in _EXTENSIONS:
|
|
223
|
+
return _EXTENSIONS[name[index:]]
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def is_excluded(path: str) -> bool:
|
|
228
|
+
"""Vrai si le fichier ne compte pas dans les langages : vendorisé, généré ou documentation."""
|
|
229
|
+
return bool(_VENDORED.search(path) or _GENERATED.search(path) or _DOCUMENTATION.search(path))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def ecosystem_for_language(name: str) -> Ecosystem | None:
|
|
233
|
+
"""L'écosystème de paquets d'un langage ; ``None`` s'il n'en a pas de reconnu."""
|
|
234
|
+
return _ECOSYSTEMS.get(name)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def detect_languages(files: Iterable[tuple[str, int]]) -> list[Language]:
|
|
238
|
+
"""Les langages de ``files``, une liste de ``(chemin POSIX, taille en octets)``.
|
|
239
|
+
|
|
240
|
+
Triés du plus lourd au plus léger. Un langage qui pèse 0 octet n'apparaît pas.
|
|
241
|
+
"""
|
|
242
|
+
sizes: Counter[str] = Counter()
|
|
243
|
+
headers = 0
|
|
244
|
+
for path, size in files:
|
|
245
|
+
language = language_for(path)
|
|
246
|
+
if language is None or is_excluded(path):
|
|
247
|
+
continue
|
|
248
|
+
if path.lower().endswith(".h"):
|
|
249
|
+
headers += size
|
|
250
|
+
else:
|
|
251
|
+
sizes[language] += size
|
|
252
|
+
if headers:
|
|
253
|
+
sizes[_header_language(sizes)] += headers
|
|
254
|
+
return [
|
|
255
|
+
Language(name=name, bytes=size, ecosystem=ecosystem_for_language(name))
|
|
256
|
+
for name, size in sorted(sizes.items(), key=lambda item: (-item[1], item[0]))
|
|
257
|
+
if size > 0
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _header_language(sizes: Counter[str]) -> str:
|
|
262
|
+
"""À qui vont les ``.h`` : au C s'il y en a, sinon au C++ ou à l'Objective-C."""
|
|
263
|
+
for language in ("C", "C++", "Objective-C"):
|
|
264
|
+
if sizes[language]:
|
|
265
|
+
return language
|
|
266
|
+
return "C"
|