Tout ingénieur s'efforce de rendre son processus de travail aussi optimisé que possible. En tant que développeurs iOS mobiles, nous devons très souvent travailler avec des structures linguistiques uniformes. Apple améliore les outils de développement en mettant beaucoup d'efforts pour nous faciliter la programmation: la mise en évidence du langage, les méthodes de complétion automatique et de nombreuses autres fonctionnalités IDE permettent à nos doigts de suivre le rythme des idées dans nos têtes.

Que fait un ingénieur lorsque l'outil requis est manquant? Certes, il fera tout lui-même! Plus tôt, nous avons
parlé de la création de nos outils personnalisés, maintenant parlons de la façon de modifier Xcode et de le faire fonctionner selon vos règles.
Nous avons pris la tâche de
JIRA Swift et créé un outil qui se transforme
s'il est laissé en une construction de
garde équivalente.

Depuis la neuvième version, Xcode fournit un nouveau mécanisme de refactorisation qui peut convertir le code localement, dans le même fichier source Swift, ou globalement lorsque vous renommez une méthode ou une propriété qui se produit dans plusieurs fichiers, même si elles sont dans des langues différentes.
Le refactoring local est entièrement implémenté dans le compilateur et le framework SourceKit, la fonctionnalité est située dans le référentiel Swift open source et est écrite en C ++. La modification du refactoring global est actuellement inaccessible aux gens ordinaires, car la base de code Xcode est fermée. Par conséquent, nous allons nous attarder sur l'histoire locale et parler de la façon de répéter notre expérience.
Ce dont vous avez besoin pour créer votre propre outil de refactoring local:
- Comprendre C ++
- Connaissance de base du compilateur
- Comprendre ce qu'est l'AST et comment l'utiliser
- Code source Swift
- Guide swift / docs / refactoring / SwiftLocalRefactoring.md
- Beaucoup de patience
Un peu sur l'AST
Un peu de bases théoriques avant de plonger dans la pratique. Voyons comment fonctionne l'architecture du compilateur Swift. Tout d'abord, le compilateur est responsable de la transformation du code en code machine exécutable.

Parmi les étapes de transformation présentées, la plus intéressante pour nous est la génération d'un arbre de syntaxe abstraite (AST) - un graphique dans lequel les sommets sont des opérateurs et les feuilles sont leurs opérandes.

Les arbres de syntaxe sont utilisés dans l'analyseur. AST est utilisé comme représentation interne dans le compilateur / interprète d'un programme informatique pour optimiser et générer du code.
Une fois l'AST généré, l'analyse est effectuée pour créer l'AST avec une vérification de type qui a été traduite dans le Swift Intermediate Language. SIL est converti, optimisé, déclassé en LLVM IR, qui se compile finalement en code machine.
Pour créer un outil de refactoring, nous devons comprendre AST et être capable de travailler avec lui. Ainsi, l'outil pourra fonctionner correctement avec les parties du code que nous voulons traiter.
Pour générer l'AST d'un fichier, exécutez la commande: swiftc -dump-ast
MyFile.swift
Vous trouverez ci-dessous la sortie vers la console AST de la fonction if let , mentionnée précédemment.

Il existe trois principaux types de nœuds dans Swift AST:
- déclarations (sous-classes de type Decl),
- expressions (sous-classes de type Expr),
- opérateurs (sous-classes de type Stmt).
Ils correspondent aux trois entités utilisées dans la langue swift elle-même. Les noms des fonctions, structures, paramètres sont des déclarations. Les expressions sont des entités qui renvoient une valeur; par exemple, appeler des fonctions. Les opérateurs font partie du langage qui définissent le flux de contrôle de l'exécution du code, mais ne renvoient pas de valeur (par exemple, if ou do-catch).
Il s'agit d'un minimum suffisant que vous devez connaître AST pour votre prochain travail.
Fonctionnement théorique de l'outil de refactoring
Pour implémenter des outils de refactoring, vous avez besoin d'informations spécifiques sur la zone du code que vous allez modifier. Les développeurs disposent d'entités auxiliaires qui accumulent des données. Le premier, ResolvedCursorInfo (refactoring basé sur le curseur), vous indique si nous sommes au début d'une expression. Si tel est le cas, l'objet de compilation correspondant à cette expression est renvoyé. La deuxième entité, RangeInfo (refactoring basé sur la plage), encapsule les données sur la plage d'origine (par exemple, combien de points d'entrée et de sortie elle possède).
Le refactoring basé sur le curseur est initié par l'emplacement du curseur dans le fichier source. Les actions de refactoring implémentent les méthodes que le mécanisme de refactoring utilise pour afficher les actions disponibles dans l'EDI et pour effectuer des transformations. Exemples d'actions basées sur le curseur: Aller à la définition, aide rapide, etc.

Considérez les actions habituelles du côté technique:
- Lorsque vous sélectionnez un emplacement dans l'éditeur Xcode, une demande est faite à sourcekitd (le framework responsable de la mise en évidence, de l'achèvement du code, etc.) pour afficher les actions de refactoring disponibles.
- Chaque action disponible est demandée par l'objet ResolvedCursorInfo pour vérifier si cette action s'applique au code sélectionné.
- La liste des actions applicables est renvoyée comme réponse de sourcekitd et affichée dans Xcode.
- Xcode applique ensuite les modifications Ă l'outil de refactoring.
Le refactoring basé sur la plage est initié en sélectionnant une plage continue de code dans le fichier source.

Dans ce cas, l'outil de refactoring passera par une chaĂ®ne d'appel similaire dĂ©crite. La diffĂ©rence est que lorsqu'elle est implĂ©mentĂ©e, l'entrĂ©e est RangeInfo au lieu de ResolvedCursorInfo. Les lecteurs intĂ©ressĂ©s peuvent se rĂ©fĂ©rer Ă
Refactoring.cpp pour plus d'informations sur les exemples de la boîte à outils Apple.
Et maintenant à la pratique de la création d'un outil.
La préparation
Tout d'abord, vous devez télécharger et construire le compilateur Swift. Des instructions détaillées se trouvent dans le référentiel officiel (
readme.md ). Voici les raccourcis clavier pour le clonage de code:
mkdir swift-source cd swift-source git clone https:
Cmake est utilisé pour décrire la structure et les dépendances du projet. En l'utilisant, vous pouvez générer un projet pour Xcode (plus pratique) ou pour ninja (plus rapide) grâce à l'une des commandes:
./utils/build-script --debug --xcode
ou
swift/utils/build-script --debug-debuginfo
Une compilation réussie nécessite la dernière version de Xcode beta (10.2.1 au moment de la rédaction) - disponible sur le
site officiel d'Apple . Pour utiliser le nouveau Xcode pour construire le projet, vous devez enregistrer le chemin Ă l'aide de l'utilitaire xcode-select:
sudo xcode-select -s /Users/username/Xcode.app
Si nous avons utilisé l'indicateur --xcode pour construire le projet pour Xcode, respectivement, puis après plusieurs heures de compilation (nous en avons eu un peu plus de deux) dans le dossier de construction, nous trouverons le fichier Swift.xcodeproj. En ouvrant le projet, nous verrons le Xcode familier avec l'indexation, les points d'arrêt.
Pour créer un nouvel instrument, nous devons ajouter le code avec la logique de l'instrument au fichier: lib / IDE / Refactoring.cpp et définir deux méthodes isApplicable et performChange. Dans la première méthode, nous décidons de générer ou non l'option de refactorisation pour le code sélectionné. Et dans le second - comment convertir le code sélectionné pour appliquer le refactoring.
Une fois la préparation effectuée, il reste à mettre en œuvre les étapes suivantes:
- Développer la logique de l'outil (le développement peut se faire de plusieurs manières - via la chaîne d'outils, via Ninja, via Xcode; toutes les options seront décrites ci-dessous)
- Implémentez deux méthodes: isApplicable et performChange (ils sont responsables de l'accès à l'outil et de son fonctionnement)
- Diagnostiquez et testez l'outil terminé avant d'envoyer le PR au référentiel Swift officiel.
Tester le fonctionnement de l'outil via la chaîne d'outils
Cette méthode de développement vous prendra beaucoup de temps en raison du long assemblage des composants, mais le résultat est immédiatement visible dans Xcode - la façon de le vérifier manuellement.
Pour commencer, construisons la chaîne d'outils Swift à l'aide de la commande:
./utils/build-toolchain some_bundle_id
La compilation de la chaîne d'outils prendra encore plus de temps que la compilation du compilateur et des dépendances. La sortie est le fichier swift-LOCAL-yyyy-mm-dd.xctoolchain du dossier swift-nightly-install, que vous devez transférer vers Xcode: / Library / Developer / Toolchains /. Ensuite, dans les paramètres IDE, sélectionnez la nouvelle chaîne d'outils, redémarrez Xcode.

Sélectionnez un morceau de code que l'outil doit traiter et recherchez l'outil dans le menu contextuel.
Développement par des tests avec Ninja
Si le projet a été construit pour Ninja et que vous avez choisi la voie TDD, le développement par des tests avec Ninja est l'une des options qui vous conviendra. Inconvénients - vous ne pouvez pas définir de points d'arrêt, comme dans le développement via Xcode.
Nous devons donc vérifier que le nouvel outil est affiché dans Xcode lorsque l'utilisateur sélectionne la construction de garde dans le code source. Nous écrivons le test dans le fichier test / refactoring / RefactoringKind / basic.swift existant:
func testConvertToGuardExpr(idxOpt: Int?) { if let idx = idxOpt { print(idx) } }
Nous indiquons que lors de la mise en évidence du code entre 266 colonnes de ligne 3 et 268 colonnes de ligne 4, nous nous attendons à l'apparition d'un élément de menu avec un nouvel outil.
L'utilisation du script
lit.py peut fournir une rétroaction plus rapide à votre cycle de développement. Vous pouvez spécifier la combinaison de test qui vous intéresse. Dans notre cas, cette suite sera RefactoringKind:
./llvm/utils/lit/lit.py -sv ./build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64/refactoring/RefactoringKind/
Par conséquent, les tests de ce fichier uniquement seront lancés. Leur mise en œuvre prendra quelques dizaines de secondes. Plus d'informations sur lit.py seront discutées plus loin dans la section Diagnostics et tests.
Le test échoue, ce qui est normal pour le paradigme TDD. Après tout, jusqu'à présent, nous n'avons pas écrit une seule ligne de code avec la logique de l'outil.
Développement par débogage et Xcode
Et enfin, la dernière méthode de développement lorsque le projet a été construit sous Xcode. Le principal avantage est la possibilité de définir des points d'arrêt et de contrôler le débogage.
Lors de la création d'un projet sous Xcode, le fichier Swift.xcodeproj est créé dans le dossier build / Xcode-DebugAssert / swift-macosx-x86_64 /. Lorsque vous ouvrez ce fichier pour la première fois, il est préférable de choisir de créer des schémas manuellement pour générer ALL_BUILD et swift-refactor vous-même:

Ensuite, nous construisons le projet avec ALL_BUILD une fois, après quoi nous utilisons le schéma swift-refactor.
L'outil de refactorisation est compilé dans un fichier exécutable distinct - swift-refactor. L'aide de ce fichier peut être affichée à l'aide de l'indicateur –help. Les paramètres les plus intéressants pour nous sont:
-source-filename=<string>
Ils peuvent être spécifiés dans le schéma comme arguments. Vous pouvez maintenant définir des points d'arrêt pour qu'ils s'arrêtent aux endroits d'intérêt lors du démarrage de l'outil. De la manière habituelle, en utilisant les commandes
p et
po dans la console Xcode, affiche les valeurs des variables correspondantes.

Implémentation IsApplicable
La méthode isApplicable accepte un ResolvedRangeInfo avec des informations sur les nœuds AST du fragment de code sélectionné à l'entrée. En sortie de la méthode, il est décidé d'afficher ou non l'outil dans le menu contextuel Xcode. L'interface ResolvedRangeInfo complète se trouve dans le
fichier include / swift / IDE / Utils.h .
Considérez les champs de la classe ResolvedRangeInfo qui sont les plus utiles dans notre cas:
- RangeKind - la première chose à faire est de vérifier le type de la zone sélectionnée. Si la zone n'est pas valide (non valide), vous pouvez retourner false. Si le type nous convient, par exemple, SingleStatement ou MultiStatement, alors continuez;
- ContainedNodes - un tableau d'éléments AST qui tombent dans la plage sélectionnée. Nous voulons nous assurer que l'utilisateur sélectionne la plage dans laquelle entre la construction if let. Pour ce faire, nous prenons le premier élément du tableau et vérifions que cet élément correspond à IfStmt (la classe qui définit le nœud AST du nœud d'instruction du sous-type if). Ensuite, voir condition. Pour simplifier l'implémentation, nous afficherons l'outil uniquement pour les expressions avec une condition. Par le type de condition (CK_PatternBinding), nous déterminons que cela est autorisé.

Pour tester isApplicable, ajoutez l'exemple de code au fichier
test / refactoring / RefactoringKind / basic.swift .

Pour que le test simule un appel Ă notre outil, vous devez ajouter une ligne dans le fichier
tools / swift-refactor / swift-refactor.cpp .

Nous implémentons performChange
Cette méthode est appelée lorsqu'un outil de refactoring est sélectionné dans le menu contextuel. La méthode a accès à ResolvedRangeInfo, ainsi qu'à isApplicable. Nous utilisons ResolvedRangeInfo et écrivons la logique de l'outil de conversion de code.
Lors de la génération de code pour des jetons statiques (régis par la syntaxe du langage), vous pouvez utiliser des entités de l'espace de noms de jeton. Par exemple, pour le mot clé guard, utilisez tok :: kw_guard. Pour les jetons dynamiques (modifiés par le développeur, par exemple, le nom de la fonction), vous devez les sélectionner dans le tableau des éléments AST.
Pour déterminer où le code converti est inséré, nous utilisons la plage sélectionnée complète à l'aide de la construction RangeInfo.ContentRange.

Diagnostics et tests
Avant de terminer de travailler sur un outil, vous devez vérifier à nouveau l'exactitude de son travail. Les tests nous aideront à nouveau. Les tests peuvent être exécutés un à la fois ou avec toutes les étendues disponibles. La façon la plus simple d'exécuter l'intégralité de la suite de tests Swift est d'utiliser la commande --test sur utils / build-script, qui exécutera la suite de tests principale. L'utilisation de utils / build-script reconstruira toutes les cibles, ce qui peut augmenter considérablement le temps de cycle de débogage.
Assurez-vous d'exécuter les tests de validation utils / build-script --validation-test avant d'apporter des modifications majeures au compilateur ou à l'API.
Il existe une autre façon d'exécuter tous les tests unitaires du compilateur - via ninja, ninja check-swift depuis build / preset / swift-macosx-x86_64. Cela prendra environ 15 minutes.
Et enfin, l'option lorsque vous devez exécuter des tests séparément. Pour appeler directement le script lit.py à partir de LLVM, vous devez le configurer pour utiliser le répertoire de construction local. Par exemple:
% $ {LLVM_SOURCE_ROOT} /utils/lit/lit.py -sv $ {SWIFT_BUILD_DIR} / test-macosx-x86_64 / Parse /
Cela exécutera les tests dans le répertoire 'test / Parse /' pour macOS 64 bits. L'option -sv fournit un indicateur de l'exécution des tests et affiche les résultats des tests ayant échoué uniquement.
Lit.py a d'autres fonctionnalités utiles, telles que les tests de synchronisation et les tests de latence. Vous pouvez afficher ces fonctionnalités et d'autres avec lit.py -h. Les plus utiles se trouvent
ici .
Pour exécuter un test, écrivez:
./llvm/utils/lit/lit.py -sv ./build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64/refactoring/RefactoringKind/basic.swift
Si nous devons récupérer les dernières modifications du compilateur, nous devons mettre à jour toutes les dépendances et effectuer un rebase. Pour mettre à niveau, exécutez ./utils/update-checkout.
Conclusions
Nous avons réussi à atteindre notre objectif - faire un outil qui n'était pas auparavant dans l'IDE pour optimiser le travail. Si vous avez également des idées sur la façon d'améliorer les produits Apple et de faciliter la vie de toute la communauté iOS, n'hésitez pas à adopter le contre-branding, car c'est plus facile qu'il n'y paraît à première vue!
En 2015, Apple a téléchargé le code source de Swift dans le domaine public, ce qui a permis de plonger dans les détails d'implémentation de son compilateur. De plus, avec Xcode 9, vous pouvez ajouter des outils de refactoring locaux. Une connaissance de base de C ++ et d'un périphérique de compilation est suffisante pour rendre votre IDE préféré un peu plus pratique.
L'expérience décrite nous a été utile - en plus de créer un outil qui simplifie le processus de développement, nous avons acquis une connaissance vraiment hardcore du langage. Une boîte de Pandore légèrement ouverte avec des processus de bas niveau vous permet de voir les tâches quotidiennes sous un nouvel angle.
Nous espérons que les connaissances acquises enrichiront également votre compréhension du développement!
Le matériel a été co-écrit avec
@victoriaqb - Victoria Kashlina, développeur iOS.
Les sources
- Dispositif de compilateur Swift. 2e partie
- Comment construire un outil basé sur un compilateur Swift? Le guide pas à pas
- Dump du Swift AST pour un projet iOS
- Présentation du testeur de stress sourcekitd
- Test rapide
- [SR-5744] Action de refactorisation pour convertir if-let en guard-let et vice versa # 24566