<commit_before>fs           = require 'fs'
path         = require 'path'

_            = require 'underscore'
builtins     = require 'builtins'

module.exports = class Metadata
  constructor: (@dependencies, @parser) ->

  generate: (@root) ->
    @defs = {} # Local variable definitions
    @exports = {}
    @bindingTypes = {}
    @modules = {}
    @classes = @parser.classes
    @files = @parser.files

    @root.traverseChildren no, (exp) => @visit(exp) # `no` means Stop at scope boundaries

  visit: (exp) ->
    @["visit#{exp.constructor.name}"](exp)
  eval:  (exp) ->
    @["eval#{exp.constructor.name}"](exp)

  visitComment: (exp) ->
    # Skip the 1st comment which is added by donna
    return if exp.comment is '~Private~'

  visitClass: (exp) ->
    return unless exp.variable?
    @defs[exp.variable.base.value] = @evalClass(exp)
    no # Do not traverse into the class methods

  visitAssign: (exp) ->
    variable = @eval(exp.variable)
    value = @eval(exp.value)

    baseName = exp.variable.base.value
    switch baseName
      when 'module'
        return if exp.variable.properties.length is 0 # Ignore `module = ...` (atom/src/browser/main.coffee)
        unless exp.variable.properties?[0]?.name?.value is 'exports'
          throw new Error 'BUG: Does not support module.somthingOtherThanExports'
        baseName = 'exports'
        firstProp = exp.variable.properties[1]
      when 'exports'
        firstProp = exp.variable.properties[0]

    switch baseName
      when 'exports'
        # Handle 3 cases:
        #
        # - `exports.foo = SomeClass`
        # - `exports.foo = 42`
        # - `exports = bar`
        if firstProp
          if value.base? && @defs[value.base.value]
            # case `exports.foo = SomeClass`
            @exports[firstProp.name.value] = @defs[value.base.value]
          else
            # case `exports.foo = 42`
            unless firstProp.name.value == value.name
              @defs[firstProp.name.value] =
                name: firstProp.name.value
                bindingType: 'exportsProperty'
                type: value.type
                range: [ [exp.variable.base.locationData.first_line, exp.variable.base.locationData.first_column], [exp.variable.base.locationData.last_line, exp.variable.base.locationData.last_column ] ]
            @exports[firstProp.name.value] =
              startLineNumber:  exp.variable.base.locationData.first_line
        else
          # case `exports = bar`
          @exports = {_default: value}
          switch value.type
            when 'class'
              @bindingTypes[value.name] = "exports"

      # case left-hand-side is anything other than `exports...`
      else
        # Handle 5 common cases:
        #
        # X     = ...
        # {X}   = ...
        # {X:Y} = ...
        # X.y   = ...
        # [X]   = ...
        switch exp.variable.base.constructor.name
          when 'Literal'
            # Something we dont care about is on the right side of the `=`.
            # This could be some garbage like an if statement.
            return unless value.range

            # case _.str = ...
            if exp.variable.properties.length > 0
              nameWithPeriods = [exp.variable.base.value].concat(_.map(exp.variable.properties, (prop) -> prop.name.value)).join(".")
              @defs[nameWithPeriods] = _.extend name: nameWithPeriods, value
            else # case X = ...
              @defs[exp.variable.base.value] = _.extend name: exp.variable.base.value, value

              # satisfies the case of npm module requires (like Grim in our tests)
              if @defs[exp.variable.base.value].type == "import"
                key = @defs[exp.variable.base.value].path || @defs[exp.variable.base.value].module
                if _.isUndefined @modules[key]
                  @modules[key] = []

                @modules[key].push { name: @defs[exp.variable.base.value].name, range: @defs[exp.variable.base.value].range }

              switch @defs[exp.variable.base.value].type
                when 'function'
                  # FIXME: Ugh. This is so fucked. We shouldnt match on name in all the files in the entire project.
                  for file in @files
                    for method in file.methods
                      if @defs[exp.variable.base.value].name == method.name
                        @defs[exp.variable.base.value].doc = method.doc.comment
                        break

          when 'Obj', 'Arr'
            for key in exp.variable.base.objects
              switch key.constructor.name
                when 'Value'
                  # case {X} = ...
                  @defs[key.base.value] = _.extend {}, value,
                    name: key.base.value
                    exportsProperty: key.base.value
                    range: [ [key.base.locationData.first_line, key.base.locationData.first_column], [key.base.locationData.last_line, key.base.locationData.last_column ] ]

                  # Store the name of the exported property to the module name
                  if @defs[key.base.value].type == "import" # I *think* this will always be true
                    if _.isUndefined @modules[@defs[key.base.value].path]
                      @modules[@defs[key.base.value].path] = []
                    @modules[@defs[key.base.value].path].push {name: @defs[key.base.value].name, range: @defs[key.base.value].range}
                when 'Assign'
                  # case {X:Y} = ...
                  @defs[key.value.base.value] = _.extend {}, value,
                    name: key.value.base.value
                    exportsProperty: key.variable.base.value
                  return no # Do not continue visiting X

                else throw new Error "BUG: Unsupported require Obj structure: #{key.constructor.name}"
          else throw new Error "BUG: Unsupported require structure: #{exp.variable.base.constructor.name}"

  visitCode: (exp) ->

  visitValue: (exp) ->

  visitCall: (exp) ->

  visitLiteral: (exp) ->

  visitObj: (exp) ->

  visitAccess: (exp) ->

  visitBlock: (exp) ->

  visitTry: (exp) ->

  visitIn: (exp) ->

  evalComment: (exp) ->
    type: 'comment'
    doc: exp.comment
    range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ]

  evalClass: (exp) ->
    className = exp.variable.base.value
    superClassName = exp.parent?.base.value
    classProperties = []
    prototypeProperties = []

    classNode = _.find(@classes, (clazz) -> clazz.getFullName() == className)

    for subExp in exp.body.expressions
      switch subExp.constructor.name
        # case Prototype-level methods (this.foo = (foo) -> ...)
        when 'Assign'
          value = @eval(subExp.value)
          @defs["#{className}.#{value.name}"] = value
          classProperties.push(value)
        when 'Value'
          # case Prototype-level properties (@foo: "foo")
          for prototypeExp in subExp.base.properties
            switch prototypeExp.constructor.name
              when 'Comment'
                value = @eval(prototypeExp)
                @defs["#{value.range[0][0]}_line_comment"] = value
              else<commit_msg>Add visitExistence method<commit_after>fs           = require 'fs'
path         = require 'path'

_            = require 'underscore'
builtins     = require 'builtins'

module.exports = class Metadata
  constructor: (@dependencies, @parser) ->

  generate: (@root) ->
    @defs = {} # Local variable definitions
    @exports = {}
    @bindingTypes = {}
    @modules = {}
    @classes = @parser.classes
    @files = @parser.files

    @root.traverseChildren no, (exp) => @visit(exp) # `no` means Stop at scope boundaries

  visit: (exp) ->
    @["visit#{exp.constructor.name}"](exp)
  eval:  (exp) ->
    @["eval#{exp.constructor.name}"](exp)

  visitComment: (exp) ->
    # Skip the 1st comment which is added by donna
    return if exp.comment is '~Private~'

  visitClass: (exp) ->
    return unless exp.variable?
    @defs[exp.variable.base.value] = @evalClass(exp)
    no # Do not traverse into the class methods

  visitAssign: (exp) ->
    variable = @eval(exp.variable)
    value = @eval(exp.value)

    baseName = exp.variable.base.value
    switch baseName
      when 'module'
        return if exp.variable.properties.length is 0 # Ignore `module = ...` (atom/src/browser/main.coffee)
        unless exp.variable.properties?[0]?.name?.value is 'exports'
          throw new Error 'BUG: Does not support module.somthingOtherThanExports'
        baseName = 'exports'
        firstProp = exp.variable.properties[1]
      when 'exports'
        firstProp = exp.variable.properties[0]

    switch baseName
      when 'exports'
        # Handle 3 cases:
        #
        # - `exports.foo = SomeClass`
        # - `exports.foo = 42`
        # - `exports = bar`
        if firstProp
          if value.base? && @defs[value.base.value]
            # case `exports.foo = SomeClass`
            @exports[firstProp.name.value] = @defs[value.base.value]
          else
            # case `exports.foo = 42`
            unless firstProp.name.value == value.name
              @defs[firstProp.name.value] =
                name: firstProp.name.value
                bindingType: 'exportsProperty'
                type: value.type
                range: [ [exp.variable.base.locationData.first_line, exp.variable.base.locationData.first_column], [exp.variable.base.locationData.last_line, exp.variable.base.locationData.last_column ] ]
            @exports[firstProp.name.value] =
              startLineNumber:  exp.variable.base.locationData.first_line
        else
          # case `exports = bar`
          @exports = {_default: value}
          switch value.type
            when 'class'
              @bindingTypes[value.name] = "exports"

      # case left-hand-side is anything other than `exports...`
      else
        # Handle 5 common cases:
        #
        # X     = ...
        # {X}   = ...
        # {X:Y} = ...
        # X.y   = ...
        # [X]   = ...
        switch exp.variable.base.constructor.name
          when 'Literal'
            # Something we dont care about is on the right side of the `=`.
            # This could be some garbage like an if statement.
            return unless value.range

            # case _.str = ...
            if exp.variable.properties.length > 0
              nameWithPeriods = [exp.variable.base.value].concat(_.map(exp.variable.properties, (prop) -> prop.name.value)).join(".")
              @defs[nameWithPeriods] = _.extend name: nameWithPeriods, value
            else # case X = ...
              @defs[exp.variable.base.value] = _.extend name: exp.variable.base.value, value

              # satisfies the case of npm module requires (like Grim in our tests)
              if @defs[exp.variable.base.value].type == "import"
                key = @defs[exp.variable.base.value].path || @defs[exp.variable.base.value].module
                if _.isUndefined @modules[key]
                  @modules[key] = []

                @modules[key].push { name: @defs[exp.variable.base.value].name, range: @defs[exp.variable.base.value].range }

              switch @defs[exp.variable.base.value].type
                when 'function'
                  # FIXME: Ugh. This is so fucked. We shouldnt match on name in all the files in the entire project.
                  for file in @files
                    for method in file.methods
                      if @defs[exp.variable.base.value].name == method.name
                        @defs[exp.variable.base.value].doc = method.doc.comment
                        break

          when 'Obj', 'Arr'
            for key in exp.variable.base.objects
              switch key.constructor.name
                when 'Value'
                  # case {X} = ...
                  @defs[key.base.value] = _.extend {}, value,
                    name: key.base.value
                    exportsProperty: key.base.value
                    range: [ [key.base.locationData.first_line, key.base.locationData.first_column], [key.base.locationData.last_line, key.base.locationData.last_column ] ]

                  # Store the name of the exported property to the module name
                  if @defs[key.base.value].type == "import" # I *think* this will always be true
                    if _.isUndefined @modules[@defs[key.base.value].path]
                      @modules[@defs[key.base.value].path] = []
                    @modules[@defs[key.base.value].path].push {name: @defs[key.base.value].name, range: @defs[key.base.value].range}
                when 'Assign'
                  # case {X:Y} = ...
                  @defs[key.value.base.value] = _.extend {}, value,
                    name: key.value.base.value
                    exportsProperty: key.variable.base.value
                  return no # Do not continue visiting X

                else throw new Error "BUG: Unsupported require Obj structure: #{key.constructor.name}"
          else throw new Error "BUG: Unsupported require structure: #{exp.variable.base.constructor.name}"

  visitCode: (exp) ->

  visitValue: (exp) ->

  visitCall: (exp) ->

  visitLiteral: (exp) ->

  visitObj: (exp) ->

  visitAccess: (exp) ->

  visitBlock: (exp) ->

  visitTry: (exp) ->

  visitIn: (exp) ->

  visitExistence: (exp) ->

  evalComment: (exp) ->
    type: 'comment'
    doc: exp.comment
    range: [ [exp.locationData.first_line, exp.locationData.first_column], [exp.locationData.last_line, exp.locationData.last_column ] ]

  evalClass: (exp) ->
    className = exp.variable.base.value
    superClassName = exp.parent?.base.value
    classProperties = []
    prototypeProperties = []

    classNode = _.find(@classes, (clazz) -> clazz.getFullName() == className)

    for subExp in exp.body.expressions
      switch subExp.constructor.name
        # case Prototype-level methods (this.foo = (foo) -> ...)
        when 'Assign'
          value = @eval(subExp.value)
          @defs["#{className}.#{value.name}"] = value
          classProperties.push(value)
        when 'Value'
          # case Prototype-level properties (@foo: "foo")
          for prototypeExp in subExp.base.properties
            switch prototypeExp.constructor.name
              when 'Comment'
                value = @eval(prototypeExp)
                @defs["#{value.range[0][0]}_line_comment"] = value
              else
{'fraction_non_alphanumeric': 0.12479281767955801, 'fraction_numerical': 0.0019337016574585636, 'mean_word_length': 1.8904191616766468, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 50, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7016137', 'n_tokens_mistral': 4212, 'n_tokens_neox': 4058, 'n_words': 959}