Skip to content
Merged
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
7 changes: 4 additions & 3 deletions ls/server/src/connection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ and retains normal function resolution.
## Argument documentation

Signature help and call-argument hover use `SimpleCommand.arguments`. A quoted
or multiline argument remains one syntax word. Bare documented `name=value`
arguments select their named parameter; positional words select their ordinal
parameter and then a documented rest parameter. This describes syntax words;
or multiline argument remains one syntax word. Documented `name=value`
arguments select their named parameter after shell quoting is removed from the
static key, including `"name=value"` and `"name=$value"`; positional words select
their ordinal parameter and then a documented rest parameter. This describes syntax words;
runtime expansion can produce a different number of shell arguments.

Within a function, argument hover recognizes direct positional parameter parts
Expand Down
6 changes: 5 additions & 1 deletion ls/server/src/connection/ast/word.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { WordNode } from '../../zshParser/ast';
import { hereDocDelimiter } from '../../zshParser/lexer/delimiter';

const RESERVED_COMMANDS: readonly string[] = [
'if', 'then', 'else', 'elif', 'fi', 'for', 'select', 'foreach', 'repeat',
Expand All @@ -15,8 +16,11 @@ export function commandInsertion(name: string): string {
return "'" + name.split("'").join(escapedQuote) + "'";
}

/** Read a named key from the static word prefix, removing shell quoting without evaluating expansions. */
export function namedKey(word: WordNode): string | null {
const text = word.raw;
const prefix = word.parts[0];
if (prefix?.kind !== 'stringLiteral') return null;
const text = hereDocDelimiter(prefix.value).delimiter;
let index = 0;
while (index < text.length) {
const char = text[index];
Expand Down
8 changes: 5 additions & 3 deletions ls/server/src/diagnostics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ shell environment was obtained successfully.
## Parameter contracts

Required arguments come from `documentationOf(definition).parameters`. Each
syntax word supplies one positional argument unless it has a documented bare
`name=value` key. Position numbers, optional parameters and rest parameters are
read directly from documentation nodes. A required `$3` needs three positional
syntax word supplies one positional argument unless its static prefix has a
documented `name=value` key after shell quote removal. This includes
`name="value"`, `"name=value"` and `"name=$value"`. A key that depends on an
expansion is not treated as a statically known name. Position numbers, optional
parameters and rest parameters are read directly from documentation nodes. A required `$3` needs three positional
words even if `$1` and `$2` are absent from the documentation.

Quoted scalar values count as one word. Words whose expansions or global aliases
Expand Down
10 changes: 10 additions & 0 deletions ls/server/test/connection/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ describe('signature help@AST', () => {
['greet ¦', 0], ['greet "hello world" ¦', 1],
['greet "hello\nworld" ¦', 1], ['greet a ' + String.fromCharCode(92) + '\n ¦', 1],
['greet one=value ¦', 0], ['greet two=val¦ue', 3],
['greet "one=value" ¦', 0], ['greet "two=val¦ue"', 3],
['greet "two=$val¦ue"', 3], ["greet 'two=val¦ue'", 3],
['greet "two=value" one=value ¦', 0], ['greet "two=value" first ¦', 1],
['greet "${key}=val¦ue"', 0], ['greet "tw${key}=val¦ue"', 0],
['greet "hello\nworld" ¦', 1], ['greet a ' + String.fromCharCode(92) + '\n ¦', 1],
['print "$(greet a ¦)"', 1], ['print x | greet ¦', 0],
['greet a; greet ¦', 0],
Expand Down Expand Up @@ -176,6 +180,12 @@ describe('argument hover@AST', () => {
contents: { value: expect.stringContaining('first') },
});
});
test.each(['greet "two=val¦ue"', 'greet "two=$val¦ue"', "greet 'two=val¦ue'",
'greet "one=value" "two=val¦ue"'])('引用した名前付き引数の説明を返す: %s', source => {
expect(fixture(source).hover()).toMatchObject({
contents: { value: expect.stringContaining('option two') },
});
});
test.each(['print "$¦1"', 'local value="$1"; print "$val¦ue"',
'local value=${1}; print "$val¦ue"', 'local value=$1; print "${val¦ue}"'])('位置引数・そのalias: %s', body => {
const f = fixture('# $1: local first\nowner() { ' + body + ' }\n');
Expand Down
32 changes: 32 additions & 0 deletions ls/server/test/connection/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,38 @@ describe('server@hydration stdio', () => {
}
}, 15000);

test('引用した名前付き引数の診断・署名・hoverを実サーバーで返す', async () => {
const root = fixture();
const caller = pathToFileURL(path.join(root, 'caller.zsh')).href;
fs.writeFileSync(path.join(root, 'definition.zsh'), [
'# $1: first', '# $message: error message',
'z.t._log.failure.handle() { print "$1" "$message" }', '',
].join('\n'));
const client = startServer();
const source = 'z.t._log.failure.handle "message=failed: expected [ $expect_display ] but got [ $actual_display ]" first\n';
const request = () => bounded(client.connection.sendRequest(DocumentDiagnosticRequest.type, {
textDocument: { uri: caller },
}));
try {
await client.initialize(root, false);
await client.open(caller, source);
expect(await request()).toMatchObject({ kind: 'full', items: [] });
const position = { line: 0, character: source.indexOf('expected') };
expect(await bounded(client.connection.sendRequest(SignatureHelpRequest.type, {
textDocument: { uri: caller }, position,
}))).toMatchObject({ activeParameter: 1, signatures: [{ activeParameter: 1 }] });
expect(await bounded(client.connection.sendRequest(HoverRequest.type, {
textDocument: { uri: caller }, position,
}))).toMatchObject({ contents: { value: expect.stringContaining('error message') } });
await client.edit(caller, 'z.t._log.failure.handle first\n', 2);
expect(await request()).toMatchObject({ kind: 'full', items: [{
code: 'missing-named-argument', message: 'Missing required named argument: message',
}] });
} finally {
await client.close();
}
}, 15000);

test('AST診断・環境解決・抑制・依存先変更を実サーバーで反映する', async () => {
const root = fixture();
const definition = path.join(root, 'definition.zsh');
Expand Down
24 changes: 23 additions & 1 deletion ls/server/test/diagnostics/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,29 @@ describe('call diagnostics@AST', () => {
const doc = '# $option: value\ngreet() { print "$option" }\n';
expect(semantic(fixture('greet option=""\n', doc).items)).toEqual([]);
expect(semantic(fixture('greet option=\n', doc).items)).toEqual([]);
expect(semantic(fixture('greet "option=value"\n', doc).items)).toMatchObject([{ code: 'missing-named-argument' }]);
expect(semantic(fixture('greet "option=value"\n', doc).items)).toEqual([]);
});
test.each(['"option=value with spaces"', '"option="', '"option=$value"',
"'option=value with spaces'", 'op"tion"=value', '"option"="$value"',
'option="value"', 'op\\tion=value'])('引用を除いた静的な名前付き引数を照合する: %s', argument => {
const doc = '# $option: value\ngreet() { print "$option" }\n';
expect(semantic(fixture('greet ' + argument + '\n', doc).items)).toEqual([]);
});
test.each(['"other=value"', '"9option=value"', '"option-extra=value"', '"option"'])('引用しても不足した名前付き引数を検出する: %s', argument => {
const doc = '# $option: value\ngreet() { print "$option" }\n';
expect(semantic(fixture('greet ' + argument + '\n', doc).items))
.toMatchObject([{ code: 'missing-named-argument', message: 'Missing required named argument: option' }]);
});
test('引用した名前付き引数を位置引数へ数えない', () => {
const doc = '# $1: first\n# $option: value\ngreet() { print "$1" "$option" }\n';
expect(semantic(fixture('greet "option=value"\n', doc).items))
.toMatchObject([{ code: 'missing-positional-argument', message: 'Missing required argument(s): $1' }]);
expect(semantic(fixture('greet "option=$value" first\n', doc).items)).toEqual([]);
});
test('失敗ログの実例を名前付き引数として認識する', () => {
const doc = '# $message: error message\nz.t._log.failure.handle() { print "$message" }\n';
const source = 'z.t._log.failure.handle "message=failed: expected [ $expect_display ] but got [ $actual_display ]"\n';
expect(semantic(fixture(source, doc).items)).toEqual([]);
});
});

Expand Down
Loading