Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
<!-- Add all new changes here. They will be moved under a version at release -->
* `NEW` `---@field (read)` / `---@field (write)` declare different types for reading and writing a field; plain `---@field` still covers both [#1298](https://github.com/LuaLS/lua-language-server/issues/1298)

## 3.19.2
`2026-10-13`
Expand Down
18 changes: 17 additions & 1 deletion locale/en-us/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -912,8 +912,11 @@ Declare a field in a class/table. This allows you to provide more in-depth
documentation for a table. As of `v3.6.0`, you can mark a field as `private`,
`protected`, `public`, or `package`.

A field can declare a different type for reading and for writing with the
`(read)` and `(write)` attributes. A plain `---@field` is used for both.

## Syntax
`---@field [scope] <name> <type> [description]`
`---@field [(read|write)] [scope] <name> <type> [description]`

## Usage
```
Expand All @@ -934,6 +937,19 @@ response = get("localhost")
--Extension provided intellisense for the below assignment
statusCode = response.status.code
```

### Different read and write types
```
---@class Entity
---@field (read) force Force
---@field (write) force Force|string

---@type Entity
local entity

entity.force = "player" -- checked against `Force|string`
local force = entity.force -- `Force`
```
---
[View Wiki](https://luals.github.io/wiki/annotations#field)
]=]
Expand Down
2 changes: 1 addition & 1 deletion locale/es-419/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ marcar un campo como privado (`private`), protegido (`protected`), público
(`public`) o en-paquete (`package`).

## Sintaxis
`---@field [alcance] <nombre> <tipo> [descripción]`
`---@field [(read|write)] [alcance] <nombre> <tipo> [descripción]`

## Uso
```
Expand Down
2 changes: 1 addition & 1 deletion locale/ja-jp/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ LUADOC_DESC_FIELD =
クラス/テーブル内のフィールドを宣言します。`v3.6.0`以降では、フィールドを`private`、`protected`、`public`、または`package`として指定することができます。

## 書き方
`---@field [scope] <name> <type> [description]`
`---@field [(read|write)] [scope] <name> <type> [description]`

## 例
```
Expand Down
2 changes: 1 addition & 1 deletion locale/pt-br/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ para uma tabela. A partir da versão `v3.6.0`, você pode marcar um campo como `
`protected`, `public`, ou `package`.

## Sintaxe
`---@field <nome> <tipo> [descrição]`
`---@field [(read|write)] <nome> <tipo> [descrição]`

## Uso
```
Expand Down
2 changes: 1 addition & 1 deletion locale/zh-cn/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ LUADOC_DESC_FIELD =
`protected`, `public` 或 `package`。

## 语法
`---@field <name> <type> [description]`
`---@field [(read|write)] <name> <type> [description]`

## 用法
```
Expand Down
2 changes: 1 addition & 1 deletion locale/zh-tw/script.lua
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ LUADOC_DESC_FIELD =
在類別/表中宣告一個欄位。 這使你可以為表提供更深入詳細的文件。

## 語法
`---@field <name> <type> [description]`
`---@field [(read|write)] <name> <type> [description]`

## 用法
```
Expand Down
7 changes: 7 additions & 0 deletions script/core/completion/completion.lua
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,13 @@ local function checkFieldOfRefs(refs, state, word, startPos, position, parent, o
count = count + 1
goto CONTINUE
end
if last
and vm.getFieldAccess(last) == 'write'
and src.type == 'doc.field'
and vm.getFieldAccess(src) ~= 'write' then
fields[name] = src
goto CONTINUE
end
if vm.getDeprecated(src) then
goto CONTINUE
end
Expand Down
2 changes: 1 addition & 1 deletion script/core/diagnostics/assign-type-mismatch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ return function (uri, callback)
valueNode = valueNode:copy():setTruthy()
end

local varNode = vm.compileNode(source)
local varNode = vm.compileWriteNode(source)
local errs = {}
if vm.canCastType(uri, varNode, valueNode, errs) then
return
Expand Down
5 changes: 5 additions & 0 deletions script/core/diagnostics/duplicate-doc-field.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ return function (uri, callback)
if view ~= myView then
goto CONTINUE
end
local myAccess = vm.getFieldAccess(myField)
local access = vm.getFieldAccess(field)
if myAccess and access and myAccess ~= access then
goto CONTINUE
end
callback {
start = myField.field.start,
finish = myField.field.finish,
Expand Down
13 changes: 11 additions & 2 deletions script/core/hover/label.lua
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ end

---@async
---@param level integer
local function asValue(source, title, level)
---@param node? vm.node
local function asValue(source, title, level, node)
local name = buildName(source, false) or ''
local ifr = vm.getInfer(source)
local ifr = vm.getInfer(node or source)
local type = ifr:view(guide.getUri(source))
local literal = ifr:viewLiterals()
local cont, maxLevel = buildTable(source, level)
Expand Down Expand Up @@ -137,6 +138,14 @@ local function asField(source, level)
if isGlobalField(source) then
return asGlobal(source, level)
end
local setter = source
if source.type == 'field' or source.type == 'method' then
setter = source.parent
end
local writeNode = vm.compileWriteNode(setter)
if writeNode ~= vm.compileNode(setter) then
return asValue(source, '(field)', level, writeNode)
end
return asValue(source, '(field)', level)
end

Expand Down
12 changes: 12 additions & 0 deletions script/core/hover/table.lua
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,23 @@ end
local function getNodeMap(uri, fields, keyMap)
---@type table<string, vm.node>
local nodeMap = {}
local readableKeys = {}
for _, field in ipairs(fields) do
if field.type == 'doc.field' and vm.getFieldAccess(field) ~= 'write' then
local key = vm.viewKey(field, uri)
if key then
readableKeys[key] = true
end
end
end
for _, field in ipairs(fields) do
local key = vm.viewKey(field, uri)
if not key or not keyMap[key] then
goto CONTINUE
end
if readableKeys[key] and vm.getFieldAccess(field) == 'write' then
goto CONTINUE
end
await.delay()
local node = vm.compileNode(field)
if nodeMap[key] then
Expand Down
2 changes: 1 addition & 1 deletion script/parser/guide.lua
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ local childMap = {
['doc.enum'] = {'enum', 'extends', 'comment', 'docAttr'},
['doc.param'] = {'param', 'extends', 'comment'},
['doc.return'] = {'#returns', 'comment'},
['doc.field'] = {'field', 'extends', 'comment'},
['doc.field'] = {'docAttr', 'field', 'extends', 'comment'},
['doc.generic'] = {'#generics', 'comment'},
['doc.generic.object'] = {'generic', 'extends', 'comment'},
['doc.vararg'] = {'vararg', 'comment'},
Expand Down
1 change: 1 addition & 0 deletions script/parser/luadoc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1218,6 +1218,7 @@ local docSwitch = util.switch()
local result = {
type = 'doc.field',
}
result.docAttr = parseDocAttr(result)
try(function ()
local tp, value = nextToken()
if tp == 'name' then
Expand Down
44 changes: 43 additions & 1 deletion script/vm/compiler.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1054,9 +1054,14 @@ end
---@param source parser.object | vm.variable
---@param key string|vm.global|vm.ANY|vm.ANYDOC
---@param pushResult fun(source: parser.object)
function vm.compileByParentNode(source, key, pushResult)
---@param access? vm.field.access # defaults to `read`
function vm.compileByParentNode(source, key, pushResult, access)
local docedResults, commonResults = vm.getNodesOfParentNode(source, key)

if key ~= vm.ANY and key ~= vm.ANYDOC then
docedResults = vm.filterFieldsByAccess(docedResults, access or 'read')
end

if #docedResults > 0 then
for _, res in ipairs(docedResults) do
pushResult(res)
Expand Down Expand Up @@ -2805,3 +2810,40 @@ function vm.compileNode(source)
---@cast node -?
return node
end

local writeSourceTypes = util.arrayToHash {
'setfield',
'setmethod',
'setindex',
'tablefield',
'tableindex',
}

-- The type a value must have to be assigned to `source`.
-- Differs from `vm.compileNode(source)` only when the field is declared
-- with both `---@field (read)` and `---@field (write)`; `vm.compileNode`
-- keeps the read type so narrowing after an assignment stays correct.
---@param source parser.object
---@return vm.node
function vm.compileWriteNode(source)
local readNode = vm.compileNode(source)
if not writeSourceTypes[source.type] then
return readNode
end
if source.bindDocs and vm.bindDocs(source) then
return readNode
end
local key = guide.getKeyName(source)
if type(key) ~= 'string' then
return readNode
end
local writeNode
vm.compileByParentNode(source.node, key, function (src)
if vm.getFieldAccess(src) ~= 'write' then
return
end
writeNode = writeNode or vm.createNode()
writeNode:merge(vm.compileNode(src))
end, 'write')
return writeNode or readNode
end
40 changes: 40 additions & 0 deletions script/vm/doc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -524,3 +524,43 @@ function vm.docHasAttr(doc, key)
end
return false
end

---@alias vm.field.access 'read' | 'write'

---@param doc parser.object
---@return vm.field.access?
function vm.getFieldAccess(doc)
if doc.type ~= 'doc.field' then
return nil
end
local read = vm.docHasAttr(doc, 'read')
local write = vm.docHasAttr(doc, 'write')
if read == write then
return nil
end
return read and 'read' or 'write'
end

-- `---@field (read)` and `---@field (write)` only narrow which declaration
-- applies when the other direction is also declared; a one-sided field is
-- used for both reads and writes, like a plain `---@field`.
---@param results parser.object[]
---@param access vm.field.access
---@return parser.object[]
function vm.filterFieldsByAccess(results, access)
local filtered = {}
local hasDocField
for _, res in ipairs(results) do
local resAccess = vm.getFieldAccess(res)
if resAccess == nil or resAccess == access then
filtered[#filtered+1] = res
if res.type == 'doc.field' then
hasDocField = true
end
end
end
if not hasDocField then
return results
end
return filtered
end
15 changes: 15 additions & 0 deletions script/vm/type.lua
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,17 @@ local function checkTableShape(parent, child, uri, mark, errs)
end
end

-- a table literal writes its fields, so `(write)` declarations win
local writeKeys = {}
for _, field in ipairs(def.fields) do
if vm.getFieldAccess(field) == 'write' then
local key = vm.getKeyName(field)
if key then
writeKeys[key] = true
end
end
end

for _, field in ipairs(def.fields) do
local key = vm.getKeyName(field)
if not key then
Expand All @@ -320,6 +331,9 @@ local function checkTableShape(parent, child, uri, mark, errs)
if not key then
goto continue
end
if writeKeys[key] and vm.getFieldAccess(field) == 'read' then
goto nextField
end

local ok
local nodeField = vm.compileNode(field)
Expand All @@ -341,6 +355,7 @@ local function checkTableShape(parent, child, uri, mark, errs)
end
failedCheck = true
end
::nextField::
end
::continue::
end
Expand Down
Loading
Loading