Estou trabalhando em um recurso de conclusão (intellisense) para C # no emacs.
A ideia é que, se um usuário digitar um fragmento e, em seguida, solicitar a conclusão por meio de uma combinação de teclas específica, o recurso de conclusão usará o reflexo do .NET para determinar as conclusões possíveis.
Fazer isso requer que o tipo de coisa que está sendo completada seja conhecido. Se for uma string, há um conjunto conhecido de métodos e propriedades possíveis; se for um Int32, tem um conjunto separado e assim por diante.
Usando o semantic, um pacote de lexer / parser de código disponível no emacs, posso localizar as declarações de variáveis e seus tipos. Dado isso, é simples usar reflexão para obter os métodos e propriedades no tipo e, em seguida, apresentar a lista de opções ao usuário. (Ok, não é muito simples de fazer no emacs, mas usando a capacidade de executar um processo do PowerShell dentro do emacs , torna-se muito mais fácil. Eu escrevo um assembly .NET personalizado para fazer reflexão, carrego no PowerShell e, em seguida, elisp rodando dentro O emacs pode enviar comandos para o PowerShell e ler as respostas, via comint. Como resultado, o emacs pode obter os resultados da reflexão rapidamente.)
O problema chega quando o código usa var
na declaração do que está sendo concluído. Isso significa que o tipo não está especificado explicitamente e a conclusão não funcionará.
Como posso determinar com segurança o tipo real usado, quando a variável é declarada com a var
palavra - chave? Só para ficar claro, não preciso determinar isso em tempo de execução. Quero determiná-lo em "Tempo de design".
Até agora, tenho estas ideias:
- compilar e invocar:
- extraia a declaração de declaração, por exemplo, `var foo =" um valor de string ";`
- concatenar uma instrução `foo.GetType ();`
- compilar dinamicamente o fragmento C # resultante em um novo assembly
- carregue o assembly em um novo AppDomain, execute o framgment e obtenha o tipo de retorno.
- descarregar e descartar o conjunto
Eu sei fazer tudo isso. Mas soa terrivelmente pesado para cada solicitação de conclusão no editor.
Acho que não preciso de um novo AppDomain sempre. Eu poderia reutilizar um único AppDomain para várias montagens temporárias e amortizar o custo de configurá-lo e desmontá-lo em várias solicitações de conclusão. Isso é mais um ajuste da ideia básica.
- compilar e inspecionar IL
Basta compilar a declaração em um módulo e inspecionar o IL para determinar o tipo real inferido pelo compilador. Como isso seria possível? O que eu usaria para examinar o IL?
Alguma ideia melhor por aí? Comentários? sugestões?
EDITAR - pensando mais sobre isso, compilar e invocar não é aceitável, porque a invocação pode ter efeitos colaterais. Portanto, a primeira opção deve ser descartada.
Além disso, acho que não posso assumir a presença do .NET 4.0.
ATUALIZAÇÃO - A resposta correta, não mencionada acima, mas gentilmente apontada por Eric Lippert, é implementar um sistema de inferência de tipo de fidelidade total. É a única maneira de determinar com segurança o tipo de uma var em tempo de design. Mas também não é fácil de fazer. Como não tenho ilusões de que quero tentar construir tal coisa, peguei o atalho da opção 2 - extrair o código de declaração relevante, compilá-lo e, em seguida, inspecionar o IL resultante.
Isso realmente funciona, para um subconjunto razoável dos cenários de conclusão.
Por exemplo, suponha que nos fragmentos de código a seguir, o? é a posição em que o usuário pede a conclusão. Isso funciona:
var x = "hello there";
x.?
A conclusão percebe que x é uma String e fornece as opções apropriadas. Ele faz isso gerando e compilando o seguinte código-fonte:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... e então inspecionando o IL com reflexão simples.
Isso também funciona:
var x = new XmlDocument();
x.?
O mecanismo adiciona as cláusulas using apropriadas ao código-fonte gerado, para que ele seja compilado corretamente e, então, a inspeção IL é a mesma.
Isso também funciona:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Significa apenas que a inspeção IL precisa encontrar o tipo da terceira variável local, em vez da primeira.
E isto:
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 é apenas um nível mais profundo que o exemplo anterior.
Mas, o que não funciona é a conclusão de qualquer variável local cuja inicialização dependa em qualquer ponto de um membro da instância ou do argumento do método local. Gostar:
var foo = this.InstanceMethod();
foo.?
Nem sintaxe LINQ.
Terei que pensar sobre o quão valioso essas coisas são antes de considerar abordá-las através do que é definitivamente um "design limitado" (palavra educada para hack) para conclusão.
Uma abordagem para abordar o problema com dependências em argumentos de método ou métodos de instância seria substituir, no fragmento de código que é gerado, compilado e então analisado por IL, as referências a essas coisas por vars locais "sintéticos" do mesmo tipo.
Outra atualização - conclusão em vars que dependem de membros da instância, agora funciona.
O que fiz foi interrogar o tipo (via semântica) e, em seguida, gerar membros substitutos sintéticos para todos os membros existentes. Para um buffer 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...
... o código gerado que é compilado, para que eu possa aprender com a saída IL o tipo do var nnn local, fica assim:
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 os membros da instância e do tipo estático estão disponíveis no código do esqueleto. Compila com sucesso. Nesse ponto, determinar o tipo de var local é simples por meio do Reflection.
O que torna isso possível é:
- a capacidade de executar o PowerShell no emacs
- o compilador C # é muito rápido. Na minha máquina, leva cerca de 0,5s para compilar um assembly na memória. Não é rápido o suficiente para a análise entre pressionamentos de tecla, mas rápido o suficiente para suportar a geração sob demanda de listas de conclusão.
Ainda não examinei o LINQ.
Isso será um problema muito maior porque o lexer / analisador semântico que o emacs tem para C #, não "faz" o LINQ.