terrestrial-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|