import java

/*
 * Counting nontrivial literal occurrences
 */

private predicate trivialPositiveIntValue(string s) {
  s = "0" or
  s = "1" or
  s = "2" or
  s = "3" or
  s = "4" or
  s = "5" or
  s = "6" or
  s = "7" or
  s = "8" or
  s = "9" or
  s = "10" or
  s = "11" or
  s = "12" or
  s = "13" or
  s = "14" or
  s = "15" or
  s = "16" or
  s = "17" or
  s = "18" or
  s = "19" or
  s = "20" or
  s = "16" or
  s = "32" or
  s = "64" or
  s = "128" or
  s = "256" or
  s = "512" or
  s = "1024" or
  s = "2048" or
  s = "4096" or
  s = "16384" or
  s = "32768" or
  s = "65536" or
  s = "1048576" or
  s = "2147483648" or
  s = "4294967296" or
  s = "15" or
  s = "31" or
  s = "63" or
  s = "127" or
  s = "255" or
  s = "511" or
  s = "1023" or
  s = "2047" or
  s = "4095" or
  s = "16383" or
  s = "32767" or
  s = "65535" or
  s = "1048577" or
  s = "2147483647" or
  s = "4294967295" or
  s = "0x00000001" or
  s = "0x00000002" or
  s = "0x00000004" or
  s = "0x00000008" or
  s = "0x00000010" or
  s = "0x00000020" or
  s = "0x00000040" or
  s = "0x00000080" or
  s = "0x00000100" or
  s = "0x00000200" or
  s = "0x00000400" or
  s = "0x00000800" or
  s = "0x00001000" or
  s = "0x00002000" or
  s = "0x00004000" or
  s = "0x00008000" or
  s = "0x00010000" or
  s = "0x00020000" or
  s = "0x00040000" or
  s = "0x00080000" or
  s = "0x00100000" or
  s = "0x00200000" or
  s = "0x00400000" or
  s = "0x00800000" or
  s = "0x01000000" or
  s = "0x02000000" or
  s = "0x04000000" or
  s = "0x08000000" or
  s = "0x10000000" or
  s = "0x20000000" or
  s = "0x40000000" or
  s = "0x80000000" or
  s = "0x00000001" or
  s = "0x00000003" or
  s = "0x00000007" or
  s = "0x0000000f" or
  s = "0x0000001f" or
  s = "0x0000003f" or
  s = "0x0000007f" or
  s = "0x000000ff" or
  s = "0x000001ff" or
  s = "0x000003ff" or
  s = "0x000007ff" or
  s = "0x00000fff" or
  s = "0x00001fff" or
  s = "0x00003fff" or
  s = "0x00007fff" or
  s = "0x0000ffff" or
  s = "0x0001ffff" or
  s = "0x0003ffff" or
  s = "0x0007ffff" or
  s = "0x000fffff" or
  s = "0x001fffff" or
  s = "0x003fffff" or
  s = "0x007fffff" or
  s = "0x00ffffff" or
  s = "0x01ffffff" or
  s = "0x03ffffff" or
  s = "0x07ffffff" or
  s = "0x0fffffff" or
  s = "0x1fffffff" or
  s = "0x3fffffff" or
  s = "0x7fffffff" or
  s = "0xffffffff" or
  s = "0x0001" or
  s = "0x0002" or
  s = "0x0004" or
  s = "0x0008" or
  s = "0x0010" or
  s = "0x0020" or
  s = "0x0040" or
  s = "0x0080" or
  s = "0x0100" or
  s = "0x0200" or
  s = "0x0400" or
  s = "0x0800" or
  s = "0x1000" or
  s = "0x2000" or
  s = "0x4000" or
  s = "0x8000" or
  s = "0x0001" or
  s = "0x0003" or
  s = "0x0007" or
  s = "0x000f" or
  s = "0x001f" or
  s = "0x003f" or
  s = "0x007f" or
  s = "0x00ff" or
  s = "0x01ff" or
  s = "0x03ff" or
  s = "0x07ff" or
  s = "0x0fff" or
  s = "0x1fff" or
  s = "0x3fff" or
  s = "0x7fff" or
  s = "0xffff" or
  s = "0x01" or
  s = "0x02" or
  s = "0x04" or
  s = "0x08" or
  s = "0x10" or
  s = "0x20" or
  s = "0x40" or
  s = "0x80" or
  s = "0x01" or
  s = "0x03" or
  s = "0x07" or
  s = "0x0f" or
  s = "0x1f" or
  s = "0x3f" or
  s = "0x7f" or
  s = "0xff" or
  s = "0x00" or
  s = "10" or
  s = "100" or
  s = "1000" or
  s = "10000" or
  s = "100000" or
  s = "1000000" or
  s = "10000000" or
  s = "100000000" or
  s = "1000000000"
}

private predicate trivialIntValue(string s) {
  trivialPositiveIntValue(s)
  or
  exists(string pos | trivialPositiveIntValue(pos) and s = "-" + pos)
}

private predicate intTrivial(Literal lit) {
  exists(string v | trivialIntValue(v) and v = lit.getLiteral())
}

private predicate longTrivial(Literal lit) {
  exists(string v | trivialIntValue(v) and v + "L" = lit.getLiteral())
}

private predicate powerOfTen(float f) {
  f = 10 or
  f = 100 or
  f = 1000 or
  f = 10000 or
  f = 100000 or
  f = 1000000 or
  f = 10000000 or
  f = 100000000 or
  f = 1000000000
}

private predicate floatTrivial(Literal lit) {
  (lit instanceof FloatingPointLiteral or lit instanceof DoubleLiteral) and
  exists(float f |
    f = lit.getValue().toFloat() and
    (f.abs() <= 20.0 or powerOfTen(f))
  )
}

private predicate stringTrivial(StringLiteral lit) { lit.getLiteral().length() < 8 }

private predicate small(Literal lit) { lit.getLiteral().length() <= 1 }

private predicate trivial(Literal lit) {
  lit instanceof CharacterLiteral or
  lit instanceof BooleanLiteral or
  lit instanceof NullLiteral or
  intTrivial(lit) or
  floatTrivial(lit) or
  stringTrivial(lit) or
  longTrivial(lit) or
  small(lit) or
  excludedLiteral(lit)
}

private predicate literalIsConstantInitializer(Literal literal, Field f) {
  exists(AssignExpr e, VarAccess access |
    access = e.getAChildExpr() and
    f = access.getVariable() and
    access.getIndex() = 0 and
    f.isFinal() and
    f.isStatic() and
    literal = e.getAChildExpr() and
    literal.getIndex() = 1
  ) and
  not trivial(literal)
}

private predicate nonTrivialValue(string value, Literal literal, string context) {
  value = literal.getLiteral() and
  not trivial(literal) and
  not literalIsConstantInitializer(literal, _) and
  not literal.getParent*() instanceof ArrayInit and
  not literal.getParent+() instanceof Annotation and
  exists(MethodAccess ma | literal = ma.getAnArgument() and ma.getMethod().getName() = context)
}

private predicate valueOccurrenceCount(string value, int n, string context) {
  n = strictcount(Literal lit | nonTrivialValue(value, lit, context)) and
  n > 20
}

private predicate occurenceCount(Literal lit, string value, int n, string context) {
  valueOccurrenceCount(value, n, context) and
  value = lit.getLiteral() and
  nonTrivialValue(_, lit, context)
}

/*
 * Literals repeated frequently
 */

private predicate check(Literal lit, string value, int n, string context, CompilationUnit f) {
  // Check that the literal is nontrivial
  not trivial(lit) and
  // Check that it is repeated a number of times
  occurenceCount(lit, value, n, context) and
  n > 20 and
  f = lit.getCompilationUnit()
}

private predicate checkWithFileCount(
  string value, int overallCount, int fileCount, string context, CompilationUnit f
) {
  fileCount = strictcount(Literal lit | check(lit, value, overallCount, context, f))
}

private predicate firstOccurrence(Literal lit, string value, string context, int n) {
  exists(CompilationUnit f, int fileCount |
    checkWithFileCount(value, n, fileCount, context, f) and
    fileCount < 100 and
    check(lit, value, n, context, f) and
    not exists(Literal lit2, int start1, int start2 |
      check(lit2, value, n, context, f) and
      lit.getLocation().getStartLine() = start1 and
      lit2.getLocation().getStartLine() = start2 and
      start2 < start1
    )
  )
}

predicate isNumber(Literal lit) {
  lit.getType().getName() = "char" or
  lit.getType().getName() = "short" or
  lit.getType().getName() = "int" or
  lit.getType().getName() = "long" or
  lit.getType().getName() = "float" or
  lit.getType().getName() = "double"
}

predicate magicConstant(Literal e, string msg) {
  exists(string value, int n, string context |
    firstOccurrence(e, value, context, n) and
    msg = "Magic constant: literal '" + value + "' is used " + n.toString() + " times in calls to " +
        context
  )
}

/*
 * Literals where there is a defined constant with the same value
 */

private predicate relevantField(Field f, string value) {
  exists(Literal lit |
    not trivial(lit) and value = lit.getLiteral() and literalIsConstantInitializer(lit, f)
  )
}

private predicate relevantType(RefType t, string value, Package p) {
  exists(Literal lit | nonTrivialValue(value, lit, _) |
    lit.getEnclosingCallable().getDeclaringType() = t and p = t.getPackage()
  )
}

private predicate fieldUsedInContext(Field constField, string context) {
  literalIsConstantInitializer(_, constField) and
  exists(MethodAccess ma |
    constField.getAnAccess() = ma.getAnArgument() and
    ma.getMethod().getName() = context
  )
}

private predicate candidateConstantForLiteral(
  Field constField, RefType literalType, Literal magicLiteral, string context
) {
  exists(Literal initLiteral |
    literalIsConstantInitializer(initLiteral, constField) and
    exists(string value |
      value = initLiteral.getLiteral() and
      nonTrivialValue(value, magicLiteral, context) and
      fieldUsedInContext(constField, context)
    ) and
    literalType = magicLiteral.getEnclosingCallable().getDeclaringType()
  )
}

private RefType inheritsProtected(Field f) {
  (f.isProtected() and result.getASupertype() = f.getDeclaringType())
  or
  exists(RefType mid | mid = inheritsProtected(f) and result.getASupertype() = mid)
}

private predicate constantForLiteral(
  Field field, string value, RefType fromType, Literal magicLiteral, string context
) {
  //public fields in public classes
  (
    candidateConstantForLiteral(field, fromType, magicLiteral, context) and
    relevantField(field, value) and
    field.getDeclaringType().isPublic() and
    field.isPublic() and
    relevantType(fromType, value, _)
  )
  or
  //in same class
  (
    candidateConstantForLiteral(field, fromType, magicLiteral, context) and
    relevantField(field, value) and
    fromType = field.getDeclaringType() and
    relevantType(fromType, value, _)
  )
  or
  //in subclass and not private
  (
    candidateConstantForLiteral(field, fromType, magicLiteral, context) and
    relevantField(field, value) and
    field.isProtected() and
    fromType = inheritsProtected(field) and
    relevantType(fromType, value, _)
  )
  or
  //not private and in same package
  (
    candidateConstantForLiteral(field, fromType, magicLiteral, context) and
    relevantField(field, value) and
    field.isPackageProtected() and
    exists(Package p |
      exists(CompilationUnit cu | cu = field.getCompilationUnit() and cu.getPackage() = p) and
      relevantType(fromType, value, p)
    )
  )
}

private predicate canUseFieldInsteadOfLiteral(Field constField, Literal magicLiteral, string context) {
  constantForLiteral(constField, _, _, magicLiteral, context)
}

predicate literalInsteadOfConstant(
  Literal magicLiteral, string message, Field constField, string linkText
) {
  exists(string context |
    canUseFieldInsteadOfLiteral(constField, magicLiteral, context) and
    message = "Literal value '" + magicLiteral.getLiteral() + "' used " + " in a call to " + context
        + "; consider using the defined constant $@." and
    linkText = constField.getName() and
    (
      constField.getCompilationUnit() = magicLiteral.getCompilationUnit() or
      not almostPrivate(constField)
    ) and
    (
      constField.getCompilationUnit().getPackage() = magicLiteral.getCompilationUnit().getPackage() or
      not almostPackageProtected(constField)
    )
  )
}

private predicate almostPrivate(Field f) {
  not exists(VarAccess va |
    va.getVariable() = f and va.getCompilationUnit() != f.getCompilationUnit()
  )
  or
  exists(Interface i | i = f.getDeclaringType() |
    forall(VarAccess va | va.getVariable() = f |
      va.getEnclosingCallable().getDeclaringType().getASupertype*() = i
    )
  )
}

private predicate almostPackageProtected(Field f) {
  not exists(VarAccess va | va.getVariable() = f |
    va.getCompilationUnit().getPackage() != f.getCompilationUnit().getPackage()
  )
}

/*
 * Removing literals from uninteresting contexts
 */

private predicate excludedLiteral(Literal lit) {
  // Remove test cases
  lit.getEnclosingCallable().getDeclaringType() instanceof TestClass
  or
  exists(MethodAccess ma | lit = ma.getAnArgument() | ma.getMethod() instanceof TestMethod)
}
