mulang 5.1.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -9,16 +9,65 @@ module Mulang
9
9
  def self.bin_path
10
10
  File.join(__dir__, '..', 'bin', 'mulang')
11
11
  end
12
- def self.analyse(analysis)
13
- Open3.popen2(bin_path, '-s') do |input, output, _thread|
14
- input.puts analysis.to_json
12
+ def self.analyse(analysis, **options)
13
+ arg, mode = Mulang::RunMode.for analysis, options
14
+ Open3.popen2(bin_path, arg) do |input, output, _thread|
15
+ input.puts mode.input(analysis).to_json
15
16
  input.close
16
- JSON.parse output.read
17
+ result = JSON.parse output.read
18
+ mode.output result
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ module RunMode
25
+ def self.for(analysis, options)
26
+ serialization = options[:serialization]
27
+ many = analysis.is_a?(Array)
28
+ if many
29
+ [encode_serialization(serialization), Mulang::RunMode::Natural]
30
+ elsif serialization
31
+ [encode_serialization(serialization), Mulang::RunMode::ForcedMany]
32
+ else
33
+ ["-s", Mulang::RunMode::Natural]
34
+ end
35
+ end
36
+
37
+ def self.encode_serialization(option)
38
+ case option
39
+ when nil then '-S'
40
+ when :bracket then '-B'
41
+ when :brace then '-C'
42
+ else raise "Unsupported serialization #{option}"
43
+ end
44
+ end
45
+
46
+ module Natural
47
+ def self.input(analysis)
48
+ analysis
49
+ end
50
+
51
+ def self.output(result)
52
+ result
53
+ end
54
+ end
55
+
56
+ module ForcedMany
57
+ def self.input(analysis)
58
+ [analysis]
59
+ end
60
+
61
+ def self.output(result)
62
+ result[0]
63
+ end
17
64
  end
18
65
  end
19
66
  end
20
67
 
68
+ require_relative './mulang/tokens'
21
69
  require_relative './mulang/inspection'
22
70
  require_relative './mulang/expectation'
23
71
  require_relative './mulang/code'
24
72
  require_relative './mulang/language'
73
+ require_relative './mulang/sexp'
@@ -1,12 +1,29 @@
1
1
  module Mulang
2
2
  class Code
3
+ attr_accessor :language, :content
3
4
  def initialize(language, content)
4
5
  @language = language
5
6
  @content = content
6
7
  end
7
8
 
8
- def ast
9
- @language.ast @content
9
+ def identifiers(**options)
10
+ @language.identifiers @content, **options
11
+ end
12
+
13
+ def ast(**options)
14
+ @language.ast @content, **options
15
+ end
16
+
17
+ def ast_analysis(**options)
18
+ @language.ast_analysis @content, **options
19
+ end
20
+
21
+ def transformed_asts(operations, **options)
22
+ @language.transformed_asts @content, operations, **options
23
+ end
24
+
25
+ def transformed_asts_analysis(operations, **options)
26
+ @language.transformed_asts_analysis @content, operations, **options
10
27
  end
11
28
 
12
29
  def sample
@@ -17,8 +34,8 @@ module Mulang
17
34
  { sample: sample, spec: spec }
18
35
  end
19
36
 
20
- def analyse(spec)
21
- Mulang.analyse analysis(spec)
37
+ def analyse(spec, **options)
38
+ Mulang.analyse analysis(spec), **options
22
39
  end
23
40
 
24
41
  def expect(binding='*', inspection)
@@ -47,12 +64,25 @@ module Mulang
47
64
  new Mulang::Language::External.new(&tool), content
48
65
  end
49
66
 
50
- def self.ast(ast)
51
- new Mulang::Language::External.new, ast
67
+ def self.analyse_many(codes, spec, **options)
68
+ run_many(codes, **options) { |it| it.analysis(spec) }
69
+ end
70
+
71
+ def self.ast_many(codes, **options)
72
+ run_many(codes, key: 'outputAst', **options) { |it| it.ast_analysis(**options) }
73
+ end
74
+
75
+ def self.transformed_asts_many(codes, operations, **options)
76
+ run_many(codes, key: 'transformedAsts', **options) { |it| it.transformed_asts_analysis(operations, **options) }
52
77
  end
53
78
 
54
79
  private
55
80
 
81
+ def self.run_many(codes, key: nil, **options)
82
+ result = Mulang.analyse(codes.map { |it| yield it }, **options)
83
+ key ? result.map { |it| it[key] } : result
84
+ end
85
+
56
86
  def expectation_results_for(result)
57
87
  raise result['reason'] if result['tag'] == 'AnalysisFailed'
58
88
  result['expectationResults']
@@ -1,12 +1,63 @@
1
1
  module Mulang::Expectation
2
- SMELLS = %w(DiscardsExceptions DoesConsolePrint DoesNilTest DoesNullTest DoesTypeTest
3
- HasAssignmentReturn HasCodeDuplication HasEmptyIfBranches HasRedundantRepeat HasLongParameterList
4
- HasMisspelledBindings HasMisspelledIdentifiers
5
- HasRedundantBooleanComparison HasRedundantGuards HasRedundantIf
6
- HasRedundantLambda HasRedundantLocalVariableReturn HasRedundantParameter
7
- HasRedundantReduction HasTooManyMethods HasTooShortBindings HasTooShortIdentifiers HasUnreachableCode
8
- HasWrongCaseBinding HasWrongCaseIdentifiers IsLongCode OverridesEqualOrHashButNotBoth
9
- ReturnsNil ReturnsNull UsesCut UsesFail UsesUnificationOperator)
2
+ LOGIC_SMELLS = %w(
3
+ HasRedundantReduction
4
+ UsesCut
5
+ UsesFail
6
+ UsesUnificationOperator
7
+ )
8
+
9
+ FUNCTIONAL_SMELLS = %w(
10
+ HasRedundantGuards
11
+ HasRedundantParameter
12
+ ShouldUseOtherwise
13
+ )
14
+
15
+ OBJECT_ORIENTED_SMELLS = %w(
16
+ DoesNilTest
17
+ DoesNullTest
18
+ HasTooManyMethods
19
+ OverridesEqualOrHashButNotBoth
20
+ ReturnsNil
21
+ ReturnsNull
22
+ )
23
+
24
+ IMPERATIVE_SMELLS = %w(
25
+ HasAssignmentCondition
26
+ HasAssignmentReturn
27
+ HasEmptyRepeat
28
+ HasRedundantLocalVariableReturn
29
+ HasRedundantRepeat
30
+ )
31
+
32
+ EXPRESSIVENESS_SMELLS = %w(
33
+ HasMisspelledBindings
34
+ HasMisspelledIdentifiers
35
+ HasTooShortIdentifiers
36
+ HasWrongCaseBinding
37
+ HasWrongCaseIdentifiers
38
+ )
39
+
40
+ GENERIC_SMELLS = %w(
41
+ DiscardsExceptions
42
+ DoesConsolePrint
43
+ DoesTypeTest
44
+ HasCodeDuplication
45
+ HasDeclarationTypos
46
+ HasEmptyIfBranches
47
+ HasEqualIfBranches
48
+ HasLongParameterList
49
+ HasRedundantBooleanComparison
50
+ HasRedundantIf
51
+ HasRedundantLambda
52
+ HasTooShortBindings
53
+ HasUnreachableCode
54
+ HasUsageTypos
55
+ IsLongCode
56
+ ShouldInvertIfCondition
57
+ ShouldUseStrictComparators
58
+ )
59
+
60
+ SMELLS = GENERIC_SMELLS + EXPRESSIVENESS_SMELLS + IMPERATIVE_SMELLS + OBJECT_ORIENTED_SMELLS + FUNCTIONAL_SMELLS + LOGIC_SMELLS
10
61
 
11
62
  def self.guess_type(expectation)
12
63
  if expectation[:binding] == '<<custom>>'
@@ -19,7 +70,7 @@ module Mulang::Expectation
19
70
  end
20
71
 
21
72
  def self.has_smell?(smell)
22
- SMELLS.include? smell
73
+ SMELLS.include? smell.split(':').first
23
74
  end
24
75
 
25
76
  def self.parse(expectation)
@@ -9,6 +9,8 @@ class Mulang::Expectation::Custom
9
9
  name
10
10
  end
11
11
 
12
+ alias translate! translate
13
+
12
14
  def to_h
13
15
  {binding: '<<custom>>', inspection: name}
14
16
  end
@@ -1,49 +1,35 @@
1
1
  module Mulang::Expectation::I18n
2
2
  class << self
3
- DEFAULT_KEYWORDS = {
4
- keyword_fail: :fail,
5
- keyword_false: :false,
6
- keyword_findall: :findall,
7
- keyword_for: :for,
8
- keyword_forall: :forall,
9
- keyword_foreach: :foreach,
10
- keyword_if: :if,
11
- keyword_is: :is,
12
- keyword_not: :not,
13
- keyword_null: :null,
14
- keyword_repeat: :repeat,
15
- keyword_switch: :switch,
16
- keyword_true: :true,
17
- keyword_while: :while,
18
- keyword_yield: :yield
19
- }
3
+ def translate(e, tokens = nil)
4
+ translate!(e, tokens)
5
+ rescue
6
+ '<unknown expectation>'
7
+ end
20
8
 
21
- def translate(e, keywords = nil)
9
+ def translate!(e, tokens = nil)
22
10
  e = e.as_v2
23
11
  key = key_for e.binding, e.inspection
24
- ::I18n.t key, translation_params(e, keywords)
25
- rescue
26
- '<unknown expectation>'
12
+ ::I18n.t key, translation_params(e, tokens)
27
13
  end
28
14
 
29
15
  alias t translate
30
16
 
31
17
  private
32
18
 
33
- def translation_params(e, keywords)
34
- with_keywords keywords,
19
+ def translation_params(e, tokens)
20
+ with_tokens tokens,
35
21
  binding: t_binding(e.binding),
36
22
  target: t_target(e.inspection),
37
23
  must: t_must(e.inspection),
38
- matching: t_matching(keywords, e.inspection)
24
+ matching: t_matching(tokens, e.inspection)
39
25
  end
40
26
 
41
27
  def key_for(binding, inspection)
42
- "mulang.inspection.#{inspection.type}#{inspection.target ? inspection.target.i18n_suffix : nil}"
28
+ "#{inspection.i18n_namespace}.#{inspection.type}#{inspection.target ? inspection.target.i18n_suffix : nil}"
43
29
  end
44
30
 
45
31
  def t_binding(binding)
46
- binding == '*' ? ::I18n.t("mulang.expectation.solution") : "<strong>#{Mulang::Inspection.parse_binding_name binding}</strong>"
32
+ binding == '*' ? ::I18n.t("mulang.expectation.solution") : "<code>#{Mulang::Inspection.parse_binding_name binding}</code>"
47
33
  end
48
34
 
49
35
  def t_must(parsed)
@@ -51,15 +37,23 @@ module Mulang::Expectation::I18n
51
37
  end
52
38
 
53
39
  def t_target(parsed)
54
- "<strong>#{parsed.target.value}</strong>" if parsed.target
40
+ "<code>#{parsed.target.value}</code>" if parsed.target
55
41
  end
56
42
 
57
- def t_matching(keywords, parsed)
58
- ::I18n.t("mulang.expectation.#{parsed.matcher.type}", with_keywords(keywords, value: parsed.matcher.value)) if parsed.matcher
43
+ def t_matching(tokens, parsed)
44
+ ::I18n.t("mulang.expectation.#{parsed.matcher.type}", with_tokens(tokens, value: parsed.matcher.value)) if parsed.matcher
59
45
  end
60
46
 
61
- def with_keywords(keywords, params)
62
- params.merge(DEFAULT_KEYWORDS).merge(keywords || {})
47
+ def with_tokens(tokens, params)
48
+ hash = if tokens.nil?
49
+ {}
50
+ elsif tokens.is_a?(Hash)
51
+ tokens
52
+ else
53
+ params.merge(Mulang::Tokens::TOKENS.indifferent_get(tokens))
54
+ end
55
+
56
+ params.merge(Mulang::Tokens::DEFAULT_TOKENS.merge(hash))
63
57
  end
64
58
  end
65
59
  end
@@ -11,8 +11,12 @@ class Mulang::Expectation::Standard
11
11
  raise "Wrong inspection #{to_h}" unless inspection?
12
12
  end
13
13
 
14
- def translate(keywords = nil)
15
- Mulang::Expectation::I18n.translate self, keywords
14
+ def translate(tokens = nil)
15
+ Mulang::Expectation::I18n.translate self, tokens
16
+ end
17
+
18
+ def translate!(tokens = nil)
19
+ Mulang::Expectation::I18n.translate! self, tokens
16
20
  end
17
21
 
18
22
  def to_h
@@ -68,7 +72,7 @@ class Mulang::Expectation::V0 < Mulang::Expectation::Standard
68
72
  end
69
73
 
70
74
  def as_v2_use
71
- Mulang::Expectation::V2.new binding, new_inspection(inspection.type.gsub('Has', 'Uses'), nil)
75
+ Mulang::Expectation::V2.new binding, new_inspection(inspection.type.gsub(/^Has/, 'Uses'), nil)
72
76
  end
73
77
 
74
78
  def as_v2_declare(simple_type)
@@ -20,11 +20,12 @@ module Mulang
20
20
  attr_accessor :type, :target, :matcher, :negated
21
21
  alias negated? negated
22
22
 
23
- def initialize(type, target, negated: false, matcher: nil)
23
+ def initialize(type, target, negated: false, matcher: nil, i18n_namespace: nil)
24
24
  @type = type
25
25
  @target = target
26
26
  @negated = negated
27
27
  @matcher = matcher
28
+ @i18n_namespace = i18n_namespace
28
29
  end
29
30
 
30
31
  def to_s
@@ -43,6 +44,10 @@ module Mulang
43
44
  matcher ? ":#{matcher.to_s}" : nil
44
45
  end
45
46
 
47
+ def i18n_namespace
48
+ @i18n_namespace || 'mulang.inspection'
49
+ end
50
+
46
51
  def self.parse_binding_name(binding_s)
47
52
  if binding_s.start_with? 'Intransitive:'
48
53
  binding_s[13..-1]
@@ -52,14 +57,34 @@ module Mulang
52
57
  end
53
58
 
54
59
  def self.parse(inspection_s)
60
+ parse_extension(inspection_s).try { |it| return it }
55
61
  match = REGEXP.match inspection_s
56
62
  raise "Invalid inspection #{inspection_s}" unless match
57
- Inspection.new(
63
+ Mulang::Inspection.new(
58
64
  match['type'],
59
65
  Mulang::Inspection::Target.parse(match['target']),
60
66
  matcher: Mulang::Inspection::Matcher.parse(match['matcher'], match['value']),
61
67
  negated: match['negation'].present?)
62
68
  end
69
+
70
+ def self.parse_extension(inspection_s)
71
+ extensions.each do |extension|
72
+ extension.parse(inspection_s).try { |it| return it }
73
+ end
74
+ nil
75
+ end
76
+
77
+ def self.extensions
78
+ @extensions ||= []
79
+ end
80
+
81
+ def self.register_extension!(extension)
82
+ extensions << extension
83
+ end
84
+
85
+ def self.unregister_extension!(extension)
86
+ extensions.delete extension
87
+ end
63
88
  end
64
89
  end
65
90
 
@@ -1,19 +1,52 @@
1
1
  module Mulang::Language
2
- class Native
3
- def initialize(language)
4
- @language = language
2
+ class Base
3
+ def identifiers(content, **options)
4
+ Mulang.analyse(identifiers_analysis(content, **options), **options)['outputIdentifiers'] rescue nil
5
5
  end
6
6
 
7
- def ast(content)
8
- Mulang.analyse(ast_analysis(content))['intermediateLanguage'] rescue nil
7
+ def identifiers_analysis(content, **options)
8
+ base_analysis content, {includeOutputIdentifiers: true}, **options
9
9
  end
10
10
 
11
- def ast_analysis(content)
11
+ def transformed_asts(content, operations, **options)
12
+ Mulang.analyse(transformed_asts_analysis(content, operations, **options), **options)['transformedAsts'] rescue nil
13
+ end
14
+
15
+ def transformed_asts_analysis(content, operations, **options)
16
+ base_analysis content, {transformationSpecs: operations}, **options
17
+ end
18
+
19
+ def normalization_options(**options)
20
+ options.except(:serialization).presence
21
+ end
22
+
23
+ private
24
+
25
+ def base_analysis(content, spec, **options)
12
26
  {
13
- sample: { tag: 'CodeSample', language: @language, content: content },
14
- spec: { expectations: [], smellsSet: { tag: 'NoSmells' }, includeIntermediateLanguage: true }
27
+ sample: sample(content),
28
+ spec: {
29
+ expectations: [],
30
+ smellsSet: { tag: 'NoSmells' },
31
+ includeOutputAst: false,
32
+ normalizationOptions: normalization_options(options)
33
+ }.merge(spec).compact
15
34
  }
16
35
  end
36
+ end
37
+
38
+ class Native < Base
39
+ def initialize(language)
40
+ @language = language
41
+ end
42
+
43
+ def ast(content, **options)
44
+ Mulang.analyse(ast_analysis(content, **options), **options)['outputAst'] rescue nil
45
+ end
46
+
47
+ def ast_analysis(content, **options)
48
+ base_analysis content, {includeOutputAst: true}, **options
49
+ end
17
50
 
18
51
  def sample(content)
19
52
  {
@@ -24,12 +57,12 @@ module Mulang::Language
24
57
  end
25
58
  end
26
59
 
27
- class External
60
+ class External < Base
28
61
  def initialize(&tool)
29
62
  @tool = block_given? ? tool : proc { |it| it }
30
63
  end
31
64
 
32
- def ast(content)
65
+ def ast(content, **args)
33
66
  @tool.call(content) rescue nil
34
67
  end
35
68