Je travaille sur une installation de complétion (intellisense) pour C # dans emacs.
L'idée est que, si un utilisateur tape un fragment, puis demande la complétion via une combinaison de touches particulière, la fonction de complétion utilisera la réflexion .NET pour déterminer les compléments possibles.
Pour ce faire, il faut connaître le type de chose en cours de réalisation. S'il s'agit d'une chaîne, il existe un ensemble connu de méthodes et de propriétés possibles; s'il s'agit d'un Int32, il a un ensemble distinct, etc.
En utilisant la sémantique, un paquet de lexer/parseur de code disponible dans emacs, je peux localiser les déclarations de variables et leurs types. Compte tenu de cela, il est simple d'utiliser la réflexion pour obtenir les méthodes et les propriétés du type, puis de présenter la liste des options à l'utilisateur. (Ok, pas tout à fait simple à faire à l'intérieur emacs, mais en utilisant la possibilité d'exécuter un processus powershell dans emacs , cela devient beaucoup plus facile. J'écris un assemblage .NET personnalisé pour faire de la réflexion, le charger dans le powershell, puis elisp s'exécutant dans emacs peut envoyer des commandes au powershell et lire les réponses, via comint. En conséquence, emacs peut obtenir rapidement les résultats de la réflexion.)
Le problème survient lorsque le code utilise var
dans la déclaration de la chose en cours. Cela signifie que le type n'est pas spécifié explicitement et que l'achèvement ne fonctionnera pas.
Comment puis-je déterminer de manière fiable le type réel utilisé, lorsque la variable est déclarée avec le mot clé var
? Juste pour être clair, je n'ai pas besoin de le déterminer lors de l'exécution. Je veux le déterminer au "Design time".
Jusqu'à présent, j'ai ces idées:
Je sais faire tout ça. Mais cela semble terriblement lourd, pour chaque demande d'achèvement dans l'éditeur.
Je suppose que je n'ai pas besoin d'un nouveau nouvel AppDomain à chaque fois. Je pourrais réutiliser un seul AppDomain pour plusieurs assemblages temporaires et amortir le coût de sa configuration et de sa suppression, sur plusieurs demandes d'achèvement. C'est plus un ajustement de l'idée de base.
Compilez simplement la déclaration dans un module, puis inspectez l'IL, pour déterminer le type réel qui a été déduit par le compilateur. Comment serait-ce possible? Que devrais-je utiliser pour examiner l'IL?
De meilleures idées là-bas? Commentaires? suggestions?
[~ # ~] modifier [~ # ~] - en y réfléchissant davantage, la compilation et l'invocation ne sont pas acceptables, car l'invocation peut avoir un côté effets. La première option doit donc être exclue.
En outre, je pense que je ne peux pas supposer la présence de .NET 4.0.
[~ # ~] mise à jour [~ # ~] - La bonne réponse, non mentionnée ci-dessus, mais doucement soulignée par Eric Lippert, est d'implémenter une système d'inférence de type fidélité. C'est le seul moyen de déterminer de manière fiable le type de var au moment de la conception. Mais ce n'est pas facile non plus. Parce que je ne me fais aucune illusion que je veux essayer de construire une telle chose, j'ai pris le raccourci de l'option 2 - extraire le code de déclaration pertinent, le compiler, puis inspecter l'IL résultant.
Cela fonctionne réellement, pour un sous-ensemble équitable des scénarios d'achèvement.
Par exemple, supposons que dans les fragments de code suivants, le? est la position à laquelle l'utilisateur demande de terminer. Cela marche:
var x = "hello there";
x.?
L'achèvement se rend compte que x est une chaîne et fournit les options appropriées. Pour ce faire, il génère puis compile le code source suivant:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... puis inspecter l'IL avec une simple réflexion.
Cela fonctionne également:
var x = new XmlDocument();
x.?
Le moteur ajoute les clauses using appropriées au code source généré, afin qu'il se compile correctement, puis l'inspection IL est la même.
Cela fonctionne aussi:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Cela signifie simplement que l'inspection IL doit trouver le type de la troisième variable locale, au lieu de la première.
Et ça:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... qui est juste un niveau plus profond que l'exemple précédent.
Mais ce qui ne fonctionne pas fonctionne, c'est l'achèvement de toute variable locale dont l'initialisation dépend à tout moment d'un membre d'instance, ou d'un argument de méthode locale. Comme:
var foo = this.InstanceMethod();
foo.?
Ni la syntaxe LINQ.
Je devrai réfléchir à la valeur de ces choses avant d'envisager de les aborder via ce qui est définitivement un "design limité" (mot poli pour pirater) pour l'achèvement.
Une approche pour résoudre le problème des dépendances sur les arguments de méthode ou les méthodes d'instance serait de remplacer, dans le fragment de code qui est généré, compilé puis analysé par IL, les références à ces choses par des variables locales "synthétiques" du même type.
Une autre mise à jour - l'achèvement des variables qui dépendent des membres de l'instance, fonctionne désormais.
Ce que j'ai fait a été d'interroger le type (via la sémantique), puis de générer des membres de remplacement synthétiques pour tous les membres existants. Pour un tampon C # comme celui-ci:
public class CsharpCompletion
{
private static int PrivateStaticField1 = 17;
string InstanceMethod1(int index)
{
...lots of code here...
return result;
}
public void Run(int count)
{
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
var fff = nnn.?
...more code here...
... le code généré qui est compilé, afin que je puisse apprendre de la sortie IL le type de la var nnn locale, ressemble à ceci:
namespace Nsbwhi0rdami {
class CsharpCompletion {
private static int PrivateStaticField1 = default(int);
string InstanceMethod1(int index) { return default(string); }
void M0zpstti30f4 (int count) {
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
}
}
}
Tous les membres d'instance et de type statique sont disponibles dans le code squelette. Il se compile avec succès. À ce stade, la détermination du type de var local est simple via la réflexion.
Ce qui rend cela possible, c'est:
Je n'ai pas encore examiné LINQ.
Ce sera un problème beaucoup plus important car le lexer/analyseur sémantique emacs a pour C #, ne "fait" pas LINQ.
Je peux vous décrire comment nous le faisons efficacement dans le "vrai" IDE C #.
La première chose que nous faisons est d'exécuter une passe qui analyse uniquement les éléments de "haut niveau" dans le code source. Nous sautons tous les corps de méthode. Cela nous permet de constituer rapidement une base de données d'informations sur l'espace de noms, les types et les méthodes (et les constructeurs, etc.) dans le code source du programme. L'analyse de chaque ligne de code dans chaque corps de méthode prendrait beaucoup trop de temps si vous essayez de le faire entre les frappes.
Lorsque le IDE doit déterminer le type d'une expression particulière à l'intérieur d'un corps de méthode - disons que vous avez tapé "foo". Nous devons déterminer quels sont les membres de foo - - nous faisons la même chose; nous sautons autant de travail que possible.
Nous commençons par une passe qui analyse uniquement les déclarations variable locale dans cette méthode. Lorsque nous exécutons cette passe, nous faisons un mappage à partir d'une paire de "portée" et de "nom" vers un "déterminant de type". Le "type determiner" est un objet qui représente la notion de "je peux déterminer le type de ce local si j'en ai besoin". Déterminer le type de section locale peut coûter cher, nous voulons donc reporter ce travail si nous en avons besoin.
Nous avons maintenant une base de données paresseusement construite qui peut nous dire le type de chaque local. Donc, revenons à ce "foo". - nous déterminons dans quelle instruction l'expression appropriée se trouve, puis exécutons l'analyseur sémantique contre cette instruction uniquement. Par exemple, supposons que vous ayez le corps de la méthode:
String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.
et maintenant nous devons comprendre que foo est de type char. Nous construisons une base de données qui contient toutes les métadonnées, les méthodes d'extension, les types de code source, etc. Nous construisons une base de données qui a des déterminants de type pour x, y et z. Nous analysons l'énoncé contenant l'expression intéressante. Nous commençons par le transformer syntaxiquement en
var z = y.Where(foo=>foo.
Afin de déterminer le type de foo, nous devons d'abord connaître le type de y. Donc, à ce stade, nous demandons au déterminant de type "quel est le type de y"? Il démarre ensuite un évaluateur d'expression qui analyse x.ToCharArray () et demande "quel est le type de x"? Nous avons un déterminant de type pour celui qui dit "J'ai besoin de rechercher" String "dans le contexte actuel". Il n'y a pas de type String dans le type actuel, donc nous regardons dans l'espace de noms. Ce n'est pas là non plus, donc nous regardons dans les directives using et découvrons qu'il y a un "using System" et que System a un type String. OK, c'est donc le type de x.
Nous interrogeons ensuite les métadonnées de System.String pour le type de ToCharArray et il dit que c'est un System.Char []. Super. Nous avons donc un type pour y.
Maintenant, nous demandons "System.Char [] a-t-il une méthode où?" Non. Nous examinons donc les directives d'utilisation; nous avons déjà précalculé une base de données contenant toutes les métadonnées pour les méthodes d'extension qui pourraient éventuellement être utilisées.
Maintenant, nous disons "OK, il existe dix-huit douzaines de méthodes d'extension nommées Where in scope, l'une d'elles a-t-elle un premier paramètre formel dont le type est compatible avec System.Char []?" Nous commençons donc une série de tests de convertibilité. Cependant, les méthodes d'extension Where sont générique, ce qui signifie que nous devons faire l'inférence de type.
J'ai écrit un moteur d'inférence de type spécial qui peut gérer les inférences incomplètes du premier argument à une méthode d'extension. Nous exécutons l'inférence de type et découvrons qu'il existe une méthode Where qui prend un IEnumerable<T>
, Et que nous pouvons faire une inférence de System.Char [] à IEnumerable<System.Char>
, Donc T est System.Char .
La signature de cette méthode est Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
, et nous savons que T est System.Char. Nous savons également que le premier argument entre parenthèses à la méthode d'extension est un lambda. Nous commençons donc un inféreur de type d'expression lambda qui dit "le paramètre formel foo est supposé être System.Char", utilisez ce fait lors de l'analyse du reste du lambda.
Nous avons maintenant toutes les informations dont nous avons besoin pour analyser le corps de la lambda, qui est "foo". Nous recherchons le type de foo, nous découvrons que selon le liant lambda c'est System.Char, et nous avons terminé; nous affichons les informations de type pour System.Char.
Et nous faisons tout sauf l'analyse de "haut niveau" entre les frappes. C'est le vrai truc délicat. En fait, écrire toute l'analyse n'est pas difficile; cela le rend assez rapide que vous pouvez le faire à une vitesse de frappe qui est le vrai bit délicat.
Bonne chance!
Je peux vous dire à peu près comment Delphi IDE fonctionne avec le compilateur Delphi pour faire intellisense (la compréhension du code est ce que Delphi l'appelle). Ce n'est pas applicable à 100% à C #, mais c'est une approche intéressante qui mérite considération.
La plupart des analyses sémantiques dans Delphi sont effectuées dans l'analyseur lui-même. Les expressions sont saisies au fur et à mesure de leur analyse, sauf dans les cas où cela n'est pas facile - dans ce cas, une analyse prospective est utilisée pour déterminer ce qui est prévu, puis cette décision est utilisée dans l'analyse.
L'analyse est en grande partie une descente récursive LL (2), à l'exception des expressions, qui sont analysées en utilisant la priorité de l'opérateur. L'une des choses distinctes à propos de Delphi est qu'il s'agit d'un langage à passage unique, donc les constructions doivent être déclarées avant d'être utilisées, donc aucune passe de niveau supérieur n'est nécessaire pour diffuser ces informations.
Cette combinaison de fonctionnalités signifie que l'analyseur possède à peu près toutes les informations nécessaires à la compréhension du code pour tout point où il est nécessaire. La façon dont cela fonctionne est la suivante: le IDE informe le lexeur du compilateur de la position du curseur (le point où le code est souhaité) et le lexer le transforme en un jeton spécial (il est appelé le jeton kibitz). Chaque fois que l'analyseur rencontre ce jeton (qui pourrait être n'importe où), il sait que c'est le signal pour renvoyer toutes les informations qu'il a à l'éditeur. Il le fait en utilisant un longjmp car il est écrit en C; ce qu'il fait, il informe l'appelant ultime du type de construction syntaxique (c'est-à-dire le contexte grammatical) dans lequel le point kibitz a été trouvé, ainsi que toutes les tables symboliques nécessaires pour ce point. Ainsi, par exemple, si le contexte est dans une expression qui est un argument à une méthode, nous pouvons vérifier les surcharges de la méthode, regarder les types d'arguments et filtrer les symboles valides uniquement à ceux qui peuvent se résoudre à ce type d'argument (cela réduit beaucoup de problèmes inutiles dans la liste déroulante Si c'est dans un contexte de portée imbriquée (par exemple après un "."), L'analyseur aura renvoyé une référence à la portée, et le IDE peut énumérer tous les symboles trouvés dans cette portée.
D'autres choses sont également faites; par exemple, les corps de méthode sont ignorés si le jeton kibitz ne se trouve pas dans leur plage - cela est fait de manière optimiste, et annulé s'il saute le jeton. L'équivalent des méthodes d'extension - les assistants de classe dans Delphi - ont une sorte de cache versionné, donc leur recherche est assez rapide. Mais l'inférence de type générique de Delphi est beaucoup plus faible que celle de C #.
Maintenant, à la question spécifique: inférer les types de variables déclarées avec var
est équivalent à la façon dont Pascal infère le type de constantes. Cela vient du type de l'expression d'initialisation. Ces types sont construits de bas en haut. Si x
est de type Integer
et y
est de type Double
, alors x + y
sera de type Double
, car ce sont les règles du langage; etc. Vous suivez ces règles jusqu'à ce que vous ayez un type pour l'expression complète sur le côté droit, et c'est le type que vous utilisez pour le symbole sur la gauche.
Si vous ne voulez pas avoir à écrire votre propre analyseur pour construire l'arborescence de syntaxe abstraite, vous pouvez envisager d'utiliser les analyseurs de SharpDevelop ou MonoDevelop , les deux sont open source.
Les systèmes Intellisense représentent généralement le code à l'aide d'un arbre de syntaxe abstraite, ce qui leur permet de résoudre le type de retour de la fonction affectée à la variable 'var' de la même manière que le compilateur le fera. Si vous utilisez VS Intellisense, vous remarquerez peut-être qu'il ne vous donnera pas le type de var tant que vous n'aurez pas entré une expression d'affectation valide (résoluble). Si l'expression est toujours ambiguë (par exemple, elle ne peut pas déduire complètement les arguments génériques de l'expression), le type var ne sera pas résolu. Cela peut être un processus assez complexe, car vous devrez peut-être marcher assez profondément dans un arbre pour résoudre le type. Par exemple:
var items = myList.OfType<Foo>().Select(foo => foo.Bar);
Le type de retour est IEnumerable<Bar>
, mais pour résoudre ce problème, il fallait savoir:
IEnumerable
.OfType<T>
qui s'applique à IEnumerable.IEnumerable<Foo>
et il existe une méthode d'extension Select
qui s'applique à cela.foo => foo.Bar
a le paramètre foo de type Foo. Ceci est déduit de l'utilisation de Select, qui prend un Func<TIn,TOut>
et comme TIn est connu (Foo), le type de foo peut être déduit.IEnumerable<TOut>
et TOut peuvent être déduits du résultat de l'expression lambda, le type d'éléments résultant doit donc être IEnumerable<Bar>
.Puisque vous ciblez Emacs, il peut être préférable de commencer avec la suite CEDET. Tous les détails qu'Eric Lippert sont déjà couverts dans l'analyseur de code dans l'outil CEDET/Sémantique pour C++. Il y a aussi un analyseur C # (qui a probablement besoin d'un peu de TLC) donc les seules parties manquantes sont liées au réglage des parties nécessaires pour C #.
Les comportements de base sont définis dans des algorithmes de base qui dépendent de fonctions surchargeables définies par langage. Le succès du moteur de complétion dépend de la quantité de réglages effectués. Avec c ++ comme guide, obtenir un support similaire à C++ ne devrait pas être trop mauvais.
La réponse de Daniel suggère d'utiliser MonoDevelop pour effectuer l'analyse et l'analyse. Cela pourrait être un mécanisme alternatif au lieu de l'analyseur C # existant, ou il pourrait être utilisé pour augmenter l'analyseur existant.
C'est difficile de bien faire. Fondamentalement, vous devez modéliser la spécification/le compilateur de langage à travers la plupart des lexing/parsing/typechecking et construire un modèle interne du code source que vous pouvez ensuite interroger. Eric le décrit en détail pour C #. Vous pouvez toujours télécharger le code source du compilateur F # (partie du F # CTP) et jeter un œil à service.fsi
pour voir l'interface exposée du compilateur F # que le service de langage F # consomme pour fournir intellisense, des info-bulles pour les types inférés, etc. Cela donne une idée d'une "interface" possible si vous aviez déjà le compilateur disponible en tant qu'API Téléphoner à.
L'autre voie consiste à réutiliser les compilateurs tels quels comme vous le décrivez, puis à utiliser la réflexion ou à regarder le code généré. Ceci est problématique du point de vue du fait que vous avez besoin de "programmes complets" pour obtenir une sortie de compilation à partir d'un compilateur, alors que lorsque vous éditez du code source dans l'éditeur, vous n'avez souvent que des "programmes partiels" qui n'analysent pas encore, ne le faites pas avoir toutes les méthodes implémentées, etc.
En bref, je pense que la version "à petit budget" est très difficile à bien faire, et la version "réelle" est très, très difficile à bien faire. (Là où "difficile" mesure ici à la fois "l'effort" et la "difficulté technique".)
NRefactory fera cela pour vous.
Pour la solution "1", vous avez une nouvelle fonctionnalité dans .NET 4 pour le faire rapidement et facilement. Donc, si vous pouvez convertir votre programme en .NET 4, ce serait votre meilleur choix.