Building a linter
Contents
Building a linter#
Dieses Tutorial zeigt, wie du TOML Fortran zum Erstellen eines Linters für deine Konfigurationsdateien verwenden kannst. Linter bieten eine Möglichkeit, einen bestimmten Stil zu bevorzugen oder die häufig gemachten Fehler zu finden.
Zielauswahl#
Dieses Tutorial wird auf die Suche nach Lint in dem Paketmanifest des Fortran-Paketmanagers (fpm) eingehen. Wir werden den Plugin-Mechanismus des Fortran-Paketmanagers verwenden, um ein neues Unterprogramm mit dem Namen lint
zu erstellen.
Wir beginnen mit der Einrichtung des Paketmanifests für unseren Linter:
name = "fpm-lint"
version = "0.1.0"
[dependencies]
toml-f.git = "https://github.com/toml-lang/toml-f.git"
Konfiguration des Linters#
Um den Linter zu konfigurieren, werden wir die extra-Sektion im Manifest verwenden, welche speziell für Tools, die mit fpm integriert werden, reserviert ist und extra.fpm.lint als Konfigurationssektion verwendet. Durch das Paketmanifest erhalten wir zwei Vorteile, erstens ist dieses Dokument in allen Projekten, die fpm verwenden, vorhanden, zweitens wenn wir unsere Konfiguration aus dem Manifest lesen können, ist sichergestellt, dass sie gültiges TOML ist.
# ...
[extra.fpm.lint]
package-name = true
bare-keys = true
Als nächstes setzen wir das Hauptprogramm, um den Linter zu starten.
program main
use, intrinsic :: iso_fortran_env, only : stderr => error_unit, stdout => output_unit
use fpm_lint_utils, only : get_argument
use tomlf, only : toml_table, toml_load, toml_error, toml_context, toml_parser_config
implicit none
logical, parameter :: color = .true.
character(:), allocatable :: manifest
type(toml_table), allocatable :: table
type(toml_error), allocatable :: error
type(toml_context) :: context
call get_argument(1, manifest)
if (.not.allocated(manifest)) manifest = "fpm.toml"
call toml_load(table, manifest, error=error, context=context, &
& config=toml_parser_config(color=color))
call handle_error(error)
contains
subroutine handle_error(error)
type(toml_error), intent(in), optional :: error
if (present(error)) then
write(stderr, '(a)') error%message
stop 1
end if
end subroutine handle_error
end program main
Wir erstellen ein Utility-Modul für die get_argument-Funktion, die zum Abrufen des Manifest-Namens verwendet wird. In den meisten Fällen können wir diesen per Default auf fpm.toml setzen, aber für Testzwecke ist es nützlich, ein Argument übergeben zu können.
!> Misc utilities for the fpm-lint implementation
module fpm_lint_utils
implicit none
private
public :: get_argument
contains
!> Obtain the command line argument at a given index
subroutine get_argument(idx, arg)
!> Index of command line argument, range [0:command_argument_count()]
integer, intent(in) :: idx
!> Command line argument
character(len=:), allocatable, intent(out) :: arg
integer :: length, stat
call get_command_argument(idx, length=length, status=stat)
if (stat == 0) then
allocate(character(len=length) :: arg, stat=stat)
end if
if (stat == 0 .and. length > 0) then
call get_command_argument(idx, arg, status=stat)
if (stat /= 0) deallocate(arg)
end if
end subroutine get_argument
end module fpm_lint_utils
Die erste Fehlerquelle, die wir bei der Parseroutine selbst erkennen können, ist das Parsen des TOML-Dokuments. Dies ist außerhalb der Verantwortung des Lint-Programms, trotzdem wollen wir prüfen, ob wir den Fehler korrekt melden können.
name = "demo"
[extra.fpm.lint]
package-name =
bare-keys = true
Durch das Starten des Lint-Programms auf diesem Dokument wird die folgende Fehlermeldung produziert, die von der toml_load-Prozedur ausgegeben wird.
❯ fpm run -- invalid.toml error: Invalid expression for value --> invalid.toml:4:15 | 4 | package-name = | ^ unexpected newline |
Mit diesem Fall abgeschlossen setzen wir uns an, die Konfiguration für den Linter zu lesen.
Unsere Konfiguration aus dem Paketmanifest wird in einem lint_config-Typ gespeichert, welcher in einem separate Modul definiert wird. Die Konfiguration wird von der Wurzel-Tabelle gelesen, d.h. wir müssen zunächst durch mehrere Untertabellen vorgehen, bevor wir die Optionen für den Linter verarbeiten können. Wir möchten hier Fehlermeldungen mit ausführlicher Kontextinformation erhalten, daher werden die origin-Angaben in den Aufrufen der get_value-Schnittstelle benötigt und wir erzeugen einen Bericht mit Hilfe des context-Wertes, den wir im Hauptprogramm erhalten haben.
!> Configuration data for the manifest linting
module fpm_lint_config
use tomlf, only : toml_table, toml_context, toml_terminal, toml_error, &
& toml_stat, get_value
implicit none
!> Configuration for the manifest linting
type :: lint_config
!> Check package name
logical :: package_name
!> Check all key paths
logical :: bare_keys
end type lint_config
contains
!> Load the configuration for the linter from the package manifest
subroutine load_lint_config(config, table, context, terminal, error)
!> Configuration for the linter
type(lint_config), intent(out) :: config
!> TOML data structure representing the manifest
type(toml_table), intent(inout) :: table
!> Context describing the data structure
type(toml_context), intent(in) :: context
!> Terminal for output
type(toml_terminal), intent(in) :: terminal
!> Error handler
type(toml_error), allocatable, intent(out) :: error
integer :: origin, stat
type(toml_table), pointer :: child1, child2, child3
call get_value(table, "extra", child1, origin=origin)
if (.not.associated(child1)) then
call make_error(error, context%report("The 'extra' table is missing.", &
& origin, "expected table", color=terminal))
return
end if
call get_value(child1, "fpm", child2, origin=origin)
if (.not.associated(child2)) then
call make_error(error, context%report("The 'fpm' table is missing.", &
& origin, "expected table", color=terminal))
return
end if
call get_value(child2, "lint", child3, origin=origin)
if (.not.associated(child3)) then
call make_error(error, context%report("The 'lint' table is missing.", &
& origin, "expected table", color=terminal))
return
end if
call get_value(child3, "package-name", config%package_name, .true., &
& stat=stat, origin=origin)
if (stat /= toml_stat%success) then
call make_error(error, context%report("Entry in 'package-name' must be boolean", &
& origin, "expected boolean value", color=terminal))
return
end if
call get_value(child3, "bare-keys", config%bare_keys, .true., &
& stat=stat, origin=origin)
if (stat /= toml_stat%success) then
call make_error(error, context%report("Entry in 'bare-key' must be boolean", &
& origin, "expected boolean value", color=terminal))
return
end if
end subroutine load_lint_config
!> Create an error message
subroutine make_error(error, message)
!> Error handler
type(toml_error), allocatable, intent(out) :: error
!> Message to be displayed
character(len=*), intent(in) :: message
allocate(error)
error%message = message
error%stat = toml_stat%fatal
end subroutine make_error
end module fpm_lint_config
Für einen einfachen Zugriff definiert wir eine make_error-Routine, die den Fehlerbehandlungsprozess ermöglicht und den Bericht aus dem Kontext speichert. An dieser Stelle sollten wir prüfen, ob die Fehlermeldungen korrekt funktionieren und den Linter auf einem falschen TOML-Dokument starten.
name = "demo"
[extra.fpm.lint]
package-name = "true"
bare-keys = true
aktuelles Hauptprogramm
Das Hauptprogramm sollte wie folgt aussehen.
program main
use, intrinsic :: iso_fortran_env, only : stderr => error_unit, stdout => output_unit
use fpm_lint_config, only : lint_config, load_lint_config
use fpm_lint_utils, only : get_argument
use tomlf, only : toml_table, toml_load, toml_error, toml_context, toml_parser_config, &
& toml_terminal
implicit none
logical, parameter :: color = .true.
character(:), allocatable :: manifest
type(toml_terminal) :: terminal
type(toml_table), allocatable :: table
type(toml_error), allocatable :: error
type(toml_context) :: context
type(lint_config) :: config
terminal = toml_terminal(color)
call get_argument(1, manifest)
if (.not.allocated(manifest)) manifest = "fpm.toml"
call toml_load(table, manifest, error=error, context=context, &
& config=toml_parser_config(color=terminal))
call handle_error(error)
call load_lint_config(config, table, context, terminal, error)
call handle_error(error)
contains
subroutine handle_error(error)
type(toml_error), intent(in), optional :: error
if (present(error)) then
write(stderr, '(a)') error%message
stop 1
end if
end subroutine handle_error
end program main
Durch das Starten des Lint-Programms auf diesem Dokument wird dies als Fehlermeldung markiert, da ein String-Wert anstatt einer Boolesche-Angabe angegeben wurde.
❯ fpm run -- fpm.toml error: Entry in 'package-name' must be boolean --> fpm.toml:4:16-21 | 4 | package-name = "true" | ^^^^^^ expected boolean value |
Zuletzt definieren wir ein Logging-Mechanismus, um die tatsächlichen Lint-Meldungen zu speichern, die nicht fatal sind. Der Logger bietet zwei Prozeduren, add_message zum Speichern einer Meldung und show_log zum Anzeigen aller gespeicherten Meldungen.
module fpm_lint_logger
implicit none
private
public :: lint_logger, new_logger
type :: log_message
character(:), allocatable :: output
end type log_message
type :: lint_logger
type(log_message), allocatable :: message(:)
contains
procedure :: add_message
procedure :: show_log
end type lint_logger
contains
subroutine new_logger(logger)
type(lint_logger), intent(out) :: logger
allocate(logger%message(0))
end subroutine new_logger
subroutine add_message(logger, message)
class(lint_logger), intent(inout) :: logger
character(*), intent(in) :: message
logger%message = [logger%message, log_message(message)]
end subroutine add_message
subroutine show_log(logger, io)
class(lint_logger), intent(in) :: logger
integer, intent(in) :: io
integer :: it
do it = 1, size(logger%message)
write(io, '(a)') logger%message(it)%output
end do
end subroutine show_log
end module fpm_lint_logger
Empfohlener Paketname#
Als erste Prüfung werden wir den Paketnamen prüfen, dazu werden folgende Regeln angewendet:
der Paketname sollte ein TOML-Bare-Schlüssel sein, um in den dependency-Bereichen keine Anführungszeichen zu erfordern, Sonderzeichen wie Punkte, Doppelpunkte, Kommas oder Schrägstriche sind nicht erlaubt
TOML bevorzugt mit Bindestrichen getrennte Kleinbuchstaben in Schlüsselnamen, daher werden wir Großschreibung (camelCase und PascalCase) und Unterstriche (snake_case) verbieten
Es gibt mehrere Möglichkeiten, Strings in TOML zu deklarieren, wir möchten das normale String-Format vorziehen
Ein Beispiel eines Paketnamen, den wir verboten werden würden, wäre fpmLinter wie im Manifest unten angezeigt.
name = "fpmLinter"
Damit wir mit unserer Implementierung beginnen können, reexportieren wir die anderen Module aus dem fpm_lint-Modul, dies ermöglicht einen klaren Import im Hauptprogramm. Anschließend definieren wir die lint_data-Prozedur, in der wir prüfen, ob der name-Schlüssel vorhanden ist, wenn nicht erstellen wir eine Meldung im info-Level und verlassen den Block-Scope, da alle weiteren Prüfungen auf die Vorhandensein des Eintrags basieren.
Wir können jetzt prüfen, ob der Eintrag als String angegeben ist oder möglicherweise als etwas anderes, wie ein Literalstring, das geprüft werden muss. Weiterhin prüfen wir, ob der Paketname nur Kleinbuchstaben, Zahlen und Bindestriche verwendet.
!> Linter for package manifests used with the Fortran package manager
module fpm_lint
use fpm_lint_config, only : lint_config, load_lint_config
use fpm_lint_logger, only : lint_logger, new_logger
use fpm_lint_utils, only : resize, get_argument
use tomlf, only : toml_table, toml_context, toml_terminal, toml_error, toml_level, &
& toml_key, get_value
use tomlf_de_token, only : token_kind, stringify
implicit none
private
public :: lint_config, load_lint_config
public :: lint_logger, new_logger
public :: lint_data
public :: get_argument
contains
!> Entry point for linting the data structure representing the package manifest
subroutine lint_data(logger, config, table, context, terminal)
!> Instance of the logger
type(lint_logger), intent(inout) :: logger
!> Configuration for the linter
type(lint_config), intent(in) :: config
!> TOML data structure
type(toml_table), intent(inout) :: table
!> Context describing the data structure
type(toml_context), intent(in) :: context
!> Terminal for output
type(toml_terminal), intent(in) :: terminal
if (config%package_name) then
check_package_name: block
character(:), allocatable :: package_name
integer :: origin
call get_value(table, "name", package_name, origin=origin)
if (.not.allocated(package_name)) then
call logger%add_message(context%report( &
"Package name entry is required in the top-level", &
origin, &
level=toml_level%info, color=terminal))
exit check_package_name
end if
if (context%token(origin)%kind /= token_kind%string) then
call logger%add_message(context%report( &
"Package name should not be a "//stringify(context%token(origin)), &
origin, &
"prefer string value (double quotes)", &
level=toml_level%info, color=terminal))
end if
if (verify(package_name, "abcdefghijklmnopqrstuvwxyz0123456789-") > 0) then
call logger%add_message(context%report( &
"Package name should be lowercase with dashes", &
origin, &
level=toml_level%info, color=terminal))
end if
end block check_package_name
end if
end subroutine lint_data
end module fpm_lint
Tipp
Der toml_level
-Parameter stellt einen statisch initialisierten abgeleiteten Typ dar, der alle verfügbaren Meldungslevels enthält. Ähnlich gilt der token_kind
-Parameter als Enumerator mit einem gültigen Namensraum.
aktuelles Hauptprogramm
Das Hauptprogramm sollte wie folgt aussehen.
program main
use, intrinsic :: iso_fortran_env, only : stderr => error_unit, stdout => output_unit
use fpm_lint, only : lint_config, load_lint_config, lint_logger, new_logger, &
& lint_data, get_argument
use tomlf, only : toml_table, toml_load, toml_error, toml_context, toml_parser_config, &
& toml_terminal
implicit none
logical, parameter :: color = .true.
integer, parameter :: detail = 1
character(:), allocatable :: manifest
type(toml_terminal) :: terminal
type(toml_table), allocatable :: table
type(toml_error), allocatable :: error
type(toml_context) :: context
type(lint_logger) :: logger
type(lint_config) :: config
terminal = toml_terminal(color)
call get_argument(1, manifest)
if (.not.allocated(manifest)) manifest = "fpm.toml"
call toml_load(table, manifest, error=error, context=context, &
& config=toml_parser_config(color=terminal, context_detail=detail))
call handle_error(error)
call load_lint_config(config, table, context, terminal, error)
call handle_error(error)
call new_logger(logger)
call lint_data(logger, config, table, context, terminal)
call logger%show_log(stdout)
contains
subroutine handle_error(error)
type(toml_error), intent(in), optional :: error
if (present(error)) then
write(stderr, '(a)') error%message
stop 1
end if
end subroutine handle_error
end program main
Wir prüfen diesen auf den Paketnamen mit camelCase von oben und können folgende Ausgabe finden.
❯ fpm run -- fpm.toml info: Package name should be lowercase with dashes --> fpm.toml:1:8-18 | 1 | name = "fpmLinter" | ^^^^^^^^^^^ |
Übung
Füge eine Prüfung für die Länge des Paketnamens hinzu, alles unter drei Zeichen ist wahrscheinlich ein schlechter Wahl, genauso wie ein zu langer Paketname.
Erzeuge einen Beispiel, um den Fehler auszulösen. Was passiert wen ein zu langer camelCase-Paketname verwendet wird?
Blanke Schlüsselpfade bevorzugt#
TOML erlaubt Schlüssel zu quotieren, was aber visuell unübersichtlich wird wenn nur einige Schlüssel quotiert werden und andere nicht. Mit unserer Paketnamenregel sollte es nicht notwendig sein, irgendeinen Schlüssel in Abhängigkeitsabschnitten zu quotieren.
Um zu bestimmen, ob ein String in dem Kontext eines Schlüssels verwendet wird, müssen wir eine Methode finden, um alle Schlüssel zu identifizieren. Wir können alle Einträge in den Datenstrukturen durchlaufen und die Schlüssel prüfen. Allerdings ist dies auch etwas teuer und wir können auch Schlüssel verpassen, die nicht aufgezeichnet werden.
name = "demo"
[dependencies]
toml-f.git = "https://github.com/toml-f/toml-f"
"toml-f".tag = "v0.2.3"
In diesem Beispiel wird das zweite Vorkommnisse des Schlüssels toml-f
nur referenzieren, aber es ist bereits in der Zeile vorher definiert. Die Anführungszeichen sind visuell identifizierbar als Lint und wir müssen eine Programmiermethode finden, um dies zu markieren.
Anstatt mit der Datenstruktur zu arbeiten, werden wir den Parser verwenden um mehr Tokens in den Kontext zu speichern. Anstatt nur Fehler zu melden, werden wir den Kontext verwenden, um Schlüssel zu identifizieren. Dies wird durch das Erhöhen der context_detail-Option in der config-Schlüssel der Parser auf eins gesetzt. Damit werden alle Tokens, außer Leerzeichen und Kommentare gespeichert.
call toml_load(table, manifest, error=error, context=context, &
& config=toml_parser_config(color=color, context_detail=1))
Tipp
Durch das Erhöhen der context_detail auf zwei werden auch Leerzeichen und Kommentare gespeichert. Dies kann hilfreich sein, wenn Checks für Leerzeichen oder Einrückungsstile geschrieben werden.
Unsere Linter-Schritt läuft wie folgt:
identifiziere alle relevanten Schlüssel im Manifest
prüfe, ob sie Schlüsselpfad-Tokens sind
erzeuge einen Bericht für jeden Schlüssel, der ein String oder ein Literal ist
Unsere Implementierung reflektiert dies durch das Sammeln einer Liste von toml_key-Objekten in list und dann durchlaufen aller Einträge, prüfen, ob sie das korrekte token_kind haben.
!> Entry point for linting the keys in the TOML document
subroutine lint_keys(logger, config, context, terminal)
!> Instance of the logger
type(lint_logger), intent(inout) :: logger
!> Configuration for the linter
type(lint_config), intent(in) :: config
!> Context describing the data structure
type(toml_context), intent(in) :: context
!> Terminal for output
type(toml_terminal), intent(in) :: terminal
integer :: it
type(toml_key), allocatable :: list(:)
call identify_keys(list, context)
if (config%bare_keys) then
do it = 1, size(list)
associate(token => context%token(list(it)%origin))
if (token%kind /= token_kind%keypath) then
call logger%add_message(context%report( &
"String used in key path", &
list(it)%origin, &
"use bare key instead", &
level=toml_level%info, color=terminal))
end if
end associate
end do
end if
end subroutine lint_keys
Um die Liste zu erzeugen müssen wir die identify_keys-Prozedur implementieren. Die Regeln in TOML für Schlüsselpfade sind einfach: vor einem Gleichheitszeichen können Schlüsselpfade sein und Schlüsselpfade können nur in Tabellen und Inline-Tabelle vorkommen. Dies kann implementiert werden, indem ein Stapel verwendet wird, um zu prüfen, ob der aktuelle Abschnitt in einer Tabelle, einem Feld oder einem Wert ist.
Wir werden immer einen neuen Abschnitt hinzufügen wenn wir das Token finden, das es öffnet, d.h. ein Wert öffnet sich mit einem Gleichheitszeichen, ein Feld mit einem rechten Klammer-Symbol, eine Inline-Tabelle mit einem rechten Klammer-Symbol. Um Tabelle-Überschriften von Inline-Arrays zu unterscheiden, fügen wir nur Arrays auf den Stapel hinzu nach einem Gleichheitszeichen. Zuletzt setzen wir den ersten Abschnitt als einen Tabelle-Abschnitt, wenn kein anderer Abschnitt vorhanden ist und wir haben alle benötigten Regeln zum Identifizieren von Schlüsselpfaden gesammelt. Analog gilt es für die Endungen der Abschnitte.
Dann können wir prüfen, ob der aktuelle Abschnitt auf dem Stapel Schlüsselpfade erlaubt und diese in unsere Liste aufnehmen.
!> Collect all key paths used in TOML document
subroutine identify_keys(list, context)
!> List of all keypaths in the TOML document
type(toml_key), allocatable, intent(out) :: list(:)
!> Context describing the data structure
type(toml_context), intent(in) :: context
integer, parameter :: table_scope = 1, array_scope = 2, value_scope = 3
integer :: it, top
integer, allocatable :: scopes(:)
allocate(list(0))
top = 0
call resize(scopes)
! Documents always start with a table scope
call push_back(scopes, top, table_scope)
do it = 1, context%top
select case(context%token(it)%kind)
case(token_kind%keypath)
! Record all key path
associate(token => context%token(it))
list = [list, toml_key(context%source(token%first:token%last), it)]
end associate
case(token_kind%string, token_kind%literal)
! Record all strings used in key paths
if (scopes(top) == table_scope) then
associate(token => context%token(it))
list = [list, toml_key(context%source(token%first+1:token%last-1), it)]
end associate
end if
case(token_kind%equal) ! Open value scope
call push_back(scopes, top, value_scope)
case(token_kind%lbrace) ! Open inline table scope
call push_back(scopes, top, table_scope)
case(token_kind%lbracket) ! Open array scope
if (scopes(top) /= table_scope) then
call push_back(scopes, top, array_scope)
end if
case(token_kind%newline) ! Close value scope in key-value pair
call pop(scopes, top, value_scope)
case(token_kind%rbrace) ! Close value and table scope in inline table
call pop(scopes, top, value_scope)
call pop(scopes, top, table_scope)
case(token_kind%comma) ! Close value scope in inline table
call pop(scopes, top, value_scope)
case(token_kind%rbracket) ! Close array scope
call pop(scopes, top, array_scope)
end select
end do
contains
!> Push a new scope onto the stack
pure subroutine push_back(scopes, top, this_scope)
!> Stack top
integer, intent(inout) :: top
!> Current stack of scopes
integer, allocatable, intent(inout) :: scopes(:)
!> Scope to push onto the stack
integer, intent(in) :: this_scope
top = top + 1
if (top > size(scopes)) call resize(scopes)
scopes(top) = this_scope
end subroutine push_back
!> Remove a matching scope from the stack
subroutine pop(scopes, top, this_scope)
!> Stack top
integer, intent(inout) :: top
!> Current stack of scopes
integer, allocatable, intent(inout) :: scopes(:)
!> Scope to remove from the stack
integer, intent(in) :: this_scope
if (top > 0) then
if (scopes(top) == this_scope) top = top - 1
end if
end subroutine pop
end subroutine identify_keys
Für einen einfachen Zugriff implementieren wir eine push_back- und pop-Funktion, um Abschnitte auf den Stapel hinzuzufügen und entfernen zu können. Die pop-Funktion führt zusätzlich einen Prüfung aus, ob wir einen passenden Abschnitt entfernen möchten und vermeiden eine Wiederholung in der Schleife auf diese Weise.
In unserem Utility-Modul implementieren wir die resize-Prozedur für ein Array von Ganzzahlen
!> Misc utilities for the fpm-lint implementation
module fpm_lint_utils
implicit none
private
public :: resize
public :: get_argument
!> Resize a 1D array to a new size
interface resize
module procedure :: resize_ints
end interface resize
contains
!> Reallocate list of integer
pure subroutine resize_ints(var, n)
!> Instance of the array to be resized
integer, allocatable, intent(inout) :: var(:)
!> Dimension of the final array size
integer, intent(in), optional :: n
integer, allocatable :: tmp(:)
integer :: this_size, new_size
integer, parameter :: initial_size = 8
if (allocated(var)) then
this_size = size(var, 1)
call move_alloc(var, tmp)
else
this_size = initial_size
end if
if (present(n)) then
new_size = n
else
new_size = this_size + this_size/2 + 1
end if
allocate(var(new_size))
if (allocated(tmp)) then
this_size = min(size(tmp, 1), size(var, 1))
var(:this_size) = tmp(:this_size)
deallocate(tmp)
end if
end subroutine resize_ints
end module fpm_lint_utils
aktuelles Hauptprogramm
Das Hauptprogramm sollte wie folgt aussehen.
program main
use, intrinsic :: iso_fortran_env, only : stderr => error_unit, stdout => output_unit
use fpm_lint, only : lint_config, load_lint_config, lint_logger, new_logger, &
& lint_data, lint_keys, get_argument
use tomlf, only : toml_table, toml_load, toml_error, toml_context, toml_parser_config, &
& toml_terminal
implicit none
logical, parameter :: color = .true.
integer, parameter :: detail = 1
character(:), allocatable :: manifest
type(toml_terminal) :: terminal
type(toml_table), allocatable :: table
type(toml_error), allocatable :: error
type(toml_context) :: context
type(lint_logger) :: logger
type(lint_config) :: config
terminal = toml_terminal(color)
call get_argument(1, manifest)
if (.not.allocated(manifest)) manifest = "fpm.toml"
call toml_load(table, manifest, error=error, context=context, &
& config=toml_parser_config(color=terminal, context_detail=detail))
call handle_error(error)
call load_lint_config(config, table, context, terminal, error)
call handle_error(error)
call new_logger(logger)
call lint_data(logger, config, table, context, terminal)
call lint_keys(logger, config, context, terminal)
call logger%show_log(stdout)
contains
subroutine handle_error(error)
type(toml_error), intent(in), optional :: error
if (present(error)) then
write(stderr, '(a)') error%message
stop 1
end if
end subroutine handle_error
end program main
In dieser Stelle können wir nun einen Aufruf in unserem Hauptprogramm für den Linter hinzufügen.
❯ fpm run -- fpm.toml info: String used in key path --> fpm.toml:5:1-8 | 5 | "toml-f".tag = "v0.2.3" | ^^^^^^^^ use bare key instead |
Jetzt für etwas schwieriges mit einer Inline-Tabelle, um zu prüfen, ob unsere Abschnittsregeln korrekt funktionieren.
name = "demo"
[dependencies]
toml-f = {git = "https://github.com/toml-f/toml-f", "tag" = "v0.2.3"}
Unsere Linter kann den tag-Eintrag korrekt als einen String in der Schlüsselpfad-Kontext identifizieren und produziert das korrekte Meldungsformat.
❯ fpm run -- fpm.toml info: String used in key path --> fpm.toml:4:53-57 | 4 | toml-f = {git = "https://github.com/toml-f/toml-f", "tag" = "v0.2.3"} | ^^^^^ use bare key instead |
Übung
Früher wurde die Verwendung eines Literal-Strings als Wert für den Paketnamen geprüft, aber ein Paketmanifest kann viel mehr Strings enthalten.
Erstelle einen Prüfung für alle String-Werte im Manifest, um sicherzustellen, dass sie mit Anführungszeichen versehen sind. Sammle String-Werte (string, literal, mstring, und mliteral) aus Array- und Wert-Abschnitten für diesen Zweck.
Kannst du einen nützlichen Vorschlag machen, wenn ein Literal-String Zeichen enthält, das in einem String mit doppelten Anführungszeichen maskiert muss?
Zusammenfassung#
Dies beendet das Linten, das wir für das fpm-Paketmanifest implementiert haben. Für einen vollständigen Linter wird die Regelmenge, die geprüft werden soll, in der Regel mit Zeit weiterentwickelt und kann auch bei Bedarf auch wieder ändern. Unsere Linter bietet zur Zeit nur einige Regeln, aber es kann zusätzliche Prüfungen hinzugefügt werden, wenn der Bedarf entsteht.
Übung
Unsere Ausgabe ist derzeit in der Reihenfolge der Prüfungen, anstatt in der Reihenfolge der Meldungen, die in der TOML-Dokumentation auftreten. Die Ausgabe der Meldungen sollte intuitiverer darstellt sein, wenn sie nach den Quellzeilen sortiert werden.
Speichere die Position des ersten Zeichens in der Ausgabe zusammen mit den Meldungen im Logger. Der Logger sollte die Meldungen nach ihrer Reihenfolge vor der Ausgabe sortieren.
Wichtig
In diesem Tutorial hast du gelernt, eigene Meldungen in deinen TOML-Eingabedaten zu erstellen. Du kannst nun
farbige Fehlermeldungen mit ausführlicher Kontextinformation ausgeben
Fehlermeldungen erstellen, wenn eine TOML-Datenstruktur gelesen wird
die Details einstellen mit denen der Kontext des TOML-Dokumentes beschrieben wird
ein TOML-Dokument prüfen, basierend auf den Tokeninformationen im Kontext