Vala, le futur

Il y a quelques jours j’ai lu ce post, qui suggérait d’utiliser Rust dans le développement d’applications et de bibliothèques GNOME. Dans ce billet, l’auteur, qui a utilisé et utilise Vala, dit qu’il aime ce langage, mais qu’un de ces principaux problèmes est qu’il est très dur à déboguer. Et je confirme, Vala est une horreur à déboguer : le compilateur génère parfois des erreurs dans le code C alors qu’il ne devrait pas, il n’y a pas de débogueur « officiel », et donc on se débrouille avec GDB et/ou Nemiver en mettant des points d’arrêts sur les noms des fonctions en C. Au final j’utilise plus souvent la technique des print un peu partout dans mon code, pour voir à quel niveau ça plante et quelle tête ont les différentes variables à ce moment-là. Bref, ça fait des sorties de trois kilomètres de long, c’est sale et pas super efficace…

En lisant les commentaires, on voit que ce n’est pas le seul reproche qui est fait à Vala : il est difficile d’être sûr que la prochaine version du compilateur sera compatible avec l’actuelle. Le projet n’a jamais atteint de version stable depuis sa création, il y a environ dix ans.

J’ai moi-même des reproches à faire à l’écosystème Vala (même si j’adore ce langage). Pour commencer, l’installation de bibliothèques est très compliqué. Si on a une distribution Linux dérivé de Debian ou de Fedora, il ne devrait pas y avoir trop de problèmes, mais dès qu’on part dans quelque chose d’un peu plus exotique, voir sur du Windows ou du Mac OS X, ça devient très problématique. Essayez de compiler VSGI sous Windows, vous allez hurler : une fois que vous aurez remplacer les parties du code ne fonctionnant que sous UNIX-likes par des versions plus mutiplateformes, vous devrez installer les différentes dépendances. Prenons par exemple la bibliothèque FastCGI utilisé par ce projet : le fichier .vapi est fourni, mais pour trouver le fichier .h et la DLL qui correspond, c’est une autre histoire !

Un autre problème : c’est la documentation. Valadoc.org pourrait être un million de fois mieux fait. Il y a trop de JavaScript inutile partout, certaines parties n’ont pas la moindre petite explication (ou alors c’est la documentation de l’API correspondante en C, avec les noms de méthode en C…), depuis quelque temps la recherche ne marche plus (heureusement un miroir avec une recherche fonctionelle a été mis en place) et les liens ne valent rien : essayer par exemple d’accéder à la documentation du type string, avec ce lien http://www.valadate.org:8000/#!api=glib-2.0/string, vous allez arriver sur la page d’accueil à tous les coups ! Il n’y a pas d’API, alors que ça pourrait être très utile pour, par exemple, Valhalla. Ajouter un paquet à l’air assez complexe (il faut passer par GitHub, puis attendre que le site soit mis à jour et surtout ça à l’air d’être un mix de différents types de fichiers…), lors qu’un formulaire avec envoi de fichiers du code source (ou juste un lien vers un dépôt Git/autre) puis une validation par l’administrateur du site me semble bien mieux (plus logique, plus simple pour tout le monde). L’outil pour générer la documentation en local n’est aussi disponible que sous Debian et dérivé, et je n’ai pas réussi à le compiler sous Fedora (je n’ai même pas tenté Windows, vous imaginez l’horreur que ça doit être). Et enfin, même si ça n’est pas très grave, c’est un peu un comble, car le site n’est pas écrit en Vala, mais en PHP ! Et on sait faire des sites en Vala : il suffit d’utiliser Valum, Valse ou même quelque chose de plus bas niveau comme LibSoup !

Il y a aussi un manque de ressource pour les vrais débutants (tutoriels pour ceux qui ont déjà programmé, et pas toujours très à jour). Et si on a un problème on doit en général passer par la mailing-list, ce qui ne me donne pas vraiment l’impression d’être en 2016.

Pour résumer, Vala : - Est une horreur à déboguer ; - Est instable (pas sûr que les différentes versions seront compatibles) ; - N’a pas de moyen simple et multiplateforme d’installer des bibliothèques (à l’instar de pip, gem, npm, cargo et tant d’autres) ; - A une documentation pour le moins… pas terrible ; - Est réservé aux personnes ayant déjà une certaine expérience en programmation.

La mort de Vala ?

À ce stade de la réflexion, on pourrait se dire qu’il vaut mieux laisser mourrir Vala, et passer progressivement à Rust comme l’a suggéré Alberto Ruiz sur son blog. J’ai juste envie de répondre : non. Vala est fantastique : il représente un gain de temps énorme (quand on a pas à déboguer le code), et il allie une syntaxe très agréable, compréhensible et haut-niveau (et il ne l’est pas seulement dans la syntaxe, il l’est réellement), tout en gardant une vitesse d’exécution exceptionnelle et il permet de générer du code réutilisable dans n’importe quel autre langage grâce à GObject. À l’heure de l’Internet des Objets, à l’heure où des applications plus intelligentes que jamais doivent tenir dans des grille-pains, prendre peu de placer dans la mémoire et être rapide est un grand avantage pour un langage, mais si en plus il permet de développer des applications complexes (IA, réseau, etc) et peu de temps et de lignes, ce langage peut être réellement utile. Vala est resté cantonné à quelques applications GNOME, mais il pourrait tourner n’importe où : du plus puissant serveur Linux, au PC Windows, en passant par l’iPhone, la tablette Android ou la carte Arduino. En se débrouillant bien, je suis sûr qu’il y a moyen de créer des bibliothèques qui supportent ses plateformes en ne fournissant qu’une seule interface de développement, grâce à Vala.

Mais bon, Rust peut surement en faire autant, alors pourquoi garder Vala ? Déjà parce que même si le langage n’est pas massivement utilisé, un certains nombre de logiciels, peut-être que vous utilisez sont écrits en Vala, et si on abandonne le langage, je ne pense pas que les développeurs aient envie de tout réécrire en Rust (ou autre) depuis zéro. Ensuite, c’est très subjectif, mais je trouve que Vala a une syntaxe très élégante, surtout si on respecte les conventions de style d’elementary OS. Et enfin, si vous voulez créer une bibliothèque utilisable à peu près dans n’importe quel autre langage, Vala est surement la meilleure solution, grâce à GObject et la syntaxe qui permet d’être très productif par rapport au C.

Abandonner Vala ne semble donc pas vraiment possible. Seuleument le projet est en manque de nouveaux contributeurs, les principaux développeurs n’ayant plus le temps de s’en occuper. Et je pense que si le projet n’attire pas, c’est parce que les outils utilisés sont trop old-schools : cgit et Bugzilla, ça donne pas trop envie de contribuer. Ce n’est pas le workflow auquel la plupart des développeurs sont maintenant habitués avec Github/Gitlab. Et si Vala était hébergé sur un de ces deux services, j’aurais, personnellement, déjà contribué. Ensuite il n’y a pas à ma connaissance d’intégration continue, ce qui semble un peu problématique pour ce genre de projet…

Comment améliorer les choses ?

Pour améliorer les choses, il faudrait :

  • Améliorer les outils, ce qui passe par :
    • Avoir un npm-like. Je me suis lancé dans la création de poulp, arteymix (le principal développeur de Valum) avait commencé à réfléchir à drakkar, mais le projet est encore au stade de la spécification. On peut déjà utiliser des sous-modules Git et Meson pour se faciliter un peu la tâche, mais ce n’est pas idéal ;
    • Avoir un vrai IDE. Il y a plein d’éditeurs de texte qui supportent plus ou moins bien Vala, mais en général on se limite à de la coloration syntaxique. GNOME Builder avait l’air plutôt intéressant, mais le support de Vala n’a pas l’air d’être une priorité, et c’est pas encore ça. J’ai écrit Valhalla (un plugin pour Atom pour aider à écrire du Vala), mais il manque encore beaucoup de choses pour arriver à quelque chose d’utilisable et surtout d’utile au quotidien. Il y a aussi elementary-ide, mais il est lui aussi en cours de développement, et niveau multiplateforme je ne suis pas sûr que ça soit trop ça…
    • Faciliter le débogage, mais ça se range aussi dans la catégorie IDE.
  • Avoir des bibliothèques intéréssantes pour à peu près tout : sites web, GUI, machine learning, etc ;
  • Avoir une documentation meilleure que l’actuelle. Ça passe par :
    • Réécrire le site de Valadoc depuis zéro et refaire tout ce qui ne va pas (voir plus haut), en ajoutant la possibilité de modifier les descriptions via une interface simple ;
    • Rendre l’outil de génération de documentation locale (la commande valadoc) multiplateforme et installable facilement ;
    • Avoir des tutoriels à jour et que même des grands débutants en programmation peuvent aborder, des exemples pour les différentes bibliothèques, des guides pas-à-pas, bref des ressources pour ne jamais rester bloqué face à une bibliothèque qu’on ne sait pas utiliser et être obligé d’aller chercher la documentation C.

Déjà, si on arrive à ça, Vala deviendra mille fois plus utilisable. Ensuite j’ai quelques idées d’améliorations sympa à apporter au projet en lui-même :

  • Déjà, fixer un maximum de bugs, surtout que certains trainent depuis plusieurs années maintenant ;
  • Au niveau du langage, pourvoir définir des opérateurs personnalisés serait un gros plus. Et avoir ce qui s’appelle en C# des méthodes d’extension aussi (c’est juste des idées qui me passe par la tête et je suis sûr qu’il y aurait plein de choses à ajouter) ;
  • Ensuite il faudrait « moderniser » le développement : installer une instance de Gitlab et mettre en place de la CI ça doit être faisable, non ? Bon en fait, GNOME veut garder son workflow actuel (je ne sais pas pourquoi) et donc à moins de forker le projet, ça risque d’être compliqué…
  • Rendre Vala vraiment multiplateforme : pouvoir compiler pour différentes plateformes et pouvoir transpiler vers plusieurs langages : on passe une option au compilateur et il nous génère le code Swift de notre application qu’on pourra ensuite compiler pour iOS, avec une autre option il génère du JavaScript près à être utilisé dans un navigateur, ou bien une application écrite en Rust (un peu comme ce que fait Haxe). Mais bon, arriver à ça va être très compliqué, même si le langage deviendrait alors super intéressant. On peut déjà avancer vers plus de multiplateforme en facilitant le développement et la compilation sous Windows et Mac OS X.

Et bien sûr, communiquer, parler du projet, sans quoi même s’il est génial, personne n’en entendra parler.

Conclusion

On peut faire quelque chose de génial de Vala, il faut juste le faire. En écrivant cet article, je me suis rendu compte qu’il fallait que je me concentre sur poulp et Valhalla, car parmi tous mes projets ce sont ces deux-là qui sont les plus importants pour le futur de Vala.

Je ne dis pas non plus qu’utiliser Rust pour GNOME est une mauvaise idée : au contraire si il permet de faire gagner du temps et de sécuriser certaines parties du code qui en ont besoin, c’est très bien. Au final, passer du tout C qu’on a un peu actuellement dans GNOME vers du C + Vala + Rust, respectivement pour ce qui peut rester en C, ce qui est utile de passer à du plus haut niveau (pour gagner du temps) et ce qui a besoin d’être « sécurisé ».

Et juste un petit mot à propos de la série d’article sur Valhalla : le dernier article n’arrivera pas tout de suite parce que le module de documentation risque de pas mal changer dans les prochaines versions. J’attendrai donc d’avoir un fonctionnement plus stable pour en parler.

Le fonctionnement de Valhalla, partie 3 : afficher les suggestions

Et voici donc le second article de cette série où je vous explique le fonctionnement du package Atom que je développe : Valhalla. Aujourd’hui, on va voir comment Valhalla fait pour afficher ses suggestions.

Tout ce passe dans dans le fichier provider.js (ou presque). On y trouve une classe (ValaProvider) divisée en trois grandes parties :

  • L’initialisation, où on va notamment lancer la découpe de notre code, comme on l’a vu dans la partie précédente. On va aussi préciser à Atom que le moteur de suggestions n’est valable que dans du Vala ;
  • Le moteur de suggestions, qui va dire quelle suggestions afficher en fonction du contexte, en se basant sur le code qu’on a découpé auparavant ;
  • Des fonctions pour nous aider un peu, par exemple pour afficher facilement une méthode dans les suggestions ou pour déterminer le type d’une expression.

On va surtout s’intéresser à la deuxième partie, c’est là que la plupart des choses se font.

Le moteur de suggestions

Pour obtenir ses suggestions, Atom va exécuter la fonction getSuggestions de notre classe (on verra plus tard comment il sait quelle fonction prendre). Cette fonction retourne une Promise qui est résolue avec un tableau contenant les suggestions sous forme d’objets, ressemblant généralement à ça.

{
    text: 'Suggestion',
    type: 'function',
    leftLabel: 'Super',
    rightLabel: 'Génial',
    description: 'Une super suggestion, vraiment géniale !',
    descriptionMoreURL: 'https://bat41.gitlab.io'
}

Ce code donnera quelque chose comme ça :

Notre suggestion

On a aussi d’autres options plus avancés, si ça vous intéresse, vous pouvez aller jeter un coup d’œuil au wiki du package qui gère l’autocomplétion.

On crée donc un tableau vide qui contiendra toutes nos suggestions par la suite, puis on utilise la méthode qu’on a déjà vu pour explorer notre arbre de blocs de code, et on filtre en fonction du contexte.

// Version (très simplifiée) pour que vous compreniez le principe.
getSuggestions ({editor, bufferPosition, scopeDescriptor, prefix, activatedManually}) {
    // On obtient la ligne actuelle.
    var line = editor.getTextInRange([[bufferPosition.row, 0], bufferPosition]);

    return new Promise ((resolve) => {
        let suggestions = [];

        const trimLine = line.trim ();

        // La méthode récursive utilisée pour explorer les blocs de code.
        let explore = (scope) => {

            // Ici par exemple, si la ligne commence par `using `,
            // on affiche les différents espaces de noms disponibles
            if (trimLine.startsWith('using ')) {
                // Le bloc actuel est un espace de nom
                if (scope.data && scope.data.type == 'namespace') {
                    // Il commence comme ce que l’utilisateur a tapé
                    if (name.startsWith(prefix) || prefix == ' ') {
                        // show suggestion
                        let suggestion = {
                            text: name + ';',
                            type: 'import',
                            displayText: name,
                            description: `The ${name} namespace.`
                        };
                        suggestions.push (suggestion);
                    }
                }
            }

            // On explore les enfants
            for (child of scope.children) {
                explore (child);
            }
        }

        // On explore tout les scopes à la racine
        for (scope of this.manager.scopes) {
            explore (scope);
        }

        // ON envoie nos suggestions
        if (trimLine != '') {
            resolve(suggestions);
        }
    });
}

En réalité, c’est bien plus complexe que ça, mais le principe reste toujours le même.

Lier tout ça au reste d’Atom

On a maintenant un joli moteur de suggestions, sauf qu’Atom ne sait pas qu’il faut l’utiliser ! Résultat : il ne se passe rien…

Il faut savoir qu’à peu près tout dans Atom est un package (et c’est peut être la plus grande force de cet éditeur) : la fonction rechercher, l’affichage des onglets, des suggestions… Et les différents packages ont un moyen très simple de communiquer entre eux, appelé services. Un package propose un service, les autres peuvent y souscrire et ils échangent des information ainsi. C’est par exemple ce que fait autocomplete-plus, le package qui affiche les suggestions : on peut ainsi lui donner nos propres moteurs de suggestions. Et Valhalla utilise ce service, grâce à ce morceau de son manifeste.

"providedServices": {
  "autocomplete.provider": {
    "versions": {
      "2.0.0": "getProvider"
    }
  }
}

Ici, on indique à autocomplete-plus qu’il peut obtenir notre moteur grâce à la méthode getProvider, qui se trouve dans l’objet exporté par Valhalla (export default ou module.export) , situé dans valhalla.js. Cette méthode retourne simplement une instance de la classe ValaProvider ! Et voilà, Atom sait où aller chercher les suggestions pour le code Vala et peut les afficher.


Cet article est terminé, mais un autre devrait bientôt arriver. En tout cas j’espère qu’il vous aura intéressé.

Le fonctionnnement de Valhalla, partie 2 : Découper le code

Cet article est donc le premier de la série où je vais vous expliquer le fonctionnnement de Valhalla. Nous allons commencer par voir comment Valhalla découpe le code des différentes bibliothèques installées.

Tout d'abord, au lancement d'Atom, le pckage va être chargé, et la fonction activate que l'on trouve dans le fichier valhalla.js (on lui indique dans le manifeste). Dans cette fonction, on va faire tout un tas de chose, mais on va surtout initialiser une nouvelle instance de la classe ValaProvider qui fournit les suggestions lorsqu'on commence à taper quelque chose. Mais cette partie ne nous intéresse pas encore. C'est juste de ce bout de code, dans le constructeur de ValaProvider dont nous allons parler :

const vapiDir = atom.config.get('valhalla.vapiDir');
this.manager = new ScopeManager();

atom.workspace.observeTextEditors((editor) => {
    // dès qu'un éditeur est créé
    if (editor.getPath() && editor.getPath().endsWith('.vala')) {
        // si c'est un fichier Vala qui y est ouvert
        editor.onDidStopChanging((event) => {
            // quand il a été modifié, on le « découpe » (voir explication après)
            this.manager.parse(editor.getText(), editor.getPath());
        });
        // Et puis on le découpe aussi maintenant
        this.manager.parse(editor.getText(), editor.getPath());
    }
});

// On lit les fichiers .vapi
fs.readdir (vapiDir, (err, files) => {
    if (err) {
       console.error (err);
       return;
    }

    for (file of files) {
       if (file.endsWith('.vapi')) {
           let content = fs.readFileSync(path.join(vapiDir, file), 'utf-8');
           // On les « découpe »
           this.manager.parse(content, file);
       }
    }
});

Ici, on crée une nouvelle instance de la classe ScopeManager, qui a pour rôle de « découper » le code des bibliothèques installées (contenu dans des fichiers .vapi) et le code écrit par l'utilisateur d'Atom et nous en faire des objets, beaucoup plus simples à utiliser. Par exemple, après avoir été « découpée », une classe ressemblera à quelque chose comme ça :

{
    top: scopeParent, // Le scope parent (un espace de nom par exemple). Je ne l'ai pas réellement mis, car il contient une référence vers ces enfants, donc cet objet et on n'en finirait pas.
    at: [[0, 0] [10, 1]], // L'endroit où est définie cette classe dans le fichier.
    name: 'public class Hey : Object ', // La définition de la classe.
    children: [enfant1, enfant2, enfant3], // Les enfants de cette classe (méthodes, propriétés ...), pareil que pour `top`, je ne les ait pas mis.
    file: 'bibli.vapi', // Le fichier d'où vient la classe.
    documentation: { // La documentation de cette classe. En général elle n'est pas dans les fichiers .vapi, mais dans les fichiers .vala (qui continnent le code écrit par l'utilisateur)
        short: 'Une super classe.',
        long: 'En vrai elle a trop la classe ! \n\n\n\n\n\n\n\n ... \n\n\n LOL.'
    },
    vapi: true, // Indique si cette classe vient d'un fichier .vapi.
    data: { // Des données à propos de la classe, qu'on parse une fois et qu'on réutilise plein de fois.
        attributes: [ // Les attributs de cette classe.
            {
                name: 'CCode',
                parameters: 'cname = ' // Oui, il n'y a pas de valeur après le =, parce que c'est une expression entre " et qu'elles sont ignorées.
            }
        ],
        type: 'class', // Indique qu'on a affaire à une classe.
        name: 'Hey', // Le nom de la classe.
        access: 'public', // Sa visibilité.
        inherits: 'Object' // Les classes dont elle hérite
    }
}

Et voilà c'est tout beau, près à être réutilisé pour afficher des suggestions !

L'algorithme qui fait ça

Je vais essayer de décomposer les grandes lignes de la méthode parse qui est chargée de faire toute cette découpe :

  • On a un tableau (this.scopes), qui contient les objets représentant la racine de chaque fichier ;
  • On trouve celui du fichier courant, ou on le crée : c'est la variable currentScope ;
  • On passe chaque caractère du code un à un, en ignorant les commentaires, les expressions litérales ("chaîne de caractères", par exemple) et tout ce qui ne nous intérèsse pas;
  • Les caractères sont ajoutés à une varibale token (qui en fait n'est pas un seul token, mais plusieurs) ;
  • Si le caractères actuel est { ou ;, ça veut dire qu'on arrive à une déclaration :
    • On va alors utiliset tout plein d'expression rationelles pour faire la découpe de notre token ;
    • On stocke ce qu'on a découpé dans la liste des enfants de currentScope, et l'objet qu'on a créé devient le currentScope ;
    • On réinitialise token;

Au final on se retrouve avec ce qu'on pourrait appeler un « arbre bi-directionnel » représentant les différentes déclarations de notre code. On peut alors l'explorer comme ceci :

const explore = (scope) => {
    // traitement des données du bloc de code ...
    // par exemple pour faire des suggestions

    for (const child of scope.children) {
        explore (child);
    }
};

for (const scope of this.scopes) {
    explore (scope);
}

Optimisations

Au début, je n'avait de propriété data contenant les différentes informations sur le bloc de code correspondant (son nom, son type, ses arguments, etc). Je devait donc réutiliser des Regex à chaque fois pour découper mes objets. Ce qui en plus de prendre de la place, ralentissait beaucoup Atom (se taper 10000 fois une dizaine de Regex différentes, ça fait pas du bien). J'ai donc pris un peu de temps pour réécrire ça comme il faut.

Une erreur un peu plus subtile, vient du fait que je stockait mes RegEx en « dur » dans mon code, comme ça.

if (klassMatch = token.match(/^(public |private |internal )?(abstract )?class ([\w\.]+)( :( [\w\.,]+)+)?( ?{)?$/)) {
    // récupérer les informations
}

Sauf que JavaScript (et à peu près tous les langages en fait) compile les RegEx lors de leur création, ce qui prend pas mal de temps aussi. J'ai donc changé ça, en stockant toutes les RegEx dont j'ai besoin dans this.re. C'est marrant, mais ça fait gagner un temps fou !

Une autre erreur que j'ai assez souvent fait avant de m'en rendre compte, est d'utiliser beaucoup de console.log (genre dans une boucle sur 10000 éléments). Atom est vraiment beaucoup ralenti, et on peut même le faire planter comme ça ! Maintenant, je préfère mettre des points d'arrêts dans le déboggueur.


Voilà ce premier article est fini, j'espère que ça vous aura intéréssé ou aidé. Le prochain devrait sortir bientôt !

Le fonctionnement de Valhalla, partie 2 : Découper le code

Cet article est donc le premier de la série où je vais vous expliquer le fonctionnement de Valhalla. Nous allons commencer par voir comment Valhalla découpe le code des différentes bibliothèques installées.

Tout d’abord, au lancement d’Atom, le pckage va être chargé, et la fonction activate que l’on trouve dans le fichier valhalla.js (on lui indique dans le manifeste). Dans cette fonction, on va faire tout un tas de chose, mais on va surtout initialiser une nouvelle instance de la classe ValaProvider qui fournit les suggestions lorsqu’on commence à taper quelque chose. Mais cette partie ne nous intéresse pas encore. C’est juste de ce bout de code, dans le constructeur de ValaProvider dont nous allons parler :

const vapiDir = atom.config.get('valhalla.vapiDir');
this.manager = new ScopeManager();

atom.workspace.observeTextEditors((editor) => {
    // dès qu'un éditeur est créé
    if (editor.getPath() && editor.getPath().endsWith('.vala')) {
        // si c'est un fichier Vala qui y est ouvert
        editor.onDidStopChanging((event) => {
            // quand il a été modifié, on le « découpe » (voir explication après)
            this.manager.parse(editor.getText(), editor.getPath());
        });
        // Et puis on le découpe aussi maintenant
        this.manager.parse(editor.getText(), editor.getPath());
    }
});

// On lit les fichiers .vapi
fs.readdir (vapiDir, (err, files) => {
    if (err) {
       console.error (err);
       return;
    }

    for (file of files) {
       if (file.endsWith('.vapi')) {
           let content = fs.readFileSync(path.join(vapiDir, file), 'utf-8');
           // On les « découpe »
           this.manager.parse(content, file);
       }
    }
});

Ici, on crée une nouvelle instance de la classe ScopeManager, qui a pour rôle de « découper » le code des bibliothèques installées (contenu dans des fichiers .vapi) et le code écrit par l’utilisateur d’Atom et nous en faire des objets, beaucoup plus simples à utiliser. Par exemple, après avoir été « découpée », une classe ressemblera à quelque chose comme ça :

{
    top: scopeParent, // Le scope parent (un espace de nom par exemple). Je ne l’ai pas réellement mis, car il contient une référence vers ces enfants, donc cet objet et on n’en finirait pas.
    at: [[0, 0] [10, 1]], // L’endroit où est définie cette classe dans le fichier.
    name: 'public class Hey : Object ', // La définition de la classe.
    children: [enfant1, enfant2, enfant3], // Les enfants de cette classe (méthodes, propriétés…), pareil que pour `top`, je ne les ait pas mis.
    file: 'bibli.vapi’, // Le fichier d' vient la classe.
    documentation: { // La documentation de cette classe. En général elle n’est pas dans les fichiers .vapi, mais dans les fichiers .vala (qui continnent le code écrit par l’utilisateur)
        short: 'Une super classe.',
        long: 'En vrai elle a trop la classe ! \n\n\n\n\n\n\n\n… \n\n\n Vous l’avez ?.'
    },
    vapi: true, // Indique si cette classe vient d’un fichier .vapi.
    data: { // Des données à propos de la classe, qu’on parse une fois et qu’on réutilise plein de fois.
        attributes: [ // Les attributs de cette classe.
            {
                name: 'CCode',
                parameters: 'cname = ' // Oui, il n’y a pas de valeur après le =, parce que c’est une expression entre " et qu’elles sont ignorées.
            }
        ],
        type: 'class', // Indique qu’on a affaire à une classe.
        name: 'Hey', // Le nom de la classe.
        access: 'public', // Sa visibilité.
        inherits: 'Object' // Les classes dont elle hérite
    }
}

Et voilà c’est tout beau, près à être réutilisé pour afficher des suggestions !

L’algorithme qui fait ça

Je vais essayer de décomposer les grandes lignes de la méthode parse qui est chargée de faire toute cette découpe :

  • On a un tableau (this.scopes), qui contient les objets représentant la racine de chaque fichier ;
  • On trouve celui du fichier courant, ou on le crée : c’est la variable currentScope ;
  • On passe chaque caractère du code un à un, en ignorant les commentaires, les expressions littérales ("chaîne de caractères", par exemple) et tout ce qui ne nous intéresse pas;
  • Les caractères sont ajoutés à une variable token (qui en fait n’est pas un seul token, mais plusieurs) ;
  • Si le charactère actuel est { ou ;, ça veut dire qu’on arrive à une déclaration :
    • On va alors utiliser tout plein d’expression rationnelles pour faire la découpe de notre token ;
    • On stocke ce qu’on a découpé dans la liste des enfants de currentScope, et l’objet qu’on a créé devient le currentScope ;
    • On réinitialise token;

Au final on se retrouve avec ce qu’on pourrait appeler un « arbre bi-directionnel » représentant les différentes déclarations de notre code. On peut alors l’explorer comme ceci :

const explore = (scope) => {
    // traitement des données du bloc de code…
    // par exemple pour faire des suggestions

    for (const child of scope.children) {
        explore (child);
    }
};

for (const scope of this.scopes) {
    explore (scope);
}

Optimisations

Au début, je n’avait de propriété data contenant les différentes informations sur le bloc de code correspondant (son nom, son type, ses arguments, etc). Je devait donc réutiliser des Regex à chaque fois pour découper mes objets. Ce qui en plus de prendre de la place, ralentissait beaucoup Atom (se taper 10000 fois une dizaine de Regex différentes, ça fait pas du bien). J’ai donc pris un peu de temps pour réécrire ça comme il faut.

Une erreur un peu plus subtile, vient du fait que je stockait mes RegEx en « dur » dans mon code, comme ça.

if (klassMatch = token.match(/^(public |private |internal )?(abstract )?class ([\w\.]+)( :( [\w\.,]+)+)?( ?{)?$/)) {
    // récupérer les informations
}

Sauf que JavaScript (et à peu près tous les langages en fait) compile les RegEx lors de leur création, ce qui prend pas mal de temps aussi. J’ai donc changé ça, en stockant toutes les RegEx dont j’ai besoin dans this.re. C’est marrant, mais ça fait gagner un temps fou !

Une autre erreur que j’ai assez souvent fait avant de m’en rendre compte, est d’utiliser beaucoup de console.log (genre dans une boucle sur 10000 éléments). Atom est vraiment beaucoup ralenti, et on peut même le faire planter comme ça ! Maintenant, je préfère mettre des points d’arrêts dans le déboggueur.


Voilà ce premier article est fini, j’espère que ça vous aura intéréssé ou aidé. Le prochain devrait sortir bientôt !

Le fonctionnnement de Valhalla, partie 1 : Introduction

Pour ceux qui ne le savent pas, je développe un package pour Atom nommé Valhalla. Il est écrit en JavaScript (ES6) et vous pouvez voir le code (libre bien-sûr) sur GitHub. Ce package vous fournit tout un tas d'outils pour vous rendre l'écriture de code dans le langage Vala plus simple.

J'ai décidé de créer une petite série d'article pour vous expliquer comment fonctionne ce package, car la création de package Atom est assez mal documentée (et en français n'en parlons pas), et que même si je prévois quelque chose de plus complet (mais pas pour tout de suite), ça permet d'avoir un aperçu de comment Atom gère ses packages.

Principaux modules

Valhalla se décompose globalement ainsi :

  • Un module qui va chercher les fichiers .vapi (où sont décrits les différentes classes, méthodes, interfaces et autres disponibles avec les bibliothèques installées sur le sytème) et qui les décompose en objets JavaScript utilisables plus tard pour plusieurs choses ;
  • Un autre module qui affiche les suggestions, à partir de ce qu'on a récupérer dans les fichier .vapi ;
  • Un autre qui est chargé d'afficher une petite documentation, toujours grâce à ce qu'on a récupéré avant ;
  • Des petits outils comme la création instantanée de classe ou d'interfaces, l'intégration avec le package atom-build, ou encore une fonctionnalité « Entourer avec ... ».

Chacun de ses points aura son article détaillé dans la petite série d'article que je prévoie écrire. Au niveau du rythme, rien n'est vraiment prévu, même si je pense faire quelque chose comme un article par semaine. En tout cas le premier est déjà disponible, juste ici.

Le fonctionnement de Valhalla, partie 1 : Introduction

Pour ceux qui ne le savent pas, je développe un package pour Atom nommé Valhalla. Il est écrit en JavaScript (ES6) et vous pouvez voir le code (libre bien-sûr) sur GitHub. Ce package vous fournit tout un tas d’outils pour vous rendre l’écriture de code dans le langage Vala plus simple.

J’ai décidé de créer une petite série d’article pour vous expliquer comment fonctionne ce package, car la création de package Atom est assez mal documentée (et en français n’en parlons pas), et que même si je prévois quelque chose de plus complet (mais pas pour tout de suite), ça permet d’avoir un aperçu de comment Atom gère ses packages.

Principaux modules

Valhalla se décompose globalement ainsi :

  • Un module qui va chercher les fichiers .vapi (où sont décrits les différentes classes, méthodes, interfaces et autres disponibles avec les bibliothèques installées sur le sytème) et qui les décompose en objets JavaScript utilisables plus tard pour plusieurs choses ;
  • Un autre module qui affiche les suggestions, à partir de ce qu’on a récupéré dans les fichier .vapi ;
  • Un autre qui est chargé d’afficher une petite documentation, toujours grâce à ce qu’on a récupéré avant ;
  • Des petits outils comme la création instantanée de classe ou d’interfaces, l’intégration avec le package atom-build, ou encore une fonctionnalité « Entourer avec… ».

Chacun de ses points aura son article détaillé dans la petite série d’article que je prévoie écrire. Au niveau du rythme, rien n’est vraiment prévu, même si je pense faire quelque chose comme un article par semaine. En tout cas le premier est déjà disponible, juste ici.

Vérifier le style de son code Vala

Aujourd'hui, je me suis décidé à mettre en place un script qui vérifierait la qualité du code de mon projet, Valse, écrit en Vala. J'ai donc tir parti de Gitlab CI, qui est depuis quelques jours disponible sur les serveurs de Framagit.

Après quelques heures de travail, j'ai réussi à écrire ce script Ruby.

# encoding: UTF-8

def explore (top)
    files = Dir.entries(top)

    vala_files = []

    files.each do |file|
        if file == '.' or file == '..' or file == '.git'
            next
        end

        if file.end_with?('.vala')
            vala_files << file
        elsif File.directory?(top + '/' + file)
            in_subdir = explore(top + '/' + file)
            in_subdir.each do |sub_file|
                vala_files << file + '/' + sub_file
            end
        end
    end
    return vala_files
end

def check (file)
    content = ''
    errors = 0

    open(file,"r:UTF-8") do |f|
        line_num = 0
        in_comm = false
        while line = f.gets

            if line.index(/\*\//) != nil
                in_comm = false
                line = line.gsub(/.*\*\//, '')
            end

            if in_comm
                line_num += 1
                next
            end

            if line.index(/\/\*/) != nil
                in_comm = true
                line = line.gsub(/.*\/\*/, '')
            end

            # removing comments
            line = line.gsub(/\/\/.*/, '')
            content += line
            line_num += 1
            # don't use as
            if line.include?(' as ')
                puts 'In file ' + file + ', at line ' + line_num.to_s + ' : avoid using as.'
                errors += 1
            end

            #capitals const
            const_re = /const [[:graph:]]* (?<name>[[:graph:]]*)/
            res = const_re.match(line)
            if not res == nil
                name = res[1]
                maj_name = name.upcase
                if name != maj_name
                    puts 'In file ' + file + ', at line ' + line_num.to_s + ' : constant ' + name + ' should be named ' + maj_name
                    errors += 1
                end
            end

            # never forget the space before a (. NEVER
            space_re = /(?!\()[[:graph:]]*\(.*\)/
            space_res = space_re.match(line.gsub(/".*"/, ''))
            if not space_res == nil
                puts 'In file ' + file + ', at line ' + line_num.to_s + ' : you forgoten a space before a ('
                errors += 1
            end
        end
    end

    content = content.gsub(/\/\*.*\*\//su, '')

    # one class or interface by file
    if content.scan(' class ').length + content.scan(' interface ').length > 1
        puts 'In file ' + file + ' : too many classes or interfaces defined here.'
        errors += 1
    end

    if errors > 0
        return true
    else
        return false
    end

end

def main

    to_check = explore ('.')

    bad_files = 0

    to_check.each do |vala|
        # puts 'Checking file ' + vala
        if check(vala)
            bad_files += 1
        end
    end

    puts 'bad files : ' + bad_files.to_s + ', total : ' + to_check.length.to_s

    coverage = 100 - (100 * bad_files.to_f / to_check.length.to_f)
    puts 'Coverage : ' + coverage.to_s[0..4]
end

main

Ce petit bout de code vous permet de vérifier :

  • Qu'on utilise pas le mot-clé as qui peux renvoyer null ;
  • Que vous appelez vos constantes avec des majuscules ;
  • Que vous n'oubliez pas d'espace avant d'ouvrir des parenthèses ;
  • Que chaque fichier ne contienne qu'un seule classe ou interface.

J'ai essayé de suivre les lignes de conduites d'elementary OS, mais il y a de nombreux points que je n'ai pas implémenté. J'améliorerai surement le script dans le futur, mais au pire, c'est du libre, vous pouvez l'adapter.

Là, je sais que vous avez un question :

Mais comment on intègre ton script à notre projet ?

Intégrer ce script à votre projet

Donc bien-sûr, pour utiliser ce script, il vous faudra une instance de Gitlab.

Ensuite, il faut que vous mettiez le contenu du script dans un fichier à la racine de votre projet nommé style.rb (ou autre, mais il faudra alors adapter votre fichier .gitlab-ci.yml).

Ensuite, il va justement falloir créer le fichier .gitlab-ci.yml. On va juste y mettre ceci.

image: ruby:latest

style:
  script:
    - ruby style.rb

Ici, on ne fait rien de très compliqué : on utilise l'image Docker de la dernière version de Ruby, et on lance notre script.

Là où ça devient plus intéréssant c'est qu'on peux savoir à quel point le code est propre. Pour cela, il faut que vous alliez dans les paramètres de votre dépôt, que vous activiez les builds, puis que vous rentriez dans Test coverage parsing, cette expression régulière : Coverage : \d+.\d+.

Maintenant, si vous lancez une build, vous aurez un joli affichage de la qualité de votre code.

Qualité du code

Vous remarquerez aussi que j'utilise la colonne Coverage, qui est plutôt faite pour les tests, mais bon, on a pas trop le choix.