1 /*
<lambda>null2 * Copyright (C) 2021 Square, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.squareup.moshi.kotlin.codegen.ksp
17
18 import com.google.auto.service.AutoService
19 import com.google.devtools.ksp.processing.CodeGenerator
20 import com.google.devtools.ksp.processing.Dependencies
21 import com.google.devtools.ksp.processing.KSPLogger
22 import com.google.devtools.ksp.processing.Resolver
23 import com.google.devtools.ksp.processing.SymbolProcessor
24 import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
25 import com.google.devtools.ksp.processing.SymbolProcessorProvider
26 import com.google.devtools.ksp.symbol.KSAnnotated
27 import com.google.devtools.ksp.symbol.KSDeclaration
28 import com.google.devtools.ksp.symbol.KSFile
29 import com.squareup.kotlinpoet.AnnotationSpec
30 import com.squareup.kotlinpoet.ClassName
31 import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
32 import com.squareup.kotlinpoet.ksp.writeTo
33 import com.squareup.moshi.JsonClass
34 import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator
35 import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED
36 import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES
37 import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES
38 import com.squareup.moshi.kotlin.codegen.api.ProguardConfig
39 import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator
40 import java.io.OutputStreamWriter
41 import java.nio.charset.StandardCharsets
42
43 @AutoService(SymbolProcessorProvider::class)
44 public class JsonClassSymbolProcessorProvider : SymbolProcessorProvider {
45 override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
46 return JsonClassSymbolProcessor(environment)
47 }
48 }
49
50 private class JsonClassSymbolProcessor(
51 environment: SymbolProcessorEnvironment
52 ) : SymbolProcessor {
53
54 private companion object {
55 val JSON_CLASS_NAME = JsonClass::class.qualifiedName!!
56 }
57
58 private val codeGenerator = environment.codeGenerator
59 private val logger = environment.logger
<lambda>null60 private val generatedOption = environment.options[OPTION_GENERATED]?.also {
61 logger.check(it in POSSIBLE_GENERATED_NAMES) {
62 "Invalid option value for $OPTION_GENERATED. Found $it, allowable values are ${POSSIBLE_GENERATED_NAMES.keys}."
63 }
64 }
65 private val generateProguardRules = environment.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true
66
processnull67 override fun process(resolver: Resolver): List<KSAnnotated> {
68 val generatedAnnotation = generatedOption?.let {
69 AnnotationSpec.builder(ClassName.bestGuess(it))
70 .addMember("value = [%S]", JsonClassSymbolProcessor::class.java.canonicalName)
71 .addMember("comments = %S", "https://github.com/square/moshi")
72 .build()
73 }
74
75 for (type in resolver.getSymbolsWithAnnotation(JSON_CLASS_NAME)) {
76 // For the smart cast
77 if (type !is KSDeclaration) {
78 logger.error("@JsonClass can't be applied to $type: must be a Kotlin class", type)
79 continue
80 }
81
82 val jsonClassAnnotation = type.findAnnotationWithType<JsonClass>() ?: continue
83
84 val generator = jsonClassAnnotation.generator
85
86 if (generator.isNotEmpty()) continue
87
88 if (!jsonClassAnnotation.generateAdapter) continue
89
90 try {
91 val originatingFile = type.containingFile!!
92 val adapterGenerator = adapterGenerator(logger, resolver, type) ?: return emptyList()
93 val preparedAdapter = adapterGenerator
94 .prepare(generateProguardRules) { spec ->
95 spec.toBuilder()
96 .apply {
97 generatedAnnotation?.let(::addAnnotation)
98 }
99 .addOriginatingKSFile(originatingFile)
100 .build()
101 }
102 preparedAdapter.spec.writeTo(codeGenerator, aggregating = false)
103 preparedAdapter.proguardConfig?.writeTo(codeGenerator, originatingFile)
104 } catch (e: Exception) {
105 logger.error(
106 "Error preparing ${type.simpleName.asString()}: ${e.stackTrace.joinToString("\n")}"
107 )
108 }
109 }
110 return emptyList()
111 }
112
adapterGeneratornull113 private fun adapterGenerator(
114 logger: KSPLogger,
115 resolver: Resolver,
116 originalType: KSDeclaration,
117 ): AdapterGenerator? {
118 val type = targetType(originalType, resolver, logger) ?: return null
119
120 val properties = mutableMapOf<String, PropertyGenerator>()
121 for (property in type.properties.values) {
122 val generator = property.generator(logger, resolver, originalType)
123 if (generator != null) {
124 properties[property.name] = generator
125 }
126 }
127
128 for ((name, parameter) in type.constructor.parameters) {
129 if (type.properties[parameter.name] == null && !parameter.hasDefault) {
130 // TODO would be nice if we could pass the parameter node directly?
131 logger.error("No property for required constructor parameter $name", originalType)
132 return null
133 }
134 }
135
136 // Sort properties so that those with constructor parameters come first.
137 val sortedProperties = properties.values.sortedBy {
138 if (it.hasConstructorParameter) {
139 it.target.parameterIndex
140 } else {
141 Integer.MAX_VALUE
142 }
143 }
144
145 return AdapterGenerator(type, sortedProperties)
146 }
147 }
148
149 /** Writes this config to a [codeGenerator]. */
ProguardConfignull150 private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator, originatingKSFile: KSFile) {
151 val file = codeGenerator.createNewFile(
152 dependencies = Dependencies(aggregating = false, originatingKSFile),
153 packageName = "",
154 fileName = outputFilePathWithoutExtension(targetClass.canonicalName),
155 extensionName = "pro"
156 )
157 // Don't use writeTo(file) because that tries to handle directories under the hood
158 OutputStreamWriter(file, StandardCharsets.UTF_8)
159 .use(::writeTo)
160 }
161