terrestrial-cli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/bin/terrestrial +44 -0
- data/circle.yml +11 -0
- data/lib/terrestrial/cli/android_xml_formatter.rb +43 -0
- data/lib/terrestrial/cli/android_xml_parser.rb +49 -0
- data/lib/terrestrial/cli/bootstrapper.rb +184 -0
- data/lib/terrestrial/cli/command.rb +20 -0
- data/lib/terrestrial/cli/detects_project_type.rb +16 -0
- data/lib/terrestrial/cli/dot_strings_formatter.rb +53 -0
- data/lib/terrestrial/cli/dot_strings_parser.rb +139 -0
- data/lib/terrestrial/cli/editor/android_xml.rb +64 -0
- data/lib/terrestrial/cli/editor/base_editor.rb +36 -0
- data/lib/terrestrial/cli/editor/objc.rb +66 -0
- data/lib/terrestrial/cli/editor/printer.rb +47 -0
- data/lib/terrestrial/cli/editor/storyboard.rb +98 -0
- data/lib/terrestrial/cli/editor/swift.rb +92 -0
- data/lib/terrestrial/cli/editor.rb +42 -0
- data/lib/terrestrial/cli/engine_mapper.rb +30 -0
- data/lib/terrestrial/cli/entry_collection_differ.rb +22 -0
- data/lib/terrestrial/cli/file_finder.rb +65 -0
- data/lib/terrestrial/cli/file_picker.rb +58 -0
- data/lib/terrestrial/cli/flight/ios_workflow.rb +81 -0
- data/lib/terrestrial/cli/flight/table_workflow.rb +77 -0
- data/lib/terrestrial/cli/flight.rb +93 -0
- data/lib/terrestrial/cli/ignite.rb +73 -0
- data/lib/terrestrial/cli/init.rb +133 -0
- data/lib/terrestrial/cli/mixpanel_client.rb +56 -0
- data/lib/terrestrial/cli/parser/android_xml.rb +82 -0
- data/lib/terrestrial/cli/parser/base_parser.rb +42 -0
- data/lib/terrestrial/cli/parser/objc.rb +127 -0
- data/lib/terrestrial/cli/parser/storyboard.rb +166 -0
- data/lib/terrestrial/cli/parser/string_analyser.rb +115 -0
- data/lib/terrestrial/cli/parser/swift.rb +102 -0
- data/lib/terrestrial/cli/parser.rb +25 -0
- data/lib/terrestrial/cli/photoshoot.rb +65 -0
- data/lib/terrestrial/cli/pull.rb +110 -0
- data/lib/terrestrial/cli/push.rb +40 -0
- data/lib/terrestrial/cli/scan.rb +72 -0
- data/lib/terrestrial/cli/string_registry.rb +30 -0
- data/lib/terrestrial/cli/terminal_ui.rb +25 -0
- data/lib/terrestrial/cli/variable_normalizer.rb +34 -0
- data/lib/terrestrial/cli/version.rb +5 -0
- data/lib/terrestrial/cli.rb +82 -0
- data/lib/terrestrial/config.rb +99 -0
- data/lib/terrestrial/creates_terrestrial_yml.rb +9 -0
- data/lib/terrestrial/web/response.rb +17 -0
- data/lib/terrestrial/web.rb +78 -0
- data/lib/terrestrial/yaml_helper.rb +48 -0
- data/lib/terrestrial.rb +7 -0
- data/terrestrial-cli.gemspec +29 -0
- metadata +188 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Parser
|
4
|
+
class AndroidXML < BaseParser
|
5
|
+
LANGUAGE = :android_xml
|
6
|
+
|
7
|
+
def self.find_strings(file)
|
8
|
+
self.new(file).find_strings
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.find_api_calls(file)
|
12
|
+
self.new(file).find_api_calls
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(file)
|
16
|
+
@path = file
|
17
|
+
@file = File.new(file)
|
18
|
+
@document = REXML::Document.new(@file)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_strings
|
22
|
+
result = []
|
23
|
+
REXML::XPath.each(@document, "//resources/string") do |node|
|
24
|
+
result << build_new_string_entry(node)
|
25
|
+
end
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_api_calls
|
30
|
+
result = []
|
31
|
+
REXML::XPath.each(@document, "//resources/string[@terrestrial=\"true\"]") do |node|
|
32
|
+
result << build_registry_entry_hash(node)
|
33
|
+
end
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_new_string_entry(node)
|
38
|
+
Hash.new.tap do |entry|
|
39
|
+
entry["language"] = LANGUAGE
|
40
|
+
entry["file"] = @path
|
41
|
+
entry["string"] = get_string_from_node(node)
|
42
|
+
entry["type"] = "android-strings-xml"
|
43
|
+
entry["line_number"] = nil
|
44
|
+
# entry.variables = get_variables_from_string(entry.string)
|
45
|
+
entry["identifier"] = node.attributes["name"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_registry_entry_hash(node)
|
50
|
+
Hash.new.tap do |entry|
|
51
|
+
entry["string"] = get_string_from_node(node)
|
52
|
+
entry["context"] = node.attributes["context"] || ""
|
53
|
+
entry["file"] = @path
|
54
|
+
entry["line_number"] = nil
|
55
|
+
entry["type"] = "android-strings-xml"
|
56
|
+
entry["id"] = node.attributes["name"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_string_from_node(node)
|
61
|
+
# Why could the text be nil?
|
62
|
+
# - If it contains valid XML!
|
63
|
+
#
|
64
|
+
# We assume anything inside the string tag is actually
|
65
|
+
# what should be shown in the UI, so we just parse it
|
66
|
+
# as a string if we realise that the parser thinks it
|
67
|
+
# is XML.
|
68
|
+
|
69
|
+
if !node.get_text.nil?
|
70
|
+
node.get_text.value
|
71
|
+
else
|
72
|
+
node.children.first.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_variables_from_string(string)
|
77
|
+
string.scan(/(\%\d\$[dsf])/).map {|match| match[0] }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Parser
|
4
|
+
class BaseParser
|
5
|
+
|
6
|
+
# Interface for finding locations in files where the Terrestrial
|
7
|
+
# API will be accessing strings
|
8
|
+
#
|
9
|
+
# file - path to source file
|
10
|
+
#
|
11
|
+
# Expected to return an array of
|
12
|
+
# Bootstrapper::NewStringEntry
|
13
|
+
# objects
|
14
|
+
def self.find_api_calls(file)
|
15
|
+
raise "Not implemented"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Interface for finding strings in a source file
|
19
|
+
#
|
20
|
+
# file - path to source file
|
21
|
+
#
|
22
|
+
# Expected to return an array of
|
23
|
+
# hashes TODO: make return an object
|
24
|
+
def self.find_string(file)
|
25
|
+
raise "Not implemented"
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find_nslocalizedstrings(file)
|
29
|
+
raise "Not implemented"
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
def self.scan_lines(path)
|
35
|
+
File.readlines(file).each_with_index do |line, index|
|
36
|
+
yield line, index
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Parser
|
4
|
+
class ObjC < BaseParser
|
5
|
+
LANGUAGE = :objc
|
6
|
+
|
7
|
+
STRING_REGEX = /@"(.*?)"/
|
8
|
+
NSLOCALIZEDSTRING_REGEX = /NSLocalizedString\(.*@"(.*)".*\)/
|
9
|
+
DOT_TRANSLATED_REGEX = /@"([^"]*)".translated\W/
|
10
|
+
TRANSLATED_WITH_CONTEXT_REGEX = /@"([^"]*)"\stranslatedWithContext:/
|
11
|
+
|
12
|
+
def self.find_strings(file)
|
13
|
+
results = []
|
14
|
+
if is_view_controller?(file)
|
15
|
+
File.readlines(file, :encoding => "UTF-8").each_with_index do |line, index|
|
16
|
+
line.encode!('UTF-16', :undef => :replace, :invalid => :replace, :replace => "")
|
17
|
+
line.encode!('UTF-8')
|
18
|
+
results.concat(analyse_line_for_strings(line,index, file))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
results
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.analyse_line_for_strings(line, index, file_path)
|
25
|
+
results = []
|
26
|
+
line.scan(STRING_REGEX).each do |match|
|
27
|
+
unless looks_suspicious(line)
|
28
|
+
results.push(Hash.new.tap do |entry|
|
29
|
+
entry["language"] = LANGUAGE
|
30
|
+
entry["file"] = file_path
|
31
|
+
entry["line_number"] = index + 1
|
32
|
+
entry["string"] = match[0]
|
33
|
+
entry["type"] = guess_type(line)
|
34
|
+
# entry.variables = get_variable_names(line) if entry.type == "stringWithFormat"
|
35
|
+
|
36
|
+
end)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
results
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.find_api_calls(file)
|
43
|
+
results = []
|
44
|
+
File.readlines(file).each_with_index do |line, index|
|
45
|
+
results.concat(analyse_line_for_dot_translated(line, index, file))
|
46
|
+
results.concat(analyse_line_for_translatedWithContext(line, index, file))
|
47
|
+
end
|
48
|
+
results
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.analyse_line_for_translatedWithContext(line, index, file_path)
|
52
|
+
results = []
|
53
|
+
line.scan(TRANSLATED_WITH_CONTEXT_REGEX).each do |match|
|
54
|
+
results.push(Hash.new.tap do |h|
|
55
|
+
h["file"] = file_path
|
56
|
+
h["line_number"] = index + 1
|
57
|
+
h["string"] = match[0]
|
58
|
+
h["type"] = "translatedWithContext"
|
59
|
+
h["context"] = get_context(line, h["string"])
|
60
|
+
end)
|
61
|
+
end
|
62
|
+
results
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.analyse_line_for_dot_translated(line, index, file_path)
|
66
|
+
results = []
|
67
|
+
line.scan(DOT_TRANSLATED_REGEX).each do |match|
|
68
|
+
results.push(Hash.new.tap do |h|
|
69
|
+
h["file"] = file_path
|
70
|
+
h["line_number"] = index + 1
|
71
|
+
h["string"] = match[0]
|
72
|
+
h["type"] = ".translated"
|
73
|
+
h["context"] = ""
|
74
|
+
end)
|
75
|
+
end
|
76
|
+
results
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.get_context(line, match)
|
80
|
+
line.match(/"#{match}" translatedWithContext:\s?@"([^"]*)"/)[1]
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.guess_type(line)
|
84
|
+
if line.include? "stringWithFormat"
|
85
|
+
"stringWithFormat"
|
86
|
+
else
|
87
|
+
"unknown"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.get_variable_names(line)
|
92
|
+
line
|
93
|
+
.scan(/stringWithFormat:\s?@"[^"]+",\s?(.*?)\][^\s*,]/)
|
94
|
+
.first.first # Array of arrays Yo.
|
95
|
+
.split(",")
|
96
|
+
.map {|var| var.gsub(/\s+/, "")}
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.looks_suspicious(line)
|
100
|
+
without_strings = line.gsub(STRING_REGEX, "")
|
101
|
+
without_strings.include?("_LOG") ||
|
102
|
+
without_strings.include?("DLog") ||
|
103
|
+
without_strings.include?("NSLog") ||
|
104
|
+
without_strings.include?("NSAssert") ||
|
105
|
+
without_strings.downcase.include?("uistoryboard") ||
|
106
|
+
without_strings.downcase.include?("instantiateviewcontrollerwithidentifier") ||
|
107
|
+
without_strings.downcase.include?("uiimage") ||
|
108
|
+
without_strings.downcase.include?("nsentitydescription") ||
|
109
|
+
without_strings.downcase.include?("nspredicate") ||
|
110
|
+
without_strings.downcase.include?("dateformat") ||
|
111
|
+
without_strings.downcase.include?("datefromstring") ||
|
112
|
+
without_strings.downcase.include?("==") ||
|
113
|
+
without_strings.downcase.include?("isequaltostring") ||
|
114
|
+
without_strings.downcase.include?("valueforkey") ||
|
115
|
+
without_strings.downcase.include?("cellidentifier") ||
|
116
|
+
without_strings.downcase.include?("uifont") ||
|
117
|
+
without_strings.downcase.include?("static ") ||
|
118
|
+
without_strings.downcase.include?("print(")
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.is_view_controller?(file)
|
122
|
+
!file.match(/ViewController/).nil?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
module Terrestrial
|
4
|
+
module Cli
|
5
|
+
module Parser
|
6
|
+
class Storyboard
|
7
|
+
LANGUAGE = :ios_storyboard
|
8
|
+
attr_reader :result
|
9
|
+
|
10
|
+
include REXML
|
11
|
+
QUERIES = {
|
12
|
+
"storyboard-label" => "//label",
|
13
|
+
"storyboard-text-field" => "//textField",
|
14
|
+
"storyboard-button" => "//button/state",
|
15
|
+
"storyboard-bar-button-item" => "//barButtonItem",
|
16
|
+
"storyboard-navbar-item" => "//navigationItem",
|
17
|
+
"storyboard-text-view" => "//textView"
|
18
|
+
}
|
19
|
+
|
20
|
+
TEXT_ATTRIBUTE = {
|
21
|
+
"storyboard-label" => "text",
|
22
|
+
"storyboard-text-field" => "placeholder",
|
23
|
+
"storyboard-button" => "title",
|
24
|
+
"storyboard-bar-button-item" => "title",
|
25
|
+
"storyboard-navbar-item" => "title",
|
26
|
+
"storyboard-text-view" => "text"
|
27
|
+
}
|
28
|
+
|
29
|
+
TYPES = QUERIES.keys
|
30
|
+
|
31
|
+
def initialize(file)
|
32
|
+
@path = file
|
33
|
+
@file = File.new(file)
|
34
|
+
@document = Document.new(@file)
|
35
|
+
@result = []
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find_strings(file)
|
39
|
+
self.new(file).find_strings
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.find_api_calls(file)
|
43
|
+
self.new(file).find_api_calls
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_api_calls
|
47
|
+
labels = []
|
48
|
+
XPath.each(@document, api_calls_query) do |node|
|
49
|
+
type = type_for(node.name)
|
50
|
+
string = get_string(node)
|
51
|
+
context = get_context(node)
|
52
|
+
|
53
|
+
labels << build_registry_entry_hash(string, context, type)
|
54
|
+
end
|
55
|
+
@result = labels
|
56
|
+
@result
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_strings
|
60
|
+
TYPES.each do |type|
|
61
|
+
@result.concat(find_entries_for_type(type))
|
62
|
+
end
|
63
|
+
@result
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
def type_for(name)
|
69
|
+
{
|
70
|
+
"label" => "storyboard-label",
|
71
|
+
"textField" => "storyboard-text-field",
|
72
|
+
"button" => "storyboard-button",
|
73
|
+
"barButtonItem" => "storyboard-bar-button-item",
|
74
|
+
"navigationItem" => "storyboard-navbar-item",
|
75
|
+
"textView" => "storyboard-text-view",
|
76
|
+
}[name]
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_entries_for_type(type)
|
80
|
+
labels = []
|
81
|
+
XPath.each(@document, QUERIES[type]) do |node|
|
82
|
+
labels << new_entry(node.attributes[TEXT_ATTRIBUTE[type]],
|
83
|
+
type: type,
|
84
|
+
id: get_id(node))
|
85
|
+
end
|
86
|
+
labels
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_id(node)
|
90
|
+
# Why? Because the button's text is not in the
|
91
|
+
# button, but in a child element, that doesn't have
|
92
|
+
# an ID. So for most situations you'll just pick the
|
93
|
+
# ID off the element, but sometimes we'll have to
|
94
|
+
# traverse back up to get the ID. If that element
|
95
|
+
# doesn't have an ID, it's a new situation and
|
96
|
+
# we should get an exception down the line.
|
97
|
+
|
98
|
+
target = node
|
99
|
+
if target.attributes["id"].nil?
|
100
|
+
target.parent.attributes["id"]
|
101
|
+
else
|
102
|
+
target.attributes["id"]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_context(node)
|
107
|
+
# //textView/userDefinedRuntimeAttributes/userDefinedRuntimeAttribute[@keyPath="contextInfo"]
|
108
|
+
context = ""
|
109
|
+
attributes = node.elements.select {|e| e.name == "userDefinedRuntimeAttributes"}.first
|
110
|
+
attributes.each_element_with_attribute("keyPath", "contextInfo") do |e|
|
111
|
+
context = e.attributes["value"]
|
112
|
+
end
|
113
|
+
context
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_string(node)
|
117
|
+
type = type_for(node.name)
|
118
|
+
if type == "storyboard-button"
|
119
|
+
node.elements.select {|e| e.name == "state"}.first.attributes[TEXT_ATTRIBUTE[type]]
|
120
|
+
else
|
121
|
+
node.attributes[TEXT_ATTRIBUTE[type]]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def api_calls_query
|
126
|
+
# Finds all the attributes that say that an element is
|
127
|
+
# translated by Terrestrial, and we then traverse
|
128
|
+
# two parents up:
|
129
|
+
#
|
130
|
+
# <*targetElement*>
|
131
|
+
# <userDefinedRuntimeAttributes>
|
132
|
+
# <userDefinedRuntimeAttribute ... /> <- these are what we find
|
133
|
+
|
134
|
+
'//userDefinedRuntimeAttribute[@type="boolean" and @value="YES"]/../..'
|
135
|
+
end
|
136
|
+
|
137
|
+
def new_entry(string, opts)
|
138
|
+
defaults = { type: "storyboard" }
|
139
|
+
values = defaults.merge(opts)
|
140
|
+
|
141
|
+
Hash.new.tap do |entry|
|
142
|
+
entry["file"] = @path
|
143
|
+
entry["language"] = LANGUAGE
|
144
|
+
entry["string"] = string.to_s
|
145
|
+
entry["type"] = values.fetch(:type)
|
146
|
+
entry["line_number"] = nil
|
147
|
+
entry["metadata"] = {
|
148
|
+
"storyboard_element_id" => values.fetch(:id)
|
149
|
+
}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def build_registry_entry_hash(string, context, type)
|
154
|
+
Hash.new.tap do |entry|
|
155
|
+
entry["string"] = string.to_s
|
156
|
+
entry["language"] = LANGUAGE
|
157
|
+
entry["context"] = context || ""
|
158
|
+
entry["file"] = @path
|
159
|
+
entry["line_number"] = nil
|
160
|
+
entry["type"] = type
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Parser
|
4
|
+
class StringAnalyser
|
5
|
+
|
6
|
+
def self.is_string_for_humans?(string, language, variables = [])
|
7
|
+
self.new(string, language, variables).decide
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(string, language, variables = [])
|
11
|
+
@string = string
|
12
|
+
@variables = variables || [] # TODO: Find out what was passing in variables as nil instead of empty array
|
13
|
+
@language = language
|
14
|
+
end
|
15
|
+
|
16
|
+
def decide
|
17
|
+
if @variables.any?
|
18
|
+
looks_like_string_without_variables?
|
19
|
+
else
|
20
|
+
if has_camel_case_words? || looks_like_sql? || is_number? || has_snake_case_words?
|
21
|
+
false
|
22
|
+
elsif number_of_words > 1 && percentage_of_none_alphanumeric < 0.15
|
23
|
+
true
|
24
|
+
elsif number_of_words == 1 && is_capitalised? && percentage_of_none_alphanumeric < 0.1
|
25
|
+
true
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def is_number?
|
33
|
+
!(@string =~ /\A[-+]?[0-9]*\.?[0-9]+\Z/).nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def number_of_words
|
37
|
+
@string.split(" ").length
|
38
|
+
end
|
39
|
+
|
40
|
+
def percentage_of_none_alphanumeric
|
41
|
+
total = @string.split("").length.to_f
|
42
|
+
non_alphanumeric = @string
|
43
|
+
.split("")
|
44
|
+
.select {|c| /[0-9a-zA-Z i\s]/.match(c).nil? }
|
45
|
+
.length
|
46
|
+
.to_f
|
47
|
+
|
48
|
+
non_alphanumeric / total
|
49
|
+
end
|
50
|
+
|
51
|
+
def looks_like_sql?
|
52
|
+
# Handle SQL with clever regex
|
53
|
+
!@string.match(/(ALTER|CREATE|DROP) TABLE/).nil? ||
|
54
|
+
!@string.match(/(DELETE|SELECT|INSERT|UPDATE).+(FROM|INTO|SET)/).nil? ||
|
55
|
+
!@string.match(/(delete|select|insert|update).+(from|into|set)/).nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
def has_weird_characters?
|
59
|
+
(@string.split("") & ["<",">","\\", "/","*"]).length > 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def has_punctuation?
|
63
|
+
(@string.split("") & [".",",","=","&"]).length > 0
|
64
|
+
end
|
65
|
+
|
66
|
+
def has_camel_case_words?
|
67
|
+
@string.split(" ")
|
68
|
+
.select {|word| !word.match(/([a-zA-Z][a-z]+[A-Z][a-zA-Z]+)/).nil? }
|
69
|
+
.any?
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_camel_case_words?
|
73
|
+
@string.split(" ")
|
74
|
+
.select {|word| !word.match(/([a-zA-Z][a-z]+[A-Z][a-zA-Z]+)/).nil? }
|
75
|
+
.any?
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_snake_case_words?
|
79
|
+
@string.split(" ")
|
80
|
+
.select {|word| !word.match(/\b\w*(_\w*)+\b/).nil? }
|
81
|
+
.any?
|
82
|
+
end
|
83
|
+
|
84
|
+
def is_capitalised?
|
85
|
+
@string == @string.capitalize
|
86
|
+
end
|
87
|
+
|
88
|
+
def looks_like_string_without_variables?
|
89
|
+
# Strip away the variables, remove extra whitespace,
|
90
|
+
# and feed that string back into the system to see
|
91
|
+
# if it now looks human readable or not.
|
92
|
+
|
93
|
+
if @language == ObjC::LANGUAGE
|
94
|
+
new_string = @string
|
95
|
+
.gsub(/(%@)|(%d)/, "")
|
96
|
+
.gsub(/\s\s/, " ")
|
97
|
+
elsif @language == Swift::LANGUAGE
|
98
|
+
new_string = @string
|
99
|
+
.gsub(/\\\(.*\)/, "")
|
100
|
+
.gsub(/\s\s/, " ")
|
101
|
+
elsif @language == AndroidXML::LANGUAGE
|
102
|
+
new_string = @string
|
103
|
+
|
104
|
+
@variables.each do |v|
|
105
|
+
new_string = new_string.gsub(v, "")
|
106
|
+
new_string = new_string.gsub(/\s\s/, " ")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
self.class.new(new_string, @language).decide
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Terrestrial
|
2
|
+
module Cli
|
3
|
+
module Parser
|
4
|
+
class Swift
|
5
|
+
LANGUAGE = :swift
|
6
|
+
|
7
|
+
NSLOCALIZEDSTRING_REGEX = /NSLocalizedString\(.*"(.*)".*\)/
|
8
|
+
STRING_REGEX = /"([^"]*)"/
|
9
|
+
DOT_TRANSLATED_REGEX = /"([^"]*)".translated\W/
|
10
|
+
TRANSLATED_WITH_CONTEXT_REGEX = /"([^"]*)".translatedWithContext/
|
11
|
+
VARIABLE_REGEX = /\\\((.*?)\)/
|
12
|
+
|
13
|
+
def self.find_strings(file)
|
14
|
+
results = []
|
15
|
+
if is_view_controller?(file)
|
16
|
+
File.readlines(file).each_with_index do |line, index|
|
17
|
+
results.concat analyse_line_for_strings(line, index, file)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
results
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.analyse_line_for_strings(line, index, file_path)
|
24
|
+
results = []
|
25
|
+
line.scan(STRING_REGEX).each do |match|
|
26
|
+
unless looks_suspicious(line)
|
27
|
+
results.push(Hash.new.tap do |entry|
|
28
|
+
entry["language"] = LANGUAGE
|
29
|
+
entry["file"] = file_path
|
30
|
+
entry["line_number"] = index + 1
|
31
|
+
entry["string"] = match[0]
|
32
|
+
entry["type"] = find_variables(match[0]).any? ? "stringWithFormat" : "unknown"
|
33
|
+
# entry.variables = find_variables(match[0])
|
34
|
+
end)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
results
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.find_api_calls(file)
|
41
|
+
results = []
|
42
|
+
File.readlines(file).each_with_index do |line, index|
|
43
|
+
line.scan(DOT_TRANSLATED_REGEX).each do |match|
|
44
|
+
results.push(Hash.new.tap do |h|
|
45
|
+
h["file"] = file
|
46
|
+
h["line_number"] = index + 1
|
47
|
+
h["string"] = match[0]
|
48
|
+
h["type"] = ".translated"
|
49
|
+
h["context"] = ""
|
50
|
+
end)
|
51
|
+
end
|
52
|
+
|
53
|
+
line.scan(TRANSLATED_WITH_CONTEXT_REGEX).each do |match|
|
54
|
+
results.push(Hash.new.tap do |h|
|
55
|
+
h["file"] = file
|
56
|
+
h["line_number"] = index + 1
|
57
|
+
h["string"] = match[0]
|
58
|
+
h["type"] = "translatedWithContext"
|
59
|
+
h["context"] = get_context(line, h["string"])
|
60
|
+
end)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
results
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.get_context(line, match)
|
67
|
+
line.match(/"#{match}"\.translatedWithContext\("([^"]*)"\)/)[1]
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.find_variables(string)
|
71
|
+
# tries to find \(asd) inside the string itself
|
72
|
+
string.scan(VARIABLE_REGEX).map {|matches| matches[0]}
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.is_view_controller?(file)
|
76
|
+
!file.match(/ViewController/).nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.looks_suspicious(line)
|
80
|
+
without_strings = line.gsub(STRING_REGEX, "")
|
81
|
+
without_strings.include?("_LOG") ||
|
82
|
+
without_strings.include?("DLog") ||
|
83
|
+
without_strings.include?("NSLog") ||
|
84
|
+
without_strings.include?("NSAssert") ||
|
85
|
+
without_strings.downcase.include?("uistoryboard") ||
|
86
|
+
without_strings.downcase.include?("instantiateviewcontrollerwithidentifier") ||
|
87
|
+
without_strings.downcase.include?("uiimage") ||
|
88
|
+
without_strings.downcase.include?("nsentitydescription") ||
|
89
|
+
without_strings.downcase.include?("nspredicate") ||
|
90
|
+
without_strings.downcase.include?("dateformat") ||
|
91
|
+
without_strings.downcase.include?("datefromstring") ||
|
92
|
+
without_strings.downcase.include?("==") ||
|
93
|
+
without_strings.downcase.include?("isequaltostring") ||
|
94
|
+
without_strings.downcase.include?("valueforkey") ||
|
95
|
+
without_strings.downcase.include?("cellidentifier") ||
|
96
|
+
without_strings.downcase.include?("uifont") ||
|
97
|
+
without_strings.downcase.include?("print(")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'terrestrial/cli/parser/base_parser'
|
2
|
+
require 'terrestrial/cli/parser/objc'
|
3
|
+
require 'terrestrial/cli/parser/swift'
|
4
|
+
require 'terrestrial/cli/parser/storyboard'
|
5
|
+
require 'terrestrial/cli/parser/android_xml'
|
6
|
+
require 'terrestrial/cli/parser/string_analyser'
|
7
|
+
|
8
|
+
module Terrestrial
|
9
|
+
module Cli
|
10
|
+
module Parser
|
11
|
+
|
12
|
+
def self.find_strings(file)
|
13
|
+
EngineMapper.parser_for(File.extname(file)).find_strings(file)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find_api_calls(file)
|
17
|
+
EngineMapper.parser_for(File.extname(file)).find_api_calls(file)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.find_nslocalizedstrings(file)
|
21
|
+
EngineMapper.parser_for(File.extname(file)).find_nslocalizedstrings(file)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|