From 7daade1c4d3756e67fe14c87d5212f0c9f77205c Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Sat, 5 Nov 2022 11:14:20 +0100 Subject: Add support for kotlin by adding flycheck-android-kotlin checker --- bin/flycheck-android-java.py | 2 +- bin/flycheck-android-kotlin.py | 296 +++++++++++++++++++++ build.gradle | 7 +- flycheck-android-experimental.el | 50 +++- .../FlycheckAndroidExperimentalInitPlugin.groovy | 3 + .../FlycheckAndroidExperimentalPlugin.groovy | 2 + src/main/groovy/FlycheckAndroidKotlinTask.groovy | 65 +++++ 7 files changed, 417 insertions(+), 8 deletions(-) create mode 100644 bin/flycheck-android-kotlin.py create mode 100644 src/main/groovy/FlycheckAndroidKotlinTask.groovy diff --git a/bin/flycheck-android-java.py b/bin/flycheck-android-java.py index 4194049..37c1b68 100644 --- a/bin/flycheck-android-java.py +++ b/bin/flycheck-android-java.py @@ -135,7 +135,7 @@ def figure_out_java_compilation(sessiondir, sourcefile, tempfile, checkstyle, if exc.errno != errno.EEXIST or not os.path.isdir(outdir): outdir = None pass - cachefile = os.path.join(sessiondir, 'gradle_output') + cachefile = os.path.join(sessiondir, 'gradle_java_output') try: if not force and os.path.getmtime(cachefile) >= mtime: with open(cachefile, 'r') as f: diff --git a/bin/flycheck-android-kotlin.py b/bin/flycheck-android-kotlin.py new file mode 100644 index 0000000..ad1d88f --- /dev/null +++ b/bin/flycheck-android-kotlin.py @@ -0,0 +1,296 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import argparse +import errno +import os +import subprocess +import sys +import tempfile + +def get_gradle_command_and_project_mtime(dir): + """Find top-project gradlew or fallback to using gradle on sub-project dir. +While iterating the paths, also check mtime on build.gradle files""" + topdir = dir + build_gradle = None + gradlew = None + mtime = -1 + while True: + test = os.path.join(topdir, 'build.gradle') + if os.path.exists(test): + if not build_gradle: + build_gradle = test + + try: + m = os.path.getmtime(test) + if m > mtime: + mtime = m + except OSError: + pass + + test = os.path.join(topdir, 'gradlew') + if os.path.exists(test): + gradlew = test + break + + test = os.path.dirname(topdir) + if test == topdir: + break + topdir = test + + if gradlew: + cmd = [gradlew, '-p', topdir] + elif build_gradle: + topdir = os.path.dirname(build_gradle) + cmd = ['gradle', '-p', topdir] + else: + topdir = '' + cmd = ['gradle'] + return (cmd, mtime, topdir) + +def filter_source_files(files): + """Remove any item not looking like a kotlin or java source file.""" + return [f for f in files if f.endswith('.kt') or f.endswith('.java')] + +def filter_java_source_files(files): + """Remove any item not looking like a java source file.""" + return [f for f in files if f.endswith('.java')] + +def cleanup_output(tmp, real): + """Remove any file in tmp that is older than the one in real""" + offset = len(tmp) + if tmp[-1] != '/': + offset = offset + 1 + for (dirpath, _, files) in os.walk(tmp): + realpath = os.path.join(real, dirpath[offset:]) + for filename in files: + try: + mtime = os.path.getmtime(os.path.join(realpath, filename)) + tmp = os.path.join(dirpath, filename) + if os.path.getmtime(tmp) <= mtime: + os.remove(tmp) + except OSError: + pass + +def run_kotlinc(cp, files, output, args, sourcefile, outdir): + """Execute kotlinc with the given options.""" + command = ['kotlinc'] + if outdir: + command.extend(['-d', outdir]) + tmp = [outdir] + if output: + tmp.append(output) + if cp: + tmp.extend(cp) + if tmp: + command.extend(['-cp', ':'.join(tmp)]) + + if os.fork() == 0: + cleanup_output(outdir, output) + sys.exit(0) + else: + if cp: + command.extend(['-cp', ':'.join(cp)]) + if output: + command.extend(['-d', output]) + command.extend(args) + command.append(sourcefile) + print(sourcefile) + if outdir: + with tempfile.NamedTemporaryFile(mode='w') as f: + command.append('@' + f.name) + for source in filter_java_source_files(files): + f.write(source) + f.write('\n') + f.flush() + return subprocess.call(command) + else: + with tempfile.NamedTemporaryFile(mode='w') as f: + command.append('@' + f.name) + for source in filter_source_files(files): + f.write(source) + f.write('\n') + f.flush() + return subprocess.call(command) + +def file_in_list(needle, files): + """Find needle in files and if so, return the list without needle. +Otherwise return None.""" + if os.path.exists(needle): + for i in range(0, len(files) - 1): + if os.path.exists(files[i]) and os.path.samefile(needle, files[i]): + return files[0:i] + files[i + 1:] + return None + +def figure_out_kotlin_compilation(sessiondir, sourcefile, tempfile, detekt, + variant, run_gen, force=False): + """Get options for Kotlin compilation from gradle project and run kotlinc.""" + (cmd, mtime, projectdir) = get_gradle_command_and_project_mtime( + os.path.dirname(sourcefile)) + output = None + cached = False + if sessiondir != None: + outdir = os.path.join(sessiondir, 'kotlin_output') + try: + os.mkdir(outdir) + except OSError as exc: + if exc.errno != errno.EEXIST or not os.path.isdir(outdir): + outdir = None + pass + cachefile = os.path.join(sessiondir, 'gradle_kotlin_output') + try: + if not force and os.path.getmtime(cachefile) >= mtime: + with open(cachefile, 'r') as f: + output = f.read() + cached = True + except (OSError, IOError): + pass + else: + outdir = None + + if not output: + flycheck_cmd = cmd + ['-q', 'flycheckAndroidKotlin'] + output = subprocess.check_output(flycheck_cmd, universal_newlines=True) + if sessiondir != None: + try: + with open(cachefile, 'w') as f: + f.write(output) + except IOError: + pass + + output = output.split('\n') + + data = None + data_type = None + variants = [] + gen = [] + ret = 0 + compiled = False + for line in output: + if line == '***' or line == '!!!': + if data_type == '*': + variants.append({'data': data, 'gen': len(gen)}) + elif data_type == '!' and variants: + gen.append(data) + data_type = line[0] + if data_type == '*': + data = {} + else: + data = [] + elif data_type == '*': + if line.startswith('variant='): + data['variant'] = line[8:] + if line.startswith('args='): + data['args'] = line[6:-1].split(', ') + elif line.startswith('cp='): + data['cp'] = line[3:].split(':') + elif line.startswith('files='): + data['files'] = line[6:].split(':') + elif line.startswith('output='): + data['output'] = line[7:] + elif data_type == '!': + data.append(line) + + if data_type == '*': + variants.append({'data': data, 'gen': len(gen)}) + elif data_type == '!' and variants: + gen.append(data) + + fallback = None + + if variant: + for v in variants: + data = v['data'] + if 'variant' in data and data['variant'] == variant: + if not fallback: + fallback = data + files = file_in_list(sourcefile, data['files']) + if files: + if run_gen and v['gen'] < len(gen): + subprocess.call(cmd + ['-q'] + gen[v['gen']]) + + ret = run_kotlinc(data['cp'], files, data['output'], + data['args'], tempfile, outdir) + compiled = True + break + + if not compiled and variants: + data = variants[0]['data'] + first = data['variant'] if 'variant' in data else None + for v in variants: + data = v['data'] + if first and 'variant' in data and data['variant'] == first: + if not fallback: + fallback = data + files = file_in_list(sourcefile, data['files']) + if files: + if run_gen and v['gen'] < len(gen): + subprocess.call(cmd + ['-q'] + gen[v['gen']]) + + ret = run_kotlinc(data['cp'], files, data['output'], + data['args'], tempfile, outdir) + compiled = True + break + + if not compiled: + # Probably need to rerun gradle to find group for sourcefile + if cached and os.path.exists(sourcefile): + return figure_out_kotlin_compilation(sessiondir, sourcefile, + tempfile, detekt, variant, + run_gen, force=True) + # OK, perhaps file doesn't exist yet or not yet added to gradle, + # whatever, assume the first group is good enough + if fallback: + ret = run_kotlin(fallback['cp'], fallback['files'], + fallback['output'], fallback['args'], + tempfile, outdir) + compiled = True + + if not ret and detekt: + cmd = [detekt['cli']] + + if 'config' in detekt: + if detekt['config']: + cmd.extend(['-c', detekt['config']]) + else: + cmd.append('--build-upon-default-config') + + cmd.append('-i') + cmd.append(tempfile) + if len(projectdir): + cwd = projectdir + else: + cwd = None + ret = subprocess.call(cmd, cwd=cwd) + + if not ret and not compiled: + print("Source file not in project and project seems empty!") + ret = -1 + + return ret + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument('--detekt-cli') + parser.add_argument('--detekt-config') + parser.add_argument('--variant') + parser.add_argument('--skip-gen', dest='gen', action='store_const', + const=False, default=True) + parser.add_argument('sessiondir', nargs='?') + parser.add_argument('sourcefile') + parser.add_argument('tempfile', nargs='?') + args = parser.parse_args(argv[1:]) + + sourcefile = args.sourcefile + tempfile = args.tempfile or sourcefile + + detekt = None + if args.detekt_cli: + detekt = {'cli': args.detekt_cli, + 'config': args.detekt_config} + + sourcefile = os.path.abspath(sourcefile) + return figure_out_kotlin_compilation(args.sessiondir, sourcefile, tempfile, + detekt, args.variant, args.gen) + +sys.exit(main(sys.argv)) diff --git a/build.gradle b/build.gradle index 3d698e7..c6c995d 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,11 @@ class PackageTask extends DefaultTask { return project.file('bin/flycheck-android-java.py') } + @InputFile + File getKotlinPythonScript() { + return project.file('bin/flycheck-android-kotlin.py') + } + @OutputFile File getOutput() { return new File(project.buildDir, "${packageName}.tar") @@ -60,7 +65,7 @@ class PackageTask extends DefaultTask { into target } project.copy { - from(javaPythonScript) + from(javaPythonScript, kotlinPythonScript) into bin } project.exec { diff --git a/flycheck-android-experimental.el b/flycheck-android-experimental.el index 0449394..4b781eb 100644 --- a/flycheck-android-experimental.el +++ b/flycheck-android-experimental.el @@ -34,15 +34,25 @@ :safe #'stringp) (make-variable-buffer-local 'flycheck-android-java-checkstyle-properties) -(flycheck-def-option-var flycheck-android-java-variant nil android-java +(flycheck-def-option-var flycheck-android-variant nil android-java android-kotlin "Variant to select instead of first." :safe #'stringp) -(make-variable-buffer-local 'flycheck-android-java-variant) +(make-variable-buffer-local 'flycheck-android-variant) -(flycheck-def-option-var flycheck-android-java-skip-gen nil android-java +(flycheck-def-option-var flycheck-android-skip-gen nil android-java android-kotlin "Set to non-nil if generate*Sources task should not be run before checking." :safe #'stringp) -(make-variable-buffer-local 'flycheck-android-java-skip-gen) +(make-variable-buffer-local 'flycheck-android-skip-gen) + +(flycheck-def-option-var flycheck-android-kotlin-detekt-cli nil android-kotlin + "Path to detekt-cli." + :safe #'stringp) +(make-variable-buffer-local 'flycheck-android-kotlin-detekt-cli) + +(flycheck-def-option-var flycheck-android-kotlin-detekt-config nil android-kotlin + "Path to detekt.yml." + :safe #'stringp) +(make-variable-buffer-local 'flycheck-android-kotlin-detekt-config) (flycheck-define-checker android-java "Java syntax checker using javac." @@ -57,9 +67,9 @@ (option "--checkstyle-properties=" flycheck-android-java-checkstyle-properties concat) (option "--variant=" - flycheck-android-java-variant concat) + flycheck-android-variant concat) (option-flag "--skip-gen" - flycheck-android-java-skip-gen) + flycheck-android-skip-gen) (eval (flycheck-android-get-sessiondir)) (eval buffer-file-name) source) @@ -83,6 +93,34 @@ (error line-start (file-name) ":" line ": " (message) line-end)) :modes java-mode) +(flycheck-define-checker android-kotlin + "Kotlin syntax checker using kotlin." + :command ("python" + (eval (flycheck-android-find-tool "kotlin")) + (option "--detekt-cli=" + flycheck-android-kotlin-detekt-cli concat) + (option "--detekt-config=" + flycheck-android-kotlin-detekt-config concat) + (option "--variant=" + flycheck-android-variant concat) + (option-flag "--skip-gen" + flycheck-android-skip-gen) + (eval (flycheck-android-get-sessiondir)) + (eval buffer-file-name) + source) + :error-patterns + ((warning line-start (file-name) ":" line ":" column ": warning:" + (message (one-or-more (not (any "^"))) + (any "^")) + line-end) + (error line-start (file-name) ":" line ":" column ": error:" + (message (one-or-more (not (any "^"))) + (any "^")) + line-end) + ;; detekt + (warning line-start (file-name) ":" line ":" column ": " (message) line-end)) + :modes kotlin-mode) + (defun flycheck-android-find-tool (tool) "Find flycheck-android-TOOL.py. TOOL=name of tool" diff --git a/src/main/groovy/FlycheckAndroidExperimentalInitPlugin.groovy b/src/main/groovy/FlycheckAndroidExperimentalInitPlugin.groovy index 7fd37cc..67340e9 100644 --- a/src/main/groovy/FlycheckAndroidExperimentalInitPlugin.groovy +++ b/src/main/groovy/FlycheckAndroidExperimentalInitPlugin.groovy @@ -12,6 +12,9 @@ class FlycheckAndroidExperimentalInitPlugin implements Plugin { || plugin.class.name == 'com.android.build.gradle.LibraryPlugin') { project.task('flycheckAndroidJava', type: FlycheckAndroidJavaTask) + } else if (plugin.class.name == 'org.jetbrains.kotlin.gradle.plugin.KotlinAndroidPluginWrapper') { + project.task('flycheckAndroidKotlin', + type: FlycheckAndroidKotlinTask) } } } diff --git a/src/main/groovy/FlycheckAndroidExperimentalPlugin.groovy b/src/main/groovy/FlycheckAndroidExperimentalPlugin.groovy index 6493812..0252e37 100644 --- a/src/main/groovy/FlycheckAndroidExperimentalPlugin.groovy +++ b/src/main/groovy/FlycheckAndroidExperimentalPlugin.groovy @@ -7,5 +7,7 @@ class FlycheckAndroidExperimentalPlugin implements Plugin { void apply(Project project) { project.task('flycheckAndroidJava', type: FlycheckAndroidJavaTask) + project.task('flycheckAndroidKotlin', + type: FlycheckAndroidKotlinTask) } } diff --git a/src/main/groovy/FlycheckAndroidKotlinTask.groovy b/src/main/groovy/FlycheckAndroidKotlinTask.groovy new file mode 100644 index 0000000..df5856d --- /dev/null +++ b/src/main/groovy/FlycheckAndroidKotlinTask.groovy @@ -0,0 +1,65 @@ +package org.thejk + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.compile.JavaCompile + +class FlycheckAndroidKotlinTask extends DefaultTask { + FlycheckAndroidKotlinTask() { + } + + @TaskAction + def action() { + def variants = [] + def android = project.extensions.findByName('android') + if (android && (android.class.simpleName == 'AppExtension_Decorated' + || android.class.simpleName == 'BaseAppModuleExtension_Decorated')) { + variants = android.applicationVariants.collect { it.name } + } else if (android && android.class.simpleName == 'LibraryExtension_Decorated') { + variants = android.libraryVariants.collect { it.name } + } else { + def buildTypes = project.extensions.buildTypes.names + if (buildTypes.isEmpty()) buildTypes = ['debug', 'release'] + def flavors = project.extensions.flavors.names + if (flavors.isEmpty()) { + variants = buildTypes + } else { + flavors.each { flavor -> + buildTypes.each { buildType -> + variants.add(flavor + buildType.capitalize()) + } + } + } + } + variants = variants.sort() + def configurations = ['', 'UnitTest'] + variants.each { variant -> + configurations.each { configuration -> + def name = 'compile' + name += variant.capitalize() + name += configuration.capitalize() + 'Kotlin' + def compile = project.tasks.findByName(name) + if (compile) { + try { + def cp = compile.classpath.asPath + println '***' + println 'variant=' + variant + println 'args=' + compile.defaultSerializedCompilerArguments + println 'cp=' + cp + println 'files=' + compile.inputs.files.asPath + println 'output=' + compile.destinationDirectory.get() + } catch (Throwable t) { + } + } + } + + def name = 'generate' + name += variant.capitalize() + 'Sources' + def generate = project.tasks.findByName(name) + if (generate != null) { + println '!!!' + println generate.path + } + } + } +} -- cgit v1.2.3-70-g09d2