Estratégias de Otimização para Análise Estática de Código em Projetos Android

No contexto das práticas de DevOps, a integração contínua (CI) tipicamente abrange etapas como submissão de código, detecção estática, testes unitários e empacotamento. A análise estática de código desempenha um papel crucial ao identificar proativamente problemas relacionados a padrões de codificação, defeitos e desempenho, contribuindo para a qualidade final do projeto.

Para projetos Android, ferramentas comuns de análise estática incluem CheckStyle, Lint e FindBugs. Com o objetivo de simplificar a integração, desenvolvemos um plugin interno que consolida essas ferramentas. Inicialmente, a incorporação deste plugin em nosso processo de PR (Pull Request) via Jenkins resultava em construções que duravam em média 1 a 2 minutos, com impacto mínimo na produtividade. Contudo, com o tempo, o volume de código cresceu exponencialmente e o uso de Flavors se tornou comum para atender a requisitos complexos. Isso elevou o tempo de construção de um único PR para 8 a 9 minutos, com a análise estática respondendo por cerca de 50% desse período. Essa degradação da eficiência na integração contínua representou um desafio significativo para a produtividade da nossa equipe de desenvolvimento.

Análise e Estratégias de Otimização

Diante do cenário apresentado, abordamos as seguintes questões:

Questão 1: Todas as Ferramentas de Análise Incluídas são Essenciais?

Para verificar a indispensabilidade de cada ferramenta, consideramos os seguintes aspectos:

  • Foco da Análise: Que tipos específicos de problemas cada ferramenta aborda?
  • Variedade de Regras: Qual a amplitude das capacidades de detecção de cada ferramenta?
  • Alvo da Análise: Quais tipos de arquivos (código-fonte, bytecode, recursos) cada ferramenta processa?
  • Princípio de Funcionamento: Uma breve descrição de como cada ferramenta opera.
  • Vantagens e Desvantagens: Comparação em termos de eficiência, extensibilidade e personalização.

É importante notar que FindBugs suporta apenas Java 1.0 a 1.8 e foi substituído por SpotBugs. No entanto, para compatibilidade com projetos legados que ainda não migraram para Java 8, mantivemos FindBugs.

Após a análise, concluímos que cada ferramenta possui um propósito específico e complementar: CheckStyle é rápido e eficaz para estilos de código e complexidade ciclomática; FindBugs (ou SpotBugs) identifica potenciais erros de codificação, questões de segurança e desempenho em código Java; e Lint, sendo a ferramenta oficial do Android, é robusta, altamente personalizável e abrangente. Portanto, a decisão foi manter a integração das três ferramentas para aproveitar suas características únicas.

Questão 2: É Possível Otimizar o Processo de Análise?

Dado que a integração das ferramentas é a melhor abordagem, o próximo desafio é a eficiência da análise. Analisamos onde o tempo é gasto no plugin atual.

Análise de Tempo de Análise Estática

A construção de projetos Android utiliza o Gradle, e cada construção envolve a execução de várias tarefas (Tasks) do Gradle. Devido às características do Gradle, cada módulo deve executar tarefas relacionadas a CheckStyle, FindBugs e Lint. Para Android, o número de tarefas é diretamente influenciado pelas variantes de construção (Variant), onde Variant = Flavor * BuildType. Assim, o número de tarefas por módulo pode ser descrito por: Flavor * BuildType * (Lint, CheckStyle, Findbugs), representando um produto cartesiano. O diagrama abaixo ilustra essa relação:

Uma construção completa executa um número de tarefas diretamente proporcional ao número de variantes. Observando o tempo de execução de tarefas em um projeto real, percebemos que o maior consumo de tempo está nas tarefas de FindBugs e Lint para cada módulo, enquanto CheckStyle tem um impacto menor. O gargalo principal, além do tempo intrínseco de cada ferramenta, é o grande número de tarefas gerado pela combinação de múltiplos módulos e variantes.

Análise de Ideias de Otimização

O tempo de análise de cada ferramenta é influenciado por seus algoritmos internos, regras de detecção e pelo volume de arquivos. Ferramentas como CheckStyle e Lint, que analisam código-fonte, dependem de análise léxica e sintática para construir uma Árvore de Sintaxe Abstrata (AST) e compará-la com as regras. FindBugs, por sua vez, opera em arquivos de bytecode (classes), necessitando que o código seja compilado antes da análise via BCEL. Otimizar os algoritmos internos das ferramentas seria complexo e com retorno incerto. Nosso foco, portanto, concentrou-se em reduzir o impacto do número de módulos e variantes.

A partir da análise de tempo, vimos que o número de módulos e variantes afeta diretamente a quantidade de tarefas. Em um cenário de PR, as modificações podem abranger múltiplos módulos e variantes. Para simplificar, consideramos primeiro o caso de um módulo com várias variantes. Dado que as fontes podem diferir entre variantes e FindBugs analisa classes compiladas, lidar com múltiplas variantes diretamente é complexo. Uma abordagem é usar diferentes Jobs Jenkins para cada variante, trocando espaço por tempo. Isso nos leva ao problema de otimizar o tempo gasto por tarefas de múltiplos módulos ao analisar uma única variante.

Para reduzir o número de módulos, poderíamos extrair componentes para repositórios independentes e executar suas análises separadamente, integrando-os como AARs. Mas para os módulos restantes, como otimizar? Todas as ferramentas processam arquivos de entrada. Se pudermos coletar todos os arquivos-fonte e de bytecode de todos os módulos em uma única tarefa, poderíamos consolidar as análises. Assim, para a análise completa, nosso objetivo principal é unificar a coleta de arquivos alvo de todos os módulos.

Questão 3: A Análise Incremental é Suportada?

As otimizações anteriores focavam na análise completa para reduzir o número de tarefas. Considerando que o tempo de análise também está ligado ao volume de arquivos, podemos focar na quantidade de arquivos. No desenvolvimento diário, um PR geralmente envolve a modificação de apenas alguns arquivos. Não faz sentido reanalisar todo o código existente. Uma análise incremental, focada apenas nos arquivos modificados, poderia reduzir significativamente o tempo. Com essa ideia, surgem as seguintes perguntas:

  • Como coletar arquivos incrementais (fontes e classes)?
  • Existem soluções de análise incremental no mercado? São viáveis para nosso contexto?
  • Como cada ferramenta de análise suporta a análise de arquivos incrementais?

A seguir, detalharemos as soluções para essas questões.

Exploração e Prática de Otimização

Otimização da Análise Completa

Coleta de Conjuntos de Arquivos Alvo de Todos os Módulos

Para obter todos os arquivos alvo, primeiro precisamos identificar quais módulos participam da análise. Um módulo no Gradle é um "Project". Portanto, basta identificar todos os "Projects" dos quais o projeto principal depende. Como a configuração de dependências pode variar entre variantes, para uma determinada variante, podemos usar a seguinte lógica para coletar os projetos relacionados:

static Set<Project> coletarProjetosDependentes(Project projetoAtual, BaseVariant varianteAlvo, Set<Project> resultado = null) {
  if (resultado == null) {
    resultado = new HashSet<>()
  }
  // Coletar tarefas de compilação da variante atual para identificar dependências
  Set<Task> tarefasDeCompilacao = varianteAlvo.javaCompiler.taskDependencies.getDependencies(varianteAlvo.javaCompiler)
  tarefasDeCompilacao.each { Task tarefa ->
    // Verificar se a tarefa pertence a outro projeto Android dependente
    if (tarefa.project != projetoAtual && GradleUtilidades.possuiPluginAndroid(tarefa.project)) {
      resultado.add(tarefa.project)
      BaseVariant varianteFilha = GradleUtilidades.obterVariante(tarefa.project)
      // Comparar variantes para garantir compatibilidade antes de recursão
      if (varianteFilha.name == varianteAlvo.name || "${varianteAlvo.flavorName}${varianteFilha.buildType.name}".toLowerCase() == varianteAlvo.name.toLowerCase()) {
        coletarProjetosDependentes(tarefa.project, varianteFilha, resultado)
      }
    }
  }
  return resultado
}

Os conjuntos de arquivos são categorizados em arquivos-fonte e arquivos de bytecode, que podem ser tratados da seguinte forma:

// Coleta de arquivos-fonte para CheckStyle
conjuntoDeProjetos.each { projetoAlvo ->
  if (projetoAlvo.plugins.hasPlugin(PluginDetectorDeCodigo) && GradleUtilidades.possuiPluginAndroid(projetoAlvo)) {
    GradleUtilidades.getExtensaoAndroid(projetoAlvo).sourceSets.all { AndroidSourceSet conjuntoFontes ->
      if (!conjuntoFontes.name.startsWith("test") && !conjuntoFontes.name.startsWith(SdkConstants.FD_TEST)) {
        source conjuntoFontes.java.srcDirs // 'source' é uma propriedade da tarefa CheckStyle
      }
    }
  }
}

// Coleta de arquivos de bytecode para FindBugs
static final Collection<String> padroesExcluidosPadrao = (exclusoesDataBindingAndroid + exclusoesAndroid + exclusoesButterKnife + exclusoesDagger2).asImmutable()

List<ConfigurableFileTree> todasAsClassesFileTree = new ArrayList<>()
ConfigurableFileTree classesDoProjetoAtual = projeto.fileTree(dir: variante.javaCompile.destinationDir, excludes: padroesExcluidosPadrao)
todasAsClassesFileTree.add(classesDoProjetoAtual)

GradleUtilidades.coletarProjetosDependentes(projeto, variante).each { projetoAlvo ->
  if (projetoAlvo.plugins.hasPlugin(PluginDetectorDeCodigo) && GradleUtilidades.possuiPluginAndroid(projetoAlvo)) {
    GradleUtilidades.getVariantesAndroid(projetoAlvo).each { BaseVariant varianteDoProjetoAlvo ->
      // Verificar se a variante alvo corresponde à variante atual ou ao buildType
      if (varianteDoProjetoAlvo.name == variante.name || "${varianteDoProjetoAlvo.name}".toLowerCase() == variante.buildType.name.toLowerCase()) {
        todasAsClassesFileTree.add(projetoAlvo.fileTree(dir: varianteDoProjetoAlvo.javaCompile.destinationDir, excludes: padroesExcluidosPadrao))
      }
    }
  }
}
// O conjunto 'todasAsClassesFileTree' pode ser usado como input para a propriedade 'classes' da tarefa FindBugs

Para a ferramenta Lint, a tarefa correspondente não oferece uma propriedade direta para especificar arquivos de varredura. Assim, não implementamos uma otimização específica para Lint em análises completas nesse contexto.

Dados de Otimização da Análise Completa

Através da otimização da análise completa para CheckStyle e FindBugs, reduzimos o tempo total de análise de cerca de 9 minutos para aproximadamente 5 minutos.

Otimização da Análise Incremental

Como vimos, nem todos os arquivos precisam ser analisados a cada vez. A análise incremental pode otimizar drasticamente a eficiência.

Pesquisa de Tecnologia para Análise Incremental

Antes de definir a solução, pesquisamos abordagens existentes:

  • Para Lint, poderíamos adaptar ideias existentes e aprofundar na sua arquitetura para encontrar uma solução incremental na versão 3.x.
  • Para CheckStyle e FindBugs, a solução envolve entender seus parâmetros de configuração para direcionar conjuntos específicos de arquivos alterados.

Observação: Ferramentas como diff_cover focam na cobertura de código de testes incrementais, o que não se alinha diretamente com nossa necessidade de análise estática de código. Embora diff_cover identifique linhas de código alteradas, para análise estática, isso não é suficiente para o contexto semântico completo, especialmente para ferramentas baseadas em bytecode como FindBugs, onde as linhas alteradas precisariam ser compiladas antes da análise, tornando a abordagem inviável.

Identificando Arquivos Modificados Incrementalmente

O primeiro passo é obter os arquivos alvo da análise. Podemos usar o comando git diff para identificar arquivos modificados. É crucial ignorar arquivos excluídos ou renomeados, focando apenas nos arquivos adicionados ou alterados, e obter apenas seus caminhos. Exemplo: git diff --name-only --diff-filter=dr commitHash1 commitHash2 compara dois commits, filtrando exclusões e renomeações, e retorna os caminhos. Isso funciona bem para arquivos modificados em um repositório local. Para cenários de Pull Request (PR), a situação é mais complexa, exigindo a comparação do código local com o branch de destino remoto. Usamos um plugin Jenkins que fornece os parâmetros ${targetBranch} (branch de destino) e ${sourceCommitHash} (hash do commit de origem). Com esses parâmetros, executamos os seguintes comandos:

git remote add origem_remota ${urlGitUpstream}
git fetch origem_remota ${branchAlvo}
git diff --name-only --diff-filter=dr $hashCommitOrigem origem_remota/$branchAlvo

  1. Configurar um apelido para o branch remoto (origem_remota), usando urlGitUpstream configurado no plugin.
  2. Obter atualizações do branch de destino remoto.
  3. Comparar as diferenças entre o commit de origem e o branch de destino, obtendo os caminhos dos arquivos.

Dessa forma, obtemos o conjunto de arquivos modificados incrementalmente.

Análise do Princípio de Análise do Lint

Antes de detalhar a análise incremental do Lint, vejamos seu fluxo de trabalho:

Arquivos Fonte da Aplicação: Inclui Java, XML, arquivos de recursos, ProGuard, etc.

lint.xml: Usado para configurar verificações a serem excluídas e níveis de severidade personalizados. Projetos geralmente definem seus próprios lint.xml.

Ferramenta Lint: Um conjunto completo de ferramentas para analisar a estrutura de código Android, executável via linha de comando, IDE ou Gradle.

Saída do Lint: Os resultados da análise do Lint.

A ferramenta Lint processa o código-fonte (entrada) e, usando vários detectores, gera resultados (saída). O Android oferece três modos de execução: linha de comando, IDEA e Gradle. Todos convergem para a execução via LintDriver. Para examinar o código-fonte do Lint, adicionamos as seguintes dependências em build.gradle:

implementation 'com.android.tools.build:gradle:3.1.1'
implementation 'com.android.tools.lint:lint-gradle:26.1.1'

Isso revela as seguintes dependências:

  • lint-api-26.1.1: Uma abstração da ferramenta Lint, fornecendo APIs para iniciá-la.
  • lint-checks-26.1.1: Contém os detectores embutidos para analisar Issues predefinidas.
  • lint-26.1.1: Atua como um scaffolding, encapsulando os dois JARs anteriores para uso em linha de comando. Tasks do Gradle herdam classes relacionadas deste JAR.
  • lint-gradle-26.1.1: Encapsula classes de lint-26.1.1 para uso com tasks do Gradle.
  • lint-gradle-api-26.1.1: O ponto de entrada real para a execução de tasks Lint no Gradle.

Compreendendo a relação desses JARs, o núcleo do Lint está nos três primeiros. Os dois últimos são adaptações para Gradle. A lógica principal reside no método analyze de LintDriver:

fun analisar() {
    // ... código omitido ...
    for (projeto in projetos) {
        dispararEvento(TipoEvento.PROJETO_REGISTRADO, projeto = projeto)
    }
    registrarDetectoresCustomizados(projetos)
    // ... código omitido ...
    try {
        for (projeto in projetos) {
            fase = 1
            val principal = requisicao.obterProjetoPrincipal(projeto)
            // O conjunto de detectores disponíveis varia entre projetos
            calcularDetectores(projeto)
            if (detectoresAplicaveis.isEmpty()) {
                // Nenhum detector habilitado neste projeto: pular
                continue
            }
            verificarProjeto(projeto, principal)
            if (cancelado) {
                break
            }
            executarFasesExtras(projeto, principal)
        }
    } catch (excecao: Throwable) {
        // Processo cancelado etc.
        if (!lidarComErroDetector(null, this, excecao)) {
            cancelar()
        }
    }
    // ... código omitido ...
}

Os três passos importantes são:

  • registrarDetectoresCustomizados(projetos): Registra detectores embutidos e personalizados, combinando-os em um CompositeIssueRegistry.
  • calcularDetectores(projeto): Coleta todos os detectores disponíveis para o projeto atual.
  • verificarProjeto(projeto, principal): Este é o passo crucial. Ele chama runFileDetectors para escanear arquivos. O escopo de varredura do Lint é definido por Scope.
fun inferir(projetos: Collection<Projeto>?): EnumSet<Escopo> {
    if (projetos == null || projetos.isEmpty()) {
        return Escopo.TODOS
    }
    var escopo = EnumSet.noneOf(Escopo::class.java)
    for (projeto in projetos) {
        val subconjunto = projeto.subconjunto
        if (subconjunto != null) {
            for (arquivo in subconjunto) {
                val nome = arquivo.name
                if (nome == MANIFESTO_ANDROID_XML) {
                    escopo.add(MANIFESTO)
                } else if (nome.endsWith(PONTO_XML)) {
                    escopo.add(ARQUIVO_RECURSO)
                } else if (nome.endsWith(PONTO_JAVA) || nome.endsWith(PONTO_KT)) {
                    escopo.add(ARQUIVO_JAVA)
                } else if (nome.endsWith(PONTO_CLASS)) {
                    escopo.add(ARQUIVO_CLASSE)
                }
                // ... outras condições de tipo de arquivo ...
            }
        } else {
            // Projeto completo especificado: usar escopo completo
            escopo = Escopo.TODOS
            break
        }
    }
    return escopo
}

Se Project.subconjunto for nulo, Scope será Scope.TODOS, indicando uma análise completa. Se não for nulo, o Lint itera sobre o subconjunto para determinar o escopo. Isso nos revela que subconjunto é o ponto chave para a análise incremental. O método runFileDetectors confirma isso:

if(escopo.contains(Escopo.ARQUIVO_JAVA)||escopo.contains(Escopo.TODOS_ARQUIVOS_JAVA)){
  val verificacoes = uniao(detectoresEscopo[Escopo.ARQUIVO_JAVA],detectoresEscopo[Escopo.TODOS_ARQUIVOS_JAVA])
  if (verificacoes != null && !verificacoes.isEmpty()) {
    val arquivos = projeto.subconjunto
    if (arquivos != null) {
      verificarArquivosJavaIndividuais(projeto, principal, verificacoes, arquivos)
    } else {
      val pastasFonte = projeto.pastasFonteJava
      val pastasTeste = if (escopo.contains(Escopo.FONTES_TESTE))
      projeto.pastasFonteTeste
      else
      emptyList<File> ()
      val pastasGeradas = if (verificarFontesGeradas)
      projeto.pastasFonteGeradas
      else
      emptyList<File> ()
      verificarJava(projeto, principal, pastasFonte, pastasTeste, pastasGeradas, verificacoes)
    }
  }
}

Se project.subconjunto não for nulo, arquivos Java individuais são escaneados. Caso contrário, pastas de código-fonte, teste e geradas são analisadas. A ordem de varredura segue o Scope (MANIFEST, RESOURCES, JAVA, CLASS, etc.). A anotação em Project.addFile e getSubset deixa claro que se files (o subconjunto) não for nulo, apenas os arquivos especificados serão escaneados.

Implementação da Tarefa Gradle de Análise Incremental do Lint

No Gradle, as tarefas Lint (lintDebug, lintRelease, lint) são geradas pelo plugin Android. Todas elas estendem LintBaseTask, e a lógica de varredura final é executada em LintGradleExecution.runLint (no JAR lint-gradle-26.1.1). Este método prepara a análise configurando IssueRegistry, LintCliFlags, LintGradleClient e LintOptions, e então chama client.run(registry). Para análise incremental, precisamos construir um LintGradleClient personalizado que injete o conjunto de arquivos modificados no Project.subconjunto. Embora a reflexão seja uma opção, a abordagem preferencial é estender LintBaseTask para criar uma tarefa incremental que, ao invés de acionar o fluxo completo, configure o LintGradleClient com o subconjunto de arquivos incrementais.

Introdução à Análise do FindBugs

FindBugs é uma ferramenta de análise estática que examina arquivos de classe ou JARs. Ela utiliza a biblioteca BCEL da Apache para analisar bytecode, comparando-o com um conjunto de padrões de defeitos. A versão 3.0.1 do FindBugs inclui mais de 300 tipos de defeitos. É altamente flexível para integração em diversas ferramentas de compilação. No contexto do Gradle, veremos suas propriedades de tarefa.

Análise das Propriedades da Tarefa FindBugs no Gradle

A tarefa FindBugs embutida no Gradle possui as seguintes propriedades importantes:

  • Classes: O conjunto de arquivos .class a serem analisados (geralmente o diretório de classes compiladas).
  • Classpath: Todos os caminhos de classes relacionados necessários para analisar os arquivos de Classes. Elas não são analisadas, apenas usadas como referência.
  • Effort: Nível de rigor da análise (MIN, Default, MAX). Quanto maior, mais rigorosa e demorada.
  • findBugsClasspath: Caminho para as bibliotecas do FindBugs (o motor de análise).
  • reportLevel: Nível de prioridade dos bugs a serem relatados (Low, Medium, High).
  • Reports: Caminho para armazenar os resultados da análise.

Para a análise incremental do FindBugs, basta especificar o conjunto de arquivos para a propriedade Classes.

Análise da Tarefa FindBugs Incremental

Observamos como o plugin FindBugs do IDEA lida com a varredura de arquivos únicos. Ao analisar um arquivo individual, ele escaneia apenas aquele arquivo e suas dependências diretas. A saída da varredura revela informações cruciais:

  • Lista de diretórios de código-fonte (Java, res, classes geradas).
  • O conjunto de classes alvo a serem analisadas (o arquivo .class correspondente ao arquivo Java atual).
  • Aux Classpath Entries: Caminhos de classes auxiliares necessárias para a análise do arquivo alvo.

Portanto, para a análise incremental, precisamos resolver a obtenção dessas propriedades: os arquivos de classe incrementais (para Classes) e seus caminhos de dependência (para Classpath ou AuxClasspath).

Configuração do AuxClasspath

A propriedade Classpath (AuxClasspath no FindBugs nativo) é vital, pois contém as classes dependentes necessárias para a análise, mas que não são alvos de análise. A ausência dessas dependências pode causar erros durante a varredura. Precisamos coletar todas as bibliotecas e classes compiladas de todos os módulos que influenciam o módulo sendo analisado.

FileCollection classesCompiladas = projeto.fileTree(dir: "${projeto.buildDir}/intermediates/classes/${variante.flavorName}/${variante.buildType.name}", includes: inclusosDasClasses)

FileCollection classpathDeDependencias = projeto.files()
GradleUtilidades.coletarProjetosDependentes(projeto, variante).each { projetoDependente ->
    GradleUtilidades.getVariantesAndroid(projetoDependente).each { varianteDependente ->
        if (varianteDependente.name.capitalize().equalsIgnoreCase(variante.name.capitalize())) {
            classpathDeDependencias += varianteDependente.javaCompile.classpath
        }
    }
}

classpath = variante.javaCompile.classpath + classpathDeDependencias + classesCompiladas

Otimização contra Falsos Positivos na Análise Incremental do FindBugs

Ao escanear arquivos incrementais, um número limitado de arquivos pode levar a falsos positivos em certas regras, algo que não ocorreria em uma varredura completa. Por exemplo, uma variável estática como public static String buildTime = ""; pode ser sinalizada como "deveria ser final", mesmo que seja intencionalmente modificada por outras classes. Se apenas a classe 'A' for escaneada, um defeito BUG_TYPE_MS_SHOULD_BE_FINAL será reportado. Para resolver isso, precisamos identificar quais classes dependem da classe 'A' e quais classes 'A' depende, e incluí-las na varrredura. Usamos ASM para essa análise de depandências:

void encontrarTodasAsClassesParaAnalise(ConfigurableFileTree todasAsClasses) {
    arquivosAScanear = [] as HashSet
    String diretorioClassesCompiladas = "${projeto.buildDir}/$DIRETORIO_ANALISE_FINDBUGS/$DIRETORIO_ORIGEM_ANALISE_FINDBUGS"

    Set<File> arquivosDeClasseDoModulo = todasAsClasses.files
    for (File arquivo : arquivosDeClasseDoModulo) {
        String[] partesCaminho = arquivo.absolutePath.split("$DIRETORIO_ANALISE_FINDBUGS/$DIRETORIO_ORIGEM_ANALISE_FINDBUGS/")
        if (partesCaminho.length > 1) {
            String nomeDaClasse = obterNomeDoArquivoSemExtensao(partesCaminho[1],'.')
            String prefixoClasseInterna = ""
            if (nomeDaClasse.contains('$')) {
                prefixoClasseInterna = nomeDaClasse.split('\\$')[0]
            }
            if (caminhosNomeClasseDiferente.contains(nomeDaClasse) || caminhosNomeClasseDiferente.contains(prefixoClasseInterna)) {
                arquivosAScanear.add(arquivo)
            } else {
                Iterable<String> classesParaResolver = new ArrayList<String>()
                classesParaResolver.add(arquivo.absolutePath)
                Set<File> classesDependentes = Dependencias.encontrarDependenciasDeClasse(projeto, new AceitadorDeClasses(), diretorioClassesCompiladas, classesParaResolver)
                for (File classeDependente : classesDependentes) {
                    if (caminhosNomeClasseDiferente.contains(obterNomeCaminhoPacote(classeDependente))) {
                        arquivosAScanear.add(arquivo)
                        break
                    }
                }
            }
        }
    }
}

Esta abordagem ajuda a mitigar falsos positivos na análise incremental do FindBugs, superando a precisão de ferramentas como o plugin FindBugs-IDEA para varreduras parciais.

Análise Incremental do CheckStyle

A análise incremental do CheckStyle é mais direta. Por ser uma ferramenta de análise de código-fonte, a propriedade source da tarefa CheckStyle permite especificar diretamente o conjunto de arquivos alvo. Assim, podemos alimentá-la com os arquivos modificados:

void configurarAnaliseIncrementalDeFonte() {
    boolean verificarPR = false
    BuscadorDeArquivosDiferentes buscadorDeDiferencas

    if (projeto.hasProperty(ExtensaoDetectorDeCodigo.VERIFICAR_PR)) {
        verificarPR = projeto.getProperties().get(ExtensaoDetectorDeCodigo.VERIFICAR_PR)
    }

    if (verificarPR) {
        buscadorDeDiferencas = new AuxiliarBuscadorDeArquivosDiferentes.BuscadorDeArquivosDiferentesPR()
    } else {
        buscadorDeDiferencas = new AuxiliarBuscadorDeArquivosDiferentes.BuscadorDeArquivosDiferentesLocal()
    }

    source buscadorDeDiferencas.encontrarArquivosDiferentes(projeto)

    if (getSource().isEmpty()) {
        println 'Nenhum arquivo Java diferente encontrado, ignorando verificação CheckStyle.'
    }
}

Resultados da Otimização

Com as otimizações de análise completa e incremental, a eficiência geral da varredura aumentou em mais de 50%. Os dados de otimização são os seguintes:

(Considerando que a imagem com os dados de otimização não foi fornecida, esta seção apenas reforça a melhoria.)

Implementação e Padronização

Generalidade da Ferramenta de Análise

Após otimizar a eficiência, nosso objetivo foi tornar o plugin acessível a mais projetos com baixo custo de integração. Para projetos existentes, queremos que o código novo seja verificado incrementalmente. Para o código legado, devido ao seu grande volume, a análise completa e gradual é preferível. Oferecemos configurações flexíveis para que os usuários possam escolher quais ferramentas integrar e se desejam análise incremental ou completa, garantindo assim a universalidade do plugin em projetos novos e antigos.

Garentia de Integridade da Análise

Mencionamos que a análise incremental do FindBugs pode gerar falsos positivos devido a um conjunto incompleto de arquivos. Para equilibrar eficiência e precisão, adotamos uma estratégia: análise incremental para PRs (eficiência) e análise completa em Daily Builds de CI (integridade e cobertura total). Em nossos projetos, a configuração é a seguinte:

apply plugin: 'detector-de-codigo' // Nome do plugin alterado

detectorDeCodigo { // Bloco de configuração alterado
    // Configura o caminho relativo para os relatórios de detecção estática
    caminhoRelativoRelatorios = rootProject.file('relatorios')

    /**
     * URL do repositório remoto, usado para detecção incremental em PRs.
     */
    urlGitUpstream = "ssh://git@xxxxxxxx.git"

    configCheckStyle { // Bloco de configuração alterado
        /**
         * Habilita ou desabilita a detecção do CheckStyle
         * Habilitado: true
         * Desabilitado: false
         */
        habilitado = true
        /**
         * Se deve abortar a verificação em caso de erro
         * Abortar: false
         * Não abortar: true. Se for true, a tarefa CheckStyle não falhará
         * e não copiará relatórios de erro.
         */
        ignorarFalhas = false
        /**
         * Se deve exibir informações de violação no log
         * Exibir: true
         * Não exibir: false
         */
        exibirViolacoes = true
        /**
         * Configura URIs para checkstyle.xml e checkstyle.xsl personalizados
         * Caminhos esperados:
         *      "${uriCheckStyle}/checkstyle.xml"
         *      "${uriCheckStyle}/checkstyle.xsl"
         *
         * Padrão é null, usando as configurações padrão do DetectorDeCodigo
         */
        uriCheckStyle = rootProject.file('qualidadeDeCodigo/checkstyle')
    }

    configFindBugs { // Bloco de configuração alterado
        /**
         * Habilita ou desabilita a detecção do FindBugs
         * Habilitado: true
         * Desabilitado: false
         */
        habilitado = true
        /**
         * Opcional, define o nível de esforço de análise, padrão é 'max'
         * min, default, ou max. 'max' é mais rigoroso, reporta mais bugs. 'min' um pouco menos.
         */
        esforco = "max"
        /**
         * Opcional, padrão é 'high'
         * low, medium, high. Se 'low', todos os bugs são reportados.
         */
        nivelRelatorio = "high"
        /**
         * Configura URIs para findbugs_include.xml e findbugs_exclude.xml personalizados
         * Caminhos esperados:
         *      "${uriFindBugs}/findbugs_include.xml"
         *      "${uriFindBugs}/findbugs_exclude.xml"
         * Padrão é null, usando as configurações padrão do DetectorDeCodigo
         */
        uriFindBugs = rootProject.file('qualidadeDeCodigo/findbugs')
    }

    configLint { // Bloco de configuração alterado
        /**
         * Habilita ou desabilita a detecção do Lint
         * Habilitado: true
         * Desabilitado: false
         */
        habilitado = true

        /**
         * Configura URIs para lint.xml e retrolambda_lint.xml personalizados
         * Caminhos esperados:
         *      "${uriConfigLint}/lint.xml"
         *      "${uriConfigLint}/retrolambda_lint.xml"
         * Padrão é null, usando as configurações padrão do DetectorDeCodigo
         */
        uriConfigLint = rootProject.file('qualidadeDeCodigo/lint')
    }
}

O plugin permite flexibilidade na escolha entre varredura incremental ou completa para diferentes cenários, como integração de projetos existentes, novos projetos ou verificações de empacotamento.

Exemplo de execução via script:

./gradlew ":${nomeModuloApp}:montar${nomeVarianteFinal}" -PdetectorHabilitado=true -PcheckStyleIncremental=true -PlintIncremental=true -PfindBugsIncremental=true -PverificarPR=${verificarPR} -PhashCommitOrigem=${hashCommitOrigem} -PbranchAlvo=${branchAlvo} --stacktrace

Desejamos que uma única tarefa exponha todos os problemas detectados pelas ferramentas sem interromper o processo, e, se executada localmente, que abra automaticamente o navegador para exibir os relatórios.

def tarefasFinalizadas = [tarefaLint, tarefaCheckStyle, tarefaFindBugs]
tarefaVerificarCodigo.finalizedBy tarefasFinalizadas

"open ${caminhoRelatorio}".execute()

Para garantir que os PRs não causem problemas de empacotamento, a tarefa acionada no PR é, na verdade, uma tarefa de empacotamento, à qual as tarefas de análise estática são anexadas. Em projetos com múltiplas Flavors, o CI dispara múltiplos Jobs que executam a análise incremental e o empacotamento para a Flavor correspondente. Além disso, um Job de empacotamento final executa uma análise completa para garantir a integridade total do código.

Tags: android Gradle Lint CheckStyle FindBugs

Publicado em 6-28 23:15