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