Blog Arolla

DepRewriter : Dépréciations intelligentes pour la correction automatique de code client

Il y a quelques mois, nous avons soumis un article de recherche à The Journal of Object Technology, dans lequel nous présentons les dépréciations intelligentes qui peuvent automatiquement réparer le code cassé. Lorsque vous appelez une méthode obsolète, cette dernière signale un avertissement, puis l'outil réecrit dynamiquement l'appel de méthode obsolète. Cela se fait à l'aide de la règle de transformation fournie par les développeurs de bibliothèques dans le cadre de la déclaration de dépréciation. L'article est toujours en cours de révision et n'est pas encore accessible au grand public. Cependant, dans ce blog, j'utiliserai des termes simples et plusieurs exemples pour vous expliquer rapidement l'idée générale derrière DepRewriter.

Que sont les dépréciations ?

Je vais commencer par expliquer les dépréciations des méthodes. Si vous les connaissez déjà, n'hésitez pas à passer à la section suivante.

Imaginez l'exemple suivant. Vous utilisez une petite bibliothèque Java open source appelée Collection v1.0.0. Il fournit une classe Collection qui a une méthode includesAllOf(). La méthode vérifie si tous les événements d'une autre collection sont inclus dans cette collection.

public class Collection<E> {
  ...
  public boolean includesAllOf(Collection<E> values) {
    return values.forEach(value -> {
      this.includes(value);
    });
  }
  ...
}

Dans votre code, vous utilisez cette bibliothèque de la manière suivante :

import Collection;
...
products.includesAllOf([“apple”, “pear”]);

Après un certain temps, la nouvelle version de Collection est publiée, v2.0.0. Dans cette version, la méthode includesAllOf() a été renommée en includesAll().

Maintenant, votre code qui appelle toujours includesAllOf() se brisera en levant l'exception NotFound. Dans ce cas, nous disons que les développeurs ont introduit le changement de rupture (breaking change) dans la bibliothèque Collection. Cela rend le code client conçu pour l'ancienne version de la bibliothèque incompatible avec la nouvelle version.

La manière habituele de résoudre ces problèmes est la dépréciation. Au lieu de supprimer immédiatement la fonctionnalité d'une bibliothèque logicielle, elle est d'abord marquée comme obsolète ("à supprimer à l'avenir") ce qui garantit une compatibilité descendante et donne aux développeurs clients le temps de mettre à jour leur code.

Dans notre cas, Collection v2.0.0 ne doit pas remplacer includesAllOf() par includesAll(). Au lieu de cela, il devrait conserver l'ancienne méthode includesAllOf(), la marquer comme obsolète et ajouter également une nouvelle méthode includesAll(). Ensuite, dans la version suivante Collection v3.0.0, la méthode obsolète includesAllOf() sera finalement supprimée.

Les langages de programmation ont une syntaxe différente pour exprimer les dépréciations. En Java

public class Collection<E> {
  ...
  public boolean includesAll(Collection<E> values) {
    return values.forEach(value -> {
      this.includes(value);
    });
  }

  @Deprecated
  public boolean includesAllOf(Collection<E> values) {
    return this.includesAll(values);
  }
  ...
}

Remarque : Dans ce cas, l'ancienne et la nouvelle méthode ont la même implémentation, donc pour éviter la duplication de code, nous appelons simplement la nouvelle méthode à partir de l'ancienne.

Le dépréciations transformantes

Avec chaque nouvelle version d'une bibliothèque logicielle, certaines parties de l'API peuvent être obsolètes. Ces dépréciations sont signalées aux développeurs clients soit dans le cadre de la documentation (notes de version) soit sous la forme d'annotations (comme démontré dans la section précédente). Il est ensuite de la responsabilité des développeurs clients de trouver tous les appels aux méthodes dépréciées dans leur base de code et de les supprimer ou de les remplacer par un substitut correct de la nouvelle API. Les IDE modernes peuvent faciliter le travail en fournissant des outils de recherche puissants, en mettant en évidence le code obsolète et en affichant la ligne de code à partir de laquelle l'avertissement d'obsolescence a été déclenché. Pourtant, c'est une tâche ennuyeuse et répétitive qui pourrait être simplifiée et partiellement automatisée.

Pharo fournit un moteur de transformation des dépréciations (a.k.a. réécriture des dépréciations) appelé DepRewriter. Lors de la dépréciation d'une méthode, les développeurs de bibliothèques peuvent spécifier une règle de transformation qui sera utilisée pour corriger automatiquement le code client. Voici l'exemple d'une méthode dépréciée avec une règle de transformation écrite en Pharo :

Collection >> includesAllOf: values
self
deprecated: ‘Use #includesAll: instead’
transformWith: ‘`@rec includesAllOf: `@arg’ ->
‘`@rec includesAll: `@arg’.

^ self includesAll: values

La première ligne déclare une méthode includesAllOf: de la classe Collection. La méthode prend un argument : valeur. Les lignes 2 à 5 désapprouvent cette méthode en appelant une autre méthode deprecated:transformWith: qui est comprise par toutes les sous-classes de Object. Le premier argument est une chaîne d'obsolescence qui sera affichée dans l'avertissement d'obsolescence : 'Use #includesAll: instead'. Le deuxième argument est une règle de transformation : ''@rec includesAllOf : '@arg' -> ''@rec includesAll : '@arg'. La dernière ligne de la méthode obsolète appelle la nouvelle méthode includesAll:.

La règle de transformation se compose de deux parties : l'antécédent (côté gauche) et le conséquent (côté droit suspendu). Les deux sont des expressions écrites dans un langage de correspondance de motifs (similaire à regex mais pour le code). L'antécédent correspond au morceau de code qui doit être remplacé et par conséquent définit le remplacement. Les jetons commençant par '@ sont des variables nommées. L'antécédent ci-dessus correspond à tous les appels de méthode à includeAllOf :. Le récepteur est stocké dans la variable '@rec, l'argent est stocké dans la variable '@arg. Ensuite, l'expression conséquente génère l'appel à includesAll : en utilisant le même récepteur et le même argument.

Lorsque le client appelle la méthode dépréciée includeAllOf:, il reçoit un avertissement de dépréciation, puis l'appel de méthode dépréciée dans le code client sera automatiquement remplacé par l'appel à includeAll:. Enfin, la nouvelle méthode includeAll: sera appelée.

Cette approche est particulièrement utile pour les langages à typage dynamique (par exemple Pharo, Python, etc.). La méthode includesAllOf: peut être implémentée par plusieurs classes, nous ne pouvons donc pas simplement remplacer tous les appels à includesAllOf: par includesAll: sans connaître le type de récepteur (certains de ces appels peuvent invoquer une autre méthode non obsolète avec le même nom). DepRewriter résout ce problème en trouvant dynamiquement l'appel de méthode correct en parcourant la pile d'appels. Ensuite, il corrige automatiquement le code client sans interrompre son exécution.

Les développeurs clients peuvent modifier les configurations de DepRewriter pour désactiver les avertissements ou désactiver la réécriture automatique et recevoir une demande explicite avant toute modification de leur code.

References

  • Stéphane Ducasse, Guillermo Polito, Oleksandr Zaitsev, Marcus Denker, and Pablo Tesone. Deprewriter: On the fly rewriting method deprecations (2020) [unpublished, submitted to JOT]
  • Stéphane Ducasse and Oleksandr Zaitsev. ParseTreeWriter Explained. Square Bracket Associates, November 2020 [draft]
Plus de publications
Plus de publications

Ph.D. Student at Inria Lille. Researcher of Software Evolution at Arolla, Paris. MSc. in Data Science at the Ukrainian Catholic University.

Comments are closed.