Skip to content

Commit

Permalink
add: MinifyCheck AstDiff and MinifyCheck phase
Browse files Browse the repository at this point in the history
code format

code format
  • Loading branch information
tmdghks authored and jhnaldo committed Oct 29, 2024
1 parent e12650e commit 73683d3
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 8 deletions.
7 changes: 6 additions & 1 deletion .completion
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ _esmeta_completions() {
local cur prev opts lastc informats outformats datafiles
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
cmdList="help extract compile build-cfg tycheck parse eval web test262-test inject inject-tracer inject-minify mutate fuzz minify-fuzz delta-debug coverage-investigate analyze"
cmdList="help extract compile build-cfg tycheck parse eval web test262-test inject inject-tracer inject-minify mutate fuzz minify-fuzz delta-debug coverage-investigate minify-check analyze"
globalOpt="-silent -error -status -time -test262dir"
helpOpt=""
extractOpt="-extract:target -extract:log -extract:eval -extract:repl"
Expand All @@ -26,6 +26,7 @@ _esmeta_completions() {
mutateOpt="-mutate:out -mutate:mutator -mutate:untilValid"
fuzzOpt="-fuzz:log -fuzz:log-interval -fuzz:out -fuzz:debug -fuzz:timeout -fuzz:trial -fuzz:duration -fuzz:seed -fuzz:cp -fuzz:k-fs -fuzz:init"
coverageinvestigateOpt="-coverage-investigate:out"
minifycheckOpt="-minify-check:out"
analyzeOpt="-analyze:repl"
# completion for commands
case "${COMP_CWORD}" in
Expand Down Expand Up @@ -107,6 +108,10 @@ _esmeta_completions() {
COMPREPLY=($(compgen -W "${globalOpt} ${extractOpt} ${compileOpt} ${buildcfgOpt} ${coverageinvestigateOpt}"))
return 0
;;
minify-check)
COMPREPLY=($(compgen -W "${globalOpt} ${extractOpt} ${minifycheckOpt}"))
return 0
;;
analyze)
COMPREPLY=($(compgen -W "${globalOpt} ${extractOpt} ${compileOpt} ${buildcfgOpt} ${analyzeOpt}"))
return 0
Expand Down
9 changes: 9 additions & 0 deletions src/main/scala/esmeta/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,15 @@ case object CmdCoverageInvestigate
)
}

/** `minify-check` command */
case object CmdMinifyCheck
extends Command("minify-check", CmdExtract >> MinifyCheck) {
val help = "check differences of ast between original and minified code."
val examples = List(
"esmeta minify-check a.js -minify-check:out=out",
)
}

// -----------------------------------------------------------------------------
// ECMAScript Static Analysis (Meta-Level Static Analysis)
// -----------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/esmeta/ESMeta.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ object ESMeta extends Git(BASE_DIR) {
CmdMinifyFuzz,
CmdDeltaDebug,
CmdCoverageInvestigate,
CmdMinifyCheck,
// ECMAScript Static Analysis (Meta-Level Static Analysis)
CmdAnalyze,
)
Expand Down Expand Up @@ -111,6 +112,7 @@ object ESMeta extends Git(BASE_DIR) {
Mutate,
Fuzz,
CoverageInvestigate,
MinifyCheck,
// ECMAScript Static Analysis (Meta-Level Static Analysis)
Analyze,
)
Expand Down
10 changes: 10 additions & 0 deletions src/main/scala/esmeta/es/Ast.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ sealed trait Ast extends ESElem with Locational {
) + "ParseNode"

/** flatten statements */
def flattenSyntactic: List[Ast] = this match
case Syntactic(_, _, _, children) =>
this :: (
for {
child <- children if child.isDefined
ast <- child.get.flattenSyntactic
} yield ast
).toList
case lex: Lexical => List(lex)

// TODO refactoring
def flattenStmt: List[Ast] = this match
case Syntactic("Script", _, 0, Vector(Some(body))) =>
Expand Down
83 changes: 76 additions & 7 deletions src/main/scala/esmeta/es/util/fuzzer/MinifyChecker.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package esmeta.es.util.fuzzer

import scala.util.*
import esmeta.cfg.CFG
import esmeta.es.Ast
import esmeta.es.*
import esmeta.parser.ESParser
import esmeta.spec.Spec
import akka.http.scaladsl.model.headers.CacheDirectives.`max-age`

class MinifyChecker(
cfg: CFG,
spec: Spec,
minify: String => Option[String], // minify function
config: MinifyCheckerConfig = MinifyCheckerConfig(),
) {
Expand All @@ -15,11 +16,14 @@ class MinifyChecker(
*/
def check(code: String): Option[MinifyCheckResult] = minify(code).map {
minified =>
val originalAst = ESParser(cfg.grammar)("Script").from(code)
val minifiedAst = ESParser(cfg.grammar)("Script").from(minified)
val originalAst = ESParser(spec.grammar)("Script").from(code)
val minifiedAst = ESParser(spec.grammar)("Script").from(minified)

val diff = checkAstDiff(originalAst, minifiedAst)
MinifyCheckResult(
diff = diff,
original = code,
minified = minified,
)
}

Expand All @@ -40,8 +44,71 @@ class MinifyChecker(
def checkAstDiff(ast1: Ast, ast2: Ast): List[Ast] =
checkAstDiffSuppl(ast1, ast2, Nil)

/* Recursive helper function for checkAstDiff */
def checkAstDiffSuppl(ast1: Ast, ast2: Ast, acc: List[Ast]): List[Ast] = ???
/* Recursive helper function for checkAstDiff using Myers algorithm */
def checkAstDiffSuppl(ast1: Ast, ast2: Ast, acc: List[Ast]): List[Ast] =
val flattenedAst1 = ast1.flattenSyntactic
val flattenedAst2 = ast2.flattenSyntactic

myersDiff(flattenedAst1, flattenedAst2)

private def myersDiffAux(
flattenedAst1: List[Ast],
flattenedAst2: List[Ast],
acc: List[Ast],
): List[Ast] =
val max = flattenedAst1.length + flattenedAst2.length
val v = Array.fill(2 * max + 1)(0)
val offset = max

var result = acc

println("------flattenedAst1-----")
flattenedAst1.foreach(ast => println(ast.name))
println("------------------------")

println("------flattenedAst2-----")
flattenedAst2.foreach(ast => println(ast.name))
println("------------------------")

// implement diff algorithm here
// partially implement just comparing ast names
// todo: make metrics for comparing asts
val n = flattenedAst1.length
val m = flattenedAst2.length

var d = 0
while (d <= max) do
for (k <- -d to d by 2) do
var i =
if (k == -d || (k != d && v(offset + k - 1) < v(offset + k + 1)))
v(offset + k + 1)
else
v(offset + k - 1) + 1
var j = i - k
while (
i < n
&& j < m
&& flattenedAst1(i).name == flattenedAst2(j).name
)
do
i += 1
j += 1
v(offset + k) = i
if (i >= n && j >= m) then return result
if (
i < n && j < m
&& flattenedAst1(i).name != flattenedAst2(j).name
)
then result = flattenedAst1(j) :: result
d += 1

println(s"different ast parts: ${result.map(_.name)}")
result

private def myersDiff(
flattenedAst1: List[Ast],
flattenedAst2: List[Ast],
): List[Ast] = myersDiffAux(flattenedAst1, flattenedAst2, Nil)
}

case class MinifyCheckerConfig(
Expand All @@ -51,4 +118,6 @@ case class MinifyCheckerConfig(
case class MinifyCheckResult(
// influentialOptions: List[String], // TODO: check options that affect minification (not right now)
diff: List[Ast],
original: String,
minified: String,
)
60 changes: 60 additions & 0 deletions src/main/scala/esmeta/phase/MinifyCheck.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package esmeta.phase

import scala.util.*
import scala.io.Source
import esmeta.CommandConfig
import esmeta.util.*
import esmeta.util.BaseUtils.*
import esmeta.parser.ESParser
import esmeta.spec.Spec
import esmeta.es.util.fuzzer.{MinifyChecker, MinifyCheckerConfig}
import esmeta.js.minifier.Minifier
import esmeta.util.SystemUtils.*

case object MinifyCheck extends Phase[Spec, Unit] {
val name = "minify-check"
val help = "check differences of ast between original and minified code."

def apply(spec: Spec, cmdConfig: CommandConfig, config: Config): Unit =
val fileName = getFirstFilename(cmdConfig, name)

// Step 1: Read the content of a.js and b.js
val codeA = Source.fromFile(fileName).getLines().mkString("\n")

// Step 2: Initialize MinifyChecker
val minifyFunction: String => Option[String] = code =>
Minifier.minifySwc(code) match
case Failure(exception) => println(s"[minify-check] $exception"); None
case Success(minified) => Some(minified)

val checker = new MinifyChecker(spec, minifyFunction, MinifyCheckerConfig())

// Step 3: Use the check method to compare the original code with its minified version
val resultA = checker.check(codeA)

// Step 4: Print the results
println(s"Results for ${fileName}:")
resultA match {
case Some(res) =>
println(s"Diff Number: ${res.diff.size}")
println(s"Diff: ${res.diff.map(ast => (ast.name, ast)).mkString(", ")}")
println(s"Original: ${res.original}")
println(s"Minified: ${res.minified}")
case None =>
println("Minification failed.")
}

val defaultConfig: Config = Config()

val options: List[PhaseOption[Config]] = List(
(
"out",
StrOption((c, s) => c.out = Some(s)),
"output json file path.",
),
)

class Config(
var out: Option[String] = None,
)
}

0 comments on commit 73683d3

Please sign in to comment.