Estoy trabajando en una instalación de finalización (intellisense) para C # en emacs.
La idea es que, si un usuario escribe un fragmento y luego solicita la finalización mediante una combinación de teclas en particular, la función de finalización utilizará la reflexión .NET para determinar las posibles finalizaciones.
Hacer esto requiere que se conozca el tipo de cosa que se está completando. Si es una cadena, hay un conjunto conocido de posibles métodos y propiedades; si es un Int32, tiene un conjunto separado y así sucesivamente.
Usando semántica, un paquete de código lexer / parser disponible en emacs, puedo localizar las declaraciones de variables y sus tipos. Dado eso, es sencillo usar la reflexión para obtener los métodos y propiedades del tipo, y luego presentar la lista de opciones al usuario. (Ok, no es muy sencillo de hacer dentro de emacs, pero usando la capacidad de ejecutar un proceso de PowerShell dentro de emacs , se vuelve mucho más fácil. Escribo un ensamblaje .NET personalizado para hacer la reflexión, lo cargo en el PowerShell y luego elisp se ejecuta dentro emacs puede enviar comandos a powershell y leer respuestas, a través de comint. Como resultado, emacs puede obtener los resultados de la reflexión rápidamente).
El problema llega cuando el código se usa var
en la declaración de lo que se está completando. Eso significa que el tipo no se especifica explícitamente y la finalización no funcionará.
¿Cómo puedo determinar de manera confiable el tipo real utilizado, cuando la variable se declara con la var
palabra clave? Para que quede claro, no necesito determinarlo en tiempo de ejecución. Quiero determinarlo en "Tiempo de diseño".
Hasta ahora tengo estas ideas:
- compilar e invocar:
- extraer la declaración de declaración, por ejemplo, `var foo =" un valor de cadena ";`
- concatenar una sentencia `foo.GetType ();`
- compilar dinámicamente el C # resultante, fragmentarlo en un nuevo ensamblado
- cargue el ensamblado en un nuevo AppDomain, ejecute el fragmento y obtenga el tipo de retorno.
- descargar y desechar el conjunto
Sé cómo hacer todo esto. Pero suena terriblemente pesado, para cada solicitud de finalización en el editor.
Supongo que no necesito un AppDomain nuevo cada vez. Podría reutilizar un solo AppDomain para múltiples ensamblajes temporales y amortizar el costo de configurarlo y desmontarlo, en múltiples solicitudes de finalización. Eso es más una modificación de la idea básica.
- compilar e inspeccionar IL
Simplemente compile la declaración en un módulo y luego inspeccione el IL para determinar el tipo real que fue inferido por el compilador. ¿Cómo sería esto posible? ¿Qué usaría para examinar el IL?
¿Alguna idea mejor por ahí? Comentarios sugerencias?
EDITAR : pensando en esto más a fondo, compilar e invocar no es aceptable, porque la invocación puede tener efectos secundarios. Por tanto, hay que descartar la primera opción.
Además, creo que no puedo asumir la presencia de .NET 4.0.
ACTUALIZACIÓN : la respuesta correcta, que no se mencionó anteriormente, pero que Eric Lippert señaló con delicadeza, es implementar un sistema de inferencia de tipo de fidelidad total. Es la única forma de determinar de forma fiable el tipo de var en tiempo de diseño. Pero tampoco es fácil de hacer. Debido a que no me hago ilusiones de querer intentar construir tal cosa, tomé el atajo de la opción 2: extraer el código de declaración relevante y compilarlo, luego inspeccionar el IL resultante.
Esto realmente funciona, para un subconjunto justo de escenarios de finalización.
Por ejemplo, suponga que en los siguientes fragmentos de código, el? es la posición en la que el usuario solicita la finalización. Esto funciona:
var x = "hello there";
x.?
La finalización se da cuenta de que x es una cadena y proporciona las opciones adecuadas. Lo hace generando y luego compilando el siguiente código fuente:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... y luego inspeccionando el IL con una simple reflexión.
Esto también funciona:
var x = new XmlDocument();
x.?
El motor agrega las cláusulas using apropiadas al código fuente generado, de modo que se compile correctamente, y luego la inspección de IL es la misma.
Esto también funciona:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Simplemente significa que la inspección de IL tiene que encontrar el tipo de la tercera variable local, en lugar de la primera.
Y esto:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... que es solo un nivel más profundo que el ejemplo anterior.
Pero lo que no funciona es la finalización de cualquier variable local cuya inicialización dependa en cualquier punto de un miembro de instancia o argumento del método local. Me gusta:
var foo = this.InstanceMethod();
foo.?
Ni la sintaxis LINQ.
Tendré que pensar en lo valiosas que son esas cosas antes de considerar abordarlas a través de lo que definitivamente es un "diseño limitado" (palabra cortés para hack) para completar.
Un enfoque para abordar el problema de las dependencias de los argumentos del método o de los métodos de instancia sería reemplazar, en el fragmento de código que se genera, se compila y luego se analiza por IL, las referencias a esas cosas con vars locales "sintéticas" del mismo tipo.
Otra actualización : la finalización en vars que dependen de los miembros de la instancia, ahora funciona.
Lo que hice fue interrogar el tipo (mediante semántica) y luego generar miembros sustitutos sintéticos para todos los miembros existentes. Para un búfer de C # como este:
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...
... el código generado que se compila, para que pueda aprender del IL de salida el tipo de var nnn local, se ve así:
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);
}
}
}
Todos los miembros de tipo instancia y estático están disponibles en el código esqueleto. Se compila con éxito. En ese punto, determinar el tipo de var local es sencillo a través de Reflection.
Lo que hace esto posible es:
- la capacidad de ejecutar powershell en emacs
- el compilador de C # es realmente rápido. En mi máquina, se necesitan aproximadamente 0,5 segundos para compilar un ensamblado en memoria. No lo suficientemente rápido para el análisis entre pulsaciones de teclas, pero lo suficientemente rápido como para admitir la generación bajo demanda de listas de finalización.
Todavía no he investigado LINQ.
Eso será un problema mucho mayor porque el lexer / analizador semántico emacs tiene para C #, no "hace" LINQ.