Skip to content

Commit 9f7daa2

Browse files
Moves definitions, compiler, sbt-exercises to its own repo
0 parents  commit 9f7daa2

File tree

36 files changed

+3091
-0
lines changed

36 files changed

+3091
-0
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
/*
2+
* Copyright 2014-2020 47 Degrees <https://47deg.com>
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+
* http://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+
17+
package org.scalaexercises.compiler
18+
19+
import org.scalaexercises.definitions.{BuildInfo, Library}
20+
import org.scalaexercises.runtime.Timestamp
21+
22+
import scala.reflect.runtime.{universe => ru}
23+
import cats.implicits._
24+
import github4s.Github
25+
import Comments.Mode
26+
import CommentRendering.RenderedComment
27+
import cats.effect.{ContextShift, IO}
28+
import github4s.domain.Commit
29+
import org.http4s.client.blaze.BlazeClientBuilder
30+
31+
import scala.concurrent.ExecutionContext
32+
33+
class CompilerJava {
34+
def compile(
35+
library: AnyRef,
36+
sources: Array[String],
37+
paths: Array[String],
38+
buildMetaInfo: AnyRef,
39+
baseDir: String,
40+
targetPackage: String,
41+
fetchContributors: Boolean
42+
): Array[String] = {
43+
44+
Compiler()
45+
.compile(
46+
library.asInstanceOf[Library],
47+
sources.toList,
48+
paths.toList,
49+
buildMetaInfo.asInstanceOf[BuildInfo],
50+
baseDir,
51+
targetPackage,
52+
fetchContributors
53+
)
54+
.fold(`🍺` => throw new Exception(`🍺`), out => Array(out._1, out._2))
55+
}
56+
}
57+
58+
case class Compiler() {
59+
60+
lazy val sourceTextExtractor = new SourceTextExtraction()
61+
62+
implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
63+
implicit val ec: ExecutionContext = ExecutionContext.global
64+
65+
lazy val clientResource = BlazeClientBuilder[IO](ec).resource
66+
67+
def compile(
68+
library: Library,
69+
sources: List[String],
70+
paths: List[String],
71+
buildMetaInfo: BuildInfo,
72+
baseDir: String,
73+
targetPackage: String,
74+
fetchContributors: Boolean
75+
) = {
76+
77+
val mirror = ru.runtimeMirror(library.getClass.getClassLoader)
78+
import mirror.universe._
79+
80+
val extracted = sourceTextExtractor.extractAll(sources, paths, baseDir)
81+
val internal = CompilerInternal(mirror, extracted)
82+
val compilationTimestamp = Timestamp.fromDate(new java.util.Date())
83+
84+
case class LibraryInfo(
85+
symbol: ClassSymbol,
86+
comment: RenderedComment.Aux[Mode.Library],
87+
sections: List[SectionInfo],
88+
color: Option[String],
89+
logoPath: String,
90+
logoData: Option[String] = None,
91+
owner: String,
92+
repository: String
93+
)
94+
95+
case class ContributionInfo(
96+
sha: String,
97+
message: String,
98+
timestamp: String,
99+
url: String,
100+
author: String,
101+
authorUrl: String,
102+
avatarUrl: String
103+
)
104+
105+
case class SectionInfo(
106+
symbol: ClassSymbol,
107+
comment: RenderedComment.Aux[Mode.Section],
108+
exercises: List[ExerciseInfo],
109+
imports: List[String] = Nil,
110+
path: Option[String] = None,
111+
contributions: List[ContributionInfo] = Nil
112+
)
113+
114+
case class ExerciseInfo(
115+
symbol: MethodSymbol,
116+
comment: RenderedComment.Aux[Mode.Exercise],
117+
code: String,
118+
qualifiedMethod: String,
119+
packageName: String,
120+
imports: List[String] = Nil
121+
)
122+
123+
def enhanceDocError(path: List[String])(error: String) =
124+
s"""$error in ${path.mkString(".")}"""
125+
126+
def maybeMakeLibraryInfo(library: Library) =
127+
for {
128+
symbol <- internal.instanceToClassSymbol(library)
129+
symbolPath = internal.symbolToPath(symbol)
130+
comment <- (internal
131+
.resolveComment(symbolPath)
132+
.flatMap(Comments.parseAndRender[Mode.Library]))
133+
.leftMap(enhanceDocError(symbolPath))
134+
sections <- checkEmptySectionList(symbol, library).flatMap {
135+
_.sections
136+
.traverse(
137+
internal
138+
.instanceToClassSymbol(_)
139+
.flatMap(symbol => maybeMakeSectionInfo(library, symbol))
140+
)
141+
}
142+
} yield LibraryInfo(
143+
symbol = symbol,
144+
comment = comment,
145+
sections = sections,
146+
color = library.color,
147+
logoPath = library.logoPath,
148+
owner = library.owner,
149+
repository = library.repository
150+
)
151+
152+
def checkEmptySectionList(librarySymbol: Symbol, library: Library): Either[String, Library] =
153+
if (library.sections.isEmpty)
154+
Either.left(
155+
s"Unable to create ${librarySymbol.fullName}: A Library object must contain at least one section"
156+
)
157+
else
158+
Either.right(library)
159+
160+
def fetchContributions(
161+
owner: String,
162+
repository: String,
163+
path: String
164+
): List[ContributionInfo] = {
165+
println(s"Fetching contributions for repository $owner/$repository file $path")
166+
val contribs = clientResource.use { client =>
167+
Github[IO](client, sys.env.get("GITHUB_TOKEN")).repos
168+
.listCommits(owner, repository, None, Option(path))
169+
}
170+
171+
contribs.unsafeRunSync().result match {
172+
case Right(result) =>
173+
result.collect({
174+
case Commit(sha, message, date, url, Some(login), Some(avatar_url), Some(author_url)) =>
175+
ContributionInfo(
176+
sha = sha,
177+
message = message,
178+
timestamp = date,
179+
url = url,
180+
author = login,
181+
avatarUrl = avatar_url,
182+
authorUrl = author_url
183+
)
184+
})
185+
case Left(ex) => throw ex
186+
}
187+
}
188+
189+
def maybeMakeSectionInfo(library: Library, symbol: ClassSymbol) = {
190+
val symbolPath = internal.symbolToPath(symbol)
191+
val filePath = extracted.symbolPaths.get(symbol.toString).filterNot(_.isEmpty)
192+
for {
193+
comment <- internal
194+
.resolveComment(symbolPath)
195+
.flatMap(Comments.parseAndRender[Mode.Section])
196+
.leftMap(enhanceDocError(symbolPath))
197+
198+
contributions = (if (fetchContributors) filePath else None).fold(
199+
List.empty[ContributionInfo]
200+
)(path => fetchContributions(library.owner, library.repository, path))
201+
202+
exercises <- symbol.toType.decls.toList
203+
.filter(symbol =>
204+
symbol.isPublic && !symbol.isSynthetic &&
205+
symbol.name != termNames.CONSTRUCTOR && symbol.isMethod
206+
)
207+
.map(_.asMethod)
208+
.filterNot(_.isGetter)
209+
.traverse(maybeMakeExerciseInfo)
210+
} yield SectionInfo(
211+
symbol = symbol,
212+
comment = comment,
213+
exercises = exercises,
214+
imports = Nil,
215+
path = extracted.symbolPaths.get(symbol.toString),
216+
contributions = contributions
217+
)
218+
}
219+
def maybeMakeExerciseInfo(
220+
symbol: MethodSymbol
221+
) = {
222+
val symbolPath = internal.symbolToPath(symbol)
223+
val pkgName = symbolPath.headOption.fold("defaultPkg")(identity)
224+
for {
225+
comment <- internal
226+
.resolveComment(symbolPath)
227+
.flatMap(Comments.parseAndRender[Mode.Exercise])
228+
.leftMap(enhanceDocError(symbolPath))
229+
method <- internal.resolveMethod(symbolPath)
230+
} yield ExerciseInfo(
231+
symbol = symbol,
232+
comment = comment,
233+
code = method.code,
234+
packageName = pkgName,
235+
imports = method.imports,
236+
qualifiedMethod = symbolPath.mkString(".")
237+
)
238+
}
239+
240+
val treeGen = TreeGen[mirror.universe.type](mirror.universe)
241+
242+
def generateTree(libraryInfo: LibraryInfo): (TermName, Tree) = {
243+
244+
val (sectionTerms, sectionAndExerciseTrees) =
245+
libraryInfo.sections.map { sectionInfo =>
246+
val (exerciseTerms, exerciseTrees) =
247+
sectionInfo.exercises.map { exerciseInfo =>
248+
treeGen.makeExercise(
249+
libraryName = libraryInfo.comment.name,
250+
name = internal.unapplyRawName(exerciseInfo.symbol.name),
251+
description = exerciseInfo.comment.description,
252+
code = exerciseInfo.code,
253+
qualifiedMethod = exerciseInfo.qualifiedMethod,
254+
packageName = exerciseInfo.packageName,
255+
imports = exerciseInfo.imports,
256+
explanation = exerciseInfo.comment.explanation
257+
)
258+
}.unzip
259+
260+
val (contributionTerms, contributionTrees) =
261+
sectionInfo.contributions.map { contributionInfo =>
262+
treeGen.makeContribution(
263+
sha = contributionInfo.sha,
264+
message = contributionInfo.message,
265+
timestamp = contributionInfo.timestamp,
266+
url = contributionInfo.url,
267+
author = contributionInfo.author,
268+
authorUrl = contributionInfo.authorUrl,
269+
avatarUrl = contributionInfo.avatarUrl
270+
)
271+
}.unzip
272+
273+
val (sectionTerm, sectionTree) =
274+
treeGen.makeSection(
275+
libraryName = libraryInfo.comment.name,
276+
name = sectionInfo.comment.name,
277+
description = sectionInfo.comment.description,
278+
exerciseTerms = exerciseTerms,
279+
imports = sectionInfo.imports,
280+
path = sectionInfo.path,
281+
contributionTerms = contributionTerms
282+
)
283+
284+
(sectionTerm, sectionTree :: exerciseTrees ++ contributionTrees)
285+
}.unzip
286+
287+
val allDependencies: List[String] = {
288+
val libraryAsDependency =
289+
s"${buildMetaInfo.organization}:${buildMetaInfo.name}_${buildMetaInfo.scalaVersion
290+
.substring(0, 4)}:${buildMetaInfo.version}"
291+
List(libraryAsDependency) // :: buildMetaInfo.libraryDependencies.toList
292+
// Evaluator can resolve transitive dependencies, the reason of only passing the library as a dependency
293+
}
294+
295+
val (buildInfoTerm, buildInfoTree) =
296+
treeGen.makeBuildInfo(
297+
name = libraryInfo.comment.name,
298+
resolvers = buildMetaInfo.resolvers.toList,
299+
libraryDependencies = allDependencies
300+
)
301+
302+
val (libraryTerm, libraryTree) = treeGen.makeLibrary(
303+
name = libraryInfo.comment.name,
304+
description = libraryInfo.comment.description,
305+
color = libraryInfo.color,
306+
logoPath = libraryInfo.logoPath,
307+
logoData = libraryInfo.logoData,
308+
sectionTerms = sectionTerms,
309+
owner = libraryInfo.owner,
310+
repository = libraryInfo.repository,
311+
timestamp = compilationTimestamp,
312+
buildInfoT = buildInfoTerm
313+
)
314+
315+
libraryTerm -> treeGen.makePackage(
316+
packageName = targetPackage,
317+
trees = libraryTree :: (sectionAndExerciseTrees.flatten :+ buildInfoTree)
318+
)
319+
320+
}
321+
322+
maybeMakeLibraryInfo(library)
323+
.map(generateTree)
324+
.map { case (TermName(kname), v) => s"$targetPackage.$kname" -> showCode(v) }
325+
326+
}
327+
328+
private case class CompilerInternal(
329+
mirror: ru.Mirror,
330+
sourceExtracted: SourceTextExtraction#Extracted
331+
) {
332+
import mirror.universe._
333+
334+
def instanceToClassSymbol(instance: AnyRef) =
335+
Either
336+
.catchNonFatal(mirror.classSymbol(instance.getClass))
337+
.leftMap(e => s"Unable to get module symbol for $instance due to: $e")
338+
339+
def resolveComment(path: List[String]) /*: Either[String, Comment] */ =
340+
Either.fromOption(
341+
sourceExtracted.comments.get(path).map(_.comment),
342+
s"""Unable to retrieve doc comment for ${path.mkString(".")}"""
343+
)
344+
345+
def resolveMethod(path: List[String]): Either[String, SourceTextExtraction#ExtractedMethod] =
346+
Either.fromOption(
347+
sourceExtracted.methods.get(path),
348+
s"""Unable to retrieve code for method ${path.mkString(".")}"""
349+
)
350+
351+
def symbolToPath(symbol: Symbol): List[String] = {
352+
def process(symbol: Symbol): List[String] = {
353+
lazy val owner = symbol.owner
354+
unapplyRawName(symbol.name) match {
355+
case `ROOT` => Nil
356+
case `EMPTY_PACKAGE_NAME_STRING` => Nil
357+
case `ROOTPKG_STRING` => Nil
358+
case value if symbol != owner => value :: process(owner)
359+
case _ => Nil
360+
}
361+
}
362+
process(symbol).reverse
363+
}
364+
365+
private[compiler] def unapplyRawName(name: Name): String = name match {
366+
case TermName(value) => value
367+
case TypeName(value) => value
368+
}
369+
370+
private lazy val EMPTY_PACKAGE_NAME_STRING = unapplyRawName(termNames.EMPTY_PACKAGE_NAME)
371+
private lazy val ROOTPKG_STRING = unapplyRawName(termNames.ROOTPKG)
372+
private lazy val ROOT = "<root>" // can't find an accessible constant for this
373+
374+
}
375+
376+
}

0 commit comments

Comments
 (0)