From 22d7f6c23be209c7b5de149f18efa850f84915f1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=80=E8=90=8C=E5=B0=8F=E6=B1=90?= <sumneko@hotmail.com>
Date: Mon, 23 Aug 2021 14:58:17 +0800
Subject: [PATCH] resolve #624 `different-requires`

---
 changelog.md                                  |   2 +
 locale/en-us/script.lua                       |   1 +
 locale/zh-cn/script.lua                       |   1 +
 .../core/diagnostics/different-requires.lua   |  52 +++++++
 script/parser/guide.lua                       |   3 +
 script/proto/define.lua                       |   2 +
 test/crossfile/diagnostic.lua                 | 137 ++++++++++++++++++
 test/crossfile/init.lua                       |   1 +
 test/diagnostics/init.lua                     |   1 -
 9 files changed, 199 insertions(+), 1 deletion(-)
 create mode 100644 script/core/diagnostics/different-requires.lua
 create mode 100644 test/crossfile/diagnostic.lua

diff --git a/changelog.md b/changelog.md
index ed39ed42f..f5f9eac01 100644
--- a/changelog.md
+++ b/changelog.md
@@ -6,6 +6,8 @@
   + `Lua.diagnostics.ignoredFiles`
   + `Lua.completion.showWord`
   + `Lua.completion.requireSeparator`
+* `NEW` diagnostics:
+  + `different-requires`
 * `CHG` hover: improve showing multi defines
 * `CHG` hover: improve showing multi comments at enums
 * `CHG` hint: `Lua.hint.paramName` now supports `Disable`, `Literal` and `All`
diff --git a/locale/en-us/script.lua b/locale/en-us/script.lua
index 35e1df8f7..16a66b0a0 100644
--- a/locale/en-us/script.lua
+++ b/locale/en-us/script.lua
@@ -41,6 +41,7 @@ DIAG_COSE_NON_OBJECT    = 'Cannot close a value of this type. (Unless set `__clo
 DIAG_COUNT_DOWN_LOOP    = 'Do you mean `{}` ?'
 DIAG_IMPLICIT_ANY       = 'Can not infer type.'
 DIAG_DEPRECATED         = 'Deprecated.'
+DIAG_DIFFERENT_REQUIRES = 'The same file is required with different names.'
 
 DIAG_CIRCLE_DOC_CLASS                 = 'Circularly inherited classes.'
 DIAG_DOC_FIELD_NO_CLASS               = 'The field must be defined after the class.'
diff --git a/locale/zh-cn/script.lua b/locale/zh-cn/script.lua
index 947873042..f1e9d95fa 100644
--- a/locale/zh-cn/script.lua
+++ b/locale/zh-cn/script.lua
@@ -41,6 +41,7 @@ DIAG_COSE_NON_OBJECT    = '无法 close 此类型的值。(除非给此类型
 DIAG_COUNT_DOWN_LOOP    = '你的意思是 `{}` 吗?'
 DIAG_IMPLICIT_ANY       = '无法推测出类型。'
 DIAG_DEPRECATED         = '已废弃。'
+DIAG_DIFFERENT_REQUIRES = '使用了不同的名字 require 了同一个文件。'
 
 DIAG_CIRCLE_DOC_CLASS                 = '循环继承的类。'
 DIAG_DOC_FIELD_NO_CLASS               = '字段必须定义在类之后。'
diff --git a/script/core/diagnostics/different-requires.lua b/script/core/diagnostics/different-requires.lua
new file mode 100644
index 000000000..9e3dfc8fc
--- /dev/null
+++ b/script/core/diagnostics/different-requires.lua
@@ -0,0 +1,52 @@
+local files   = require 'files'
+local guide   = require 'parser.guide'
+local lang    = require 'language'
+local config  = require 'config'
+local vm      = require 'vm'
+local ws      = require 'workspace'
+
+return function (uri, callback)
+    local state = files.getState(uri)
+    if not state then
+        return
+    end
+    local cache = vm.getCache 'different-requires'
+    guide.eachSpecialOf(state.ast, 'require', function (source)
+        local call = source.next
+        if not call or call.type ~= 'call' then
+            return
+        end
+        local arg1 = call.args and call.args[1]
+        if not arg1 or arg1.type ~= 'string' then
+            return
+        end
+        local literal = arg1[1]
+        local results = ws.findUrisByRequirePath(literal)
+        local result = results and results[1]
+        if not result then
+            return
+        end
+        local other = cache[result]
+        if not other then
+            cache[result] = {
+                source  = arg1,
+                require = literal,
+            }
+            return
+        end
+        if other.require ~= literal then
+            callback {
+                start   = arg1.start,
+                finish  = arg1.finish,
+                related = {
+                    {
+                        start  = other.source.start,
+                        finish = other.source.finish,
+                        uri    = guide.getUri(other.source),
+                    }
+                },
+                message = lang.script('DIAG_DIFFERENT_REQUIRES'),
+            }
+        end
+    end)
+end
diff --git a/script/parser/guide.lua b/script/parser/guide.lua
index 378d26139..6dcfbf956 100644
--- a/script/parser/guide.lua
+++ b/script/parser/guide.lua
@@ -722,6 +722,9 @@ function m.eachSource(ast, callback)
 end
 
 --- 获取指定的 special
+---@param ast      parser.guide.object
+---@param name     string
+---@param callback fun(source: parser.guide.object)
 function m.eachSpecialOf(ast, name, callback)
     local root = m.getRoot(ast)
     if not root.specials then
diff --git a/script/proto/define.lua b/script/proto/define.lua
index a07a39037..9fbbea354 100644
--- a/script/proto/define.lua
+++ b/script/proto/define.lua
@@ -74,6 +74,7 @@ m.DiagnosticDefaultSeverity = {
     ['count-down-loop']         = 'Warning',
     ['no-implicit-any']         = 'Information',
     ['deprecated']              = 'Warning',
+    ['different-requires']      = 'Warning',
 
     ['duplicate-doc-class']     = 'Warning',
     ['undefined-doc-class']     = 'Warning',
@@ -125,6 +126,7 @@ m.DiagnosticDefaultNeededFileStatus = {
     ['count-down-loop']         = 'Any',
     ['no-implicit-any']         = 'None',
     ['deprecated']              = 'Opened',
+    ['different-requires']      = 'Any',
 
     ['duplicate-doc-class']     = 'Any',
     ['undefined-doc-class']     = 'Any',
diff --git a/test/crossfile/diagnostic.lua b/test/crossfile/diagnostic.lua
new file mode 100644
index 000000000..3d4eb5523
--- /dev/null
+++ b/test/crossfile/diagnostic.lua
@@ -0,0 +1,137 @@
+local files    = require 'files'
+local furi     = require 'file-uri'
+local core     = require 'core.diagnostics'
+local config   = require 'config'
+local platform = require 'bee.platform'
+
+config.get 'Lua.diagnostics.neededFileStatus'['deprecated'] = 'Any'
+
+rawset(_G, 'TEST', true)
+
+local function catch_target(script, sep)
+    local list = {}
+    local cur = 1
+    local cut = 0
+    while true do
+        local start, finish  = script:find(('<%%%s.-%%%s>'):format(sep, sep), cur)
+        if not start then
+            break
+        end
+        list[#list+1] = { start - cut, finish - 4 - cut }
+        cur = finish + 1
+        cut = cut + 4
+    end
+    local new_script = script:gsub(('<%%%s(.-)%%%s>'):format(sep, sep), '%1')
+    return new_script, list
+end
+
+local function founded(targets, results)
+    if #targets ~= #results then
+        return false
+    end
+    for _, target in ipairs(targets) do
+        for _, result in ipairs(results) do
+            if target[1] == result[1]
+            and target[2] == result[2]
+            and target[3] == result[3]
+            then
+                goto NEXT
+            end
+        end
+        do return false end
+        ::NEXT::
+    end
+    return true
+end
+
+function TEST(datas)
+    files.removeAll()
+
+    local targetList = {}
+    local sourceUri
+    for _, data in ipairs(datas) do
+        local uri = furi.encode(data.path)
+        local new, list = catch_target(data.content, '!')
+        for _, position in ipairs(list) do
+            targetList[#targetList+1] = {
+                position[1],
+                position[2],
+                uri,
+            }
+        end
+        data.content = new
+        files.setText(uri, new)
+    end
+
+    local result = {}
+    for _, data in ipairs(datas) do
+        local uri = furi.encode(data.path)
+        local results = {}
+        core(uri, function (result)
+            for _, res in ipairs(result) do
+                results[#results+1] = res
+            end
+        end)
+        for i, position in ipairs(results) do
+            result[i] = {
+                position.start,
+                position.finish,
+                uri,
+            }
+        end
+    end
+    assert(founded(targetList, result))
+end
+
+TEST {
+    {
+        path = 'f/a.lua',
+        content = '',
+    },
+    {
+        path = 'b.lua',
+        content = 'require "a"',
+    },
+    {
+        path = 'c.lua',
+        content = 'require <!"f.a"!>',
+    },
+}
+
+TEST {
+    {
+        path = 'f/a.lua',
+        content = '',
+    },
+    {
+        path = 'a.lua',
+        content = '',
+    },
+    {
+        path = 'b.lua',
+        content = 'require "a"',
+    },
+    {
+        path = 'c.lua',
+        content = 'require "f.a"',
+    },
+}
+
+TEST {
+    {
+        path = 'a.lua',
+        content = '',
+    },
+    {
+        path = 'f/a.lua',
+        content = '',
+    },
+    {
+        path = 'b.lua',
+        content = 'require "a"',
+    },
+    {
+        path = 'c.lua',
+        content = 'require "f.a"',
+    },
+}
diff --git a/test/crossfile/init.lua b/test/crossfile/init.lua
index a7402815f..1ed2a9430 100644
--- a/test/crossfile/init.lua
+++ b/test/crossfile/init.lua
@@ -3,3 +3,4 @@ require 'crossfile.references'
 require 'crossfile.allreferences'
 require 'crossfile.hover'
 require 'crossfile.completion'
+require 'crossfile.diagnostic'
diff --git a/test/diagnostics/init.lua b/test/diagnostics/init.lua
index b90b4b260..54ac73eff 100644
--- a/test/diagnostics/init.lua
+++ b/test/diagnostics/init.lua
@@ -2,7 +2,6 @@ local core   = require 'core.diagnostics'
 local files  = require 'files'
 local config = require 'config'
 local util   = require 'utility'
-local define = require 'proto.define'
 
 config.get 'Lua.diagnostics.neededFileStatus'['deprecated'] = 'Any'
 
-- 
GitLab